MCP and Go, making new tools for AI is easy

- By Conor Hodder
Blog post image

AI Tools are the key to implementing successful agents, and with the rise of larger and larger parameterised models the implementation of new agents is becoming simpler, faster and more accessible. What they’re missing is the tools to actually do their job, so let’s use MCP to create one.

What is Anthropic’s Model Context Protocol (MCP)?

Before we can look at how to write a tool using MCP we should talk about what it is and what it isn’t.

Simply, it’s a definition for how two applications should communicate with each other – that’s it.

This is one of the first challenges most teams face when defining ecosystems, so it should sound familiar: “How do we expose the tooling we have to others without exposing the complexity?”

Notice this has nothing to do with AI – the tools you’re building could be for AIs, but they’re not necessarily made of them.

Who are we building for?

This analogy becomes a lot easier when you think of it in service of a User Persona. Let’s use an example of a carpenter named Timberly.

Timberly has been working for over 30 years – they’re battle hardened, know all the tricks of the trade and all the shortcuts they possibly could need to get a job done. However, while they’re armed with all the knowledge in the world, they need some tools to be able to effectively apply that knowledge to their work.

Timberly has recently been asked to build a dresser for a customer and they have now commissioned you to source them these tools. The customer would like a simple dresser with three drawers. Each drawer should be 1 foot tall, and the whole dresser should be 4 feet wide and 2 feet deep.

Making a tool

Let’s go about building Timberly a way to calculate how large a wood sheet they need to buy (we’re keeping this example simple, so please ignore the lack of things like blade width in here). To start with, Timberbly just wants a tool that works. They don’t care if it’s efficient, they just need something to get started.

type WoodSheet struct {
    Width uint32
    Height uint32
}

func DetermineSheetSize(width, depth, drawerCount, drawerHeight uint32) WoodSheet {
    return WoodSheet{
        // We are just building out a massive array here, and we know that this 
        // is incredibly inefficient, but for Timerbly's purpose it will work for now.
        // They can reuse the leftover wood for other projects
        Width: width * depth * 2 * drawerCount
        Height: width * depth
    }
}

Well great, that code will technically work (the best kind of working). This is the full domain logic, it is incredibly easy to test and validate and complexity can scale independently of the presentation layer.

Making the Toolbox

However, Timberly is a carpenter: they don’t know how to use Go. We need to find a way to explain the tool I’ve just built, and any future tools we may build. This is where MCP comes to the rescue!

I’m going to use mcp-golang to write a server for this.

type DresserWoodSheetCalculatorArgs struct {
    // The json tags below define how a client will provide the information.
    // The jsonschema  tags define whether it is required or optional, and describe what the field is intended to contain.
    // We can type the fields here as well to define what the format should be,
    // in this example all arguments are numbers, but they could be any primitive
    Width        uint32 `json:"width" jsonschema:"required,description=The width of the piece of the dresser"`
    Depth        uint32 `json:"depth" jsonschema:"required,description=The height of the piece of the dresser"`
    DrawerCount  uint32 `json:"drawerCount" jsonschema:"required,description=The number of drawers in the dresser"`
    DrawerHeight uint32 `json:"drawerHeight" jsonschema:"required,description=The height of each drawer"`
}

func main() {
    // We make an empty struct that takes no memory so we can wait on it down below
    // there are other alternatives for how to make it wait forever, but this is probably the cheapest
    done := make(chan struct{})

    // For this example we are going to use STDIO, however you could make this an external HTTP server as well
    server := mcp_golang.NewServer(stdio.NewStdioServerTransport())

    // We want to register the new tool in a way that lets users know about it
    err := server.RegisterTool(
        // We define the name of the tool that we want to expose. This is used to uniquely identify the tools
        // across the entire MCP ecosystem
        "wood_sheet_calculator",

        // The description of the tool defines when it should be used, so keep it descriptive for Timberly
        "Determines what size wood to purchase that can be cut up to construct a dresser",

        // This is the function to run when the tool is executed, so the consumer knows they need to provide
        // the args and that they will get back their answer
        func(args DresserWoodSheetCalculatorArgs) (*mcp_golang.ToolResponse, error) {
            // Get the sheet here so we don't have to mess with the mcp tooling in the domain logic
            sheet := DetermineSheetSize(args.Width, args.Depth, args.DrawerCount, args.DrawerHeight)
            return mcp_golang.NewToolResponse(
                mcp_golang.NewTextContent(sheet.String()),
            ), nil
        })
    if err != nil {
        panic(err)
    }
    
    // Serve the server
    if serveErr := server.Serve(); serveErr != nil {
        panic(serveErr)
    }

    // Wait for the channel to be closed which will never occur, but also take up the least amount of CPU
    <-done
}

Exploring the tool we’ve just made

Before we give our wood sheet calculator tool to Timberly, we want to make sure that it’s working as intended. There are a few utilities we can use, but my preferred way to do this is via Anthropic’s Inspector. With this, I can just run npx @modelcontextprotocol/inspector which will expose a nice GUI on port 8080 that I can use to open the toolbox.

I need to tell Inspector where my toolbox is (a local executable or a docker image) and how to open it (STDIO or SSE). I also need to add any arguments or environment variables that should be passed to the toolbox. In this example we don’t have any, but this could be API keys or runtime configuration.

MCP Inspector Setup Interface

Once I have it connected I can begin exploring the toolbox:

MCP Tool Run Interface

Okay perfect – I now can not only see the wood sheet calculator tool, but also I can test it by putting in some values. Because I’ve told the tool what kind of inputs I’m expecting, it won’t let me put in bad parameters (so I can’t ask for a width of “potatoes”) – which shows the MCP toolbox knows what the wood sheet calculator needs to do its job effectively.

Giving Timberly the tool

I’m going to hand-wave and create a simulated Timberly for this (expect to see some more content regarding building Agentic AI’s coming soon). Then I’m going to hook our synthetic Timberly up to use this MCP toolbox.

Every IDE has a slightly different example for how to do this, but in this example I’m going to hook it up to Cursor. I open Cursor, select “Cursor Settings” and set up a “New Global MCP Server”. I give it a path to the local executable I’ve built, and now I can chat to Cursor and it knows that it has access to the MCP toolbox.

Settings for adding a new MCP Server

Time to tell Timberly what I want and try out my tool. In this, I am going to pretend to be the customer and ask Timberly for what I want just like a real customer would. Timberly will then look through the toolbox and determine the best tool for this job and let me know what the next step is.

Chatting with Timberly

So not only has Timberly figured out the tool exists, they know how to use it and how to interpret the results. There are a few bugs in this system, including one Timerbly has caught, which is that we’re using an extremely inefficient piece of wood for this.

The second bug that it got to (with a bit more prompting) is that Timberly is not actually able to cut the wood because Timberly is not an AI, not a human, so we need to add a new tool into the MCP toolbox. This time, we’re going to explain to how to cut the wood.

I’m just going to stub it out for the purposes of this example.

    type CutWoodArgs struct {
        Sheet     WoodSheet   `json:"sheet" jsonschema:"required,description=The source sheet of wood to cut up"`
        CutSheets []WoodSheet `json:"targetSheets" jsonschema:"required,description=The sheets that the caller wants cut"`
    }

    err = server.RegisterTool(
        "wood_cutter",
        "cut a single board of wood into multiple smaller pieces",
        func(args CutWoodArgs) (*mcp_golang.ToolResponse, error) {
            // Pretend a factory has now cut the wood up
            c, _ := json.Marshal(args.CutSheets)

            return mcp_golang.NewToolResponse(
                mcp_golang.NewTextContent(string(c)),
            ), nil
        })
    if err != nil {
        panic(err)
    }

And now I can recompile the MCP toolbox, and ask Timberly to cut this wood up for me.

Chatting with Timberly Pt 2

See how easy it is to add new tools to the toolbox of an MCP server?

Complete type-safety. Separation of domain logic from the presentation layer. Easy testing.

Where could this go?

This example was an incredibly simple one – Timerbly only needed a simple measuring and cutting tool.

MCP toolboxes can have far more complex uses, including interacting with bespoke software or enforcing security like ensuring access is via a single path to systems like AWS.

These tools aren’t just limited to AI either: imagine a world where you want to build a back-office system, with a variety of tools exposed directly to consumers. These users don’t need fancy LLM’s in front of the tools, they’re educated and are able to leverage them effectively. A simple interface like Antropic’s Inspector, combined with defining the toolboxes they can use, will give you a powerful system for these users, for free.

In the future, Software Development is going to be focused more and more around building tools for Agents (whether AI or not). Either way, our users stay the same – they are real, able to be interacted with, and they have need for the tools we can deliver them.

About Conor Hodder

Technical Lead at Kablamo