Go SDK for Anthropic Model Context Protocol

Several Software Development Kits (SDK) already exist for developing Model Context Protocol (MCP) servers in Typescript and Python.

However, the Go programming language presents a compelling alternative for creating robust and efficient MCP server implementations.

Go offers several key advantages that make it particularly well-suited for developing Model Context Protocol servers:

  • Performance: Go is known for its high performance and efficient concurrency model, which is crucial for server-side applications.
  • Simplicity: The language’s clean syntax and straightforward design make it easy to develop and maintain complex server infrastructures.
  • Strong Typing: Go’s strong type system helps prevent runtime errors and ensures more reliable code.
  • Compilation Speed: Quick compilation times enable faster development and iteration cycles.

In this document, I will provide a quick description of this SDK and illustrate how Go is particularly well-suited for this kind of development.

A Notion document retrieval server example

The code of the SDK is available at: https://github.com/llmcontext/gomcp. This SDK is still at version 0.1.0 as the public APIs may still change.

You can find an example of a Go Application with the Go SDK at: https://github.com/llmcontext/mcpnotion

This application allows Claude to retrieve a Notion document identified by its pageId .

A use case is to ask Claude to proofread a Notion page.

Note: you can find the caude for this example in github at: https://github.com/llmcontext/mcpnotion

Claude MCP server configuration

As described in my previous post, you bind your MCP server to Claude through the Application Support/Claude/claude_desktop_config.json configuration file.

You can easily access this file from the Settings menu in Claude:

The content of this file is something like this:

{
"mcpServers": {
"mcpnotion": {
"command": "/usr/local/bin/mcpnotion",
"args": ["--configFile", "/etc/gomcp/mcpnotion.json"]
}
}
}

IMPORTANT: you need to restart Claude each time you change that file!

You can confirm in Claude that your MCP server is properly configured if you see it there:

If you click on that button you should see:

The first time Clause will call your function, a popup will ask you for permission:

You will also be able to see from Claude, the parameters and result from this call:

How to use the gomcp SDK

Install the SDK in your go project with:

go get github.com/llmcontext/gomcp

The integration is done in 3 steps in your main function:

package main
import (
"flag"
"fmt"
"os"
"github.com/llmcontext/gomcp"
"github.com/llmcontext/mcpnotion/tools"
)
func main() {
configFile := flag.String("configFile", "", "config file path (required)")
flag.Parse()
if *configFile == "" {
fmt.Println("Config file is required")
flag.PrintDefaults()
os.Exit(1)
}
// step 1
mcp, err := gomcp.NewModelContextProtocolServer(*configFile)
if err != nil {
fmt.Println("Error creating MCP server:", err)
os.Exit(1)
}
toolRegistry := mcp.GetToolRegistry()
// step 2
err = tools.RegisterTools(toolRegistry)
if err != nil {
fmt.Println("Error registering tools:", err)
os.Exit(1)
}
// step 3
transport := mcp.StdioTransport()
mcp.Start("mcpnotion", "0.1.0", transport)
}

Step 1

The gomcp.NewModelContextProtocolServer() call initialize and configure the SDK. You pass as a parameter the configuration file that is like this:

{
"logging": {
"file": "/var/log/gomcp/mcpnotion.log",
"level": "debug",
"withStderr": false
},
"tools": [
{
"name": "notion",
"description": "Get a notion document",
"configuration": {
"notionToken": "ntn_<redacted>"
}
}
]
}

This file allows you to configure the logging of the server. As a reminder your server can never write on stdout as this is is the communication channel between Claude and your server.

The tools section of the configuration file is where you set the configuration parameter of each tools that your server is implementing.

Step 2

This is the step where you wire the tools you want to expose into the MCP server. See the next section for more information about that step.

Step 3

This is where you actually start the server. The server needs a transport mechanism used to communicate with Claude and right now only the stdin/stdout transport is supported.

Tools configuration

Let’s consider our Notion example again.

You have a function to read a notion document:

func getPageContent(ctx context.Context, client *notionapi.Client, pageId string) ([]string, error)

( code available here )

This function takes 3 parameters:

  • the usual Go context. This context gives access to a logger integrated to the MCP server
  • the Notion client, coming from a Notion SDK
  • the pageId that, in the context of the MCP server, will be coming from the Claude application

Tool Context

The notion client needs to have access to a notion integration token to be allowed to read the content of the page: this parameter is set in the configuration file (see above).

To get that value you define a type:

type NotionGetDocumentConfiguration struct {
// the API key for the Notion client.
NotionToken string `json:"notionToken" jsonschema_description:"the notion token for the Notion client."`
}

This is where we see how Go is well suited for this kind of tool.

The type definition contains json tags and jsonschema_description tags.

Those tags allow us to generate a Json schema and validate at run time that the configuration data are matching what we expect: if the token is not provided or if there is a typo in the name of the property, or if there is an extra property in the configuration file, the MCP server won’t start.

The way you expose that configuration to the MCP server is by providing an initialization function:

func NotionToolInit(ctx context.Context, config *NotionGetDocumentConfiguration) (*NotionGetDocumentContext, error) {
client := notionapi.NewClient(notionapi.Token(config.NotionToken))
// we need to initialize the Notion client
return &NotionGetDocumentContext{NotionClient: client}, nil
}

This function will be called by the MCP server at startup time, with the actual data from the configuration file, and that function should return a tool specific context that will be used each time a tool function is called, indirectly, by the Claude application.

This tool specific context for this Notion example is used to store the Notion client:

type NotionGetDocumentContext struct {
// The Notion client.
NotionClient *notionapi.Client
}

Finally, you need to write the actual function that will do the job of retrieving a Notion page for Claude:

// retrieves the content of a Notion page identified by the PageId.
func NotionGetPage(ctx context.Context,
toolCtx *NotionGetDocumentContext,
input *NotionGetDocumentInput,
output types.ToolCallResult) error {
logger := gomcp.GetLogger(ctx)
logger.Info("NotionGetPage", types.LogArg{
"pageId": input.PageId,
})
content, err := getPageContent(ctx, toolCtx.NotionClient, input.PageId)
if err != nil {
return err
}
output.AddTextContent(strings.Join(content, "\n"))
return nil
}

This function takes 4 parameters:

  • the usual Go context. You see in the code how we can retrieve a logger from the context.
  • the tool specific context: coming from the previous step
  • the input parameters coming from Claude
  • the output parameter is the mechanism that allows you to return data in the format suitable for the MCP protocol (see interface definition here)

One important part is to define how we retrieve parameters from Claude.

You do that by defining a type describing those parameters:

type NotionGetDocumentInput struct {
PageId string `json:"pageId" jsonschema_description:"the ID of the Notion page to retrieve."`
}

Using those tags, the MCP server is able to generate a schema that will be provided to Claude during the listing tools phase of the MCP protocol:

"tools": [
{
"name": "notion_get_page",
"description": "Get the markdown content of a notion page",
"inputSchema": {
"properties": {
"pageId": {
"type": "string",
"description": "the ID of the Notion page to retrieve."
}
},
"additionalProperties": false,
"type": "object",
"required": [
"pageId"
]
}
}
]

We now have 2 functions:

  • the tool initialization function. Here NotionToolInit that instantiates the Notion client and makes it available in the tool context.
  • the tool function,NotionGetPage that does the actual job of retrieving the Notion page and returns it to Claude in the expected format.

The Go SDK allows you to have:

  • multiple *tool providers *: your server could expose multiple set of functions, like functions to interact with Notion, and function that interacts with a local SQL database etc…
  • each tool provider will have one or multiple functions

You declare those tool providers and their functions in the final step of the SDK integration:

func RegisterTools(toolRegistry types.ToolRegistry) error {
toolProvider, err := toolRegistry.DeclareToolProvider("notion", NotionToolInit)
if err != nil {
return err
}
err = toolProvider.AddTool("notion_get_page",
"Get the markdown content of a notion page", NotionGetPage)
if err != nil {
return err
}
return nil
}

That is the function called in step 2 described above.

That steps is the glue between the MCP server and your application code:

  • the DeclareToolProvider gives a name to your provider: that name is the property that will be used to retrieve the configuration parameters from your configuration file. You also provide the initialization function to the SDK.
  • the AddTool is the way you add functions to the tool provider: you give the name that will be given to claude and a description of that function. Those are the parameters that will show up in the listing tools phase show above. You also provide the actual processing function.

Is Go a good language to implement a Model Context Protocol SDK?

The RegisterTools function above show where Go is particularly well suited for the MCP protocol.

First, using interfaces you can expose a very small surface area to use the SDK (see code here):

type ToolProvider interface {
AddTool(toolName string, description string, toolHandler interface{}) error
}
type ToolRegistry interface {
DeclareToolProvider(toolName string, toolInitFunction interface{}) (ToolProvider, error)
}
type ModelContextProtocol interface {
StdioTransport() Transport
GetToolRegistry() ToolRegistry
Start(serverName string, serverVersion string, transport Transport) error
}

That’s all there is to know API wise.

Another powerful feature of Golang are its reflection capabilities.

For instance, you add a tool to the SDK with:

AddTool(toolName string, description string, toolHandler interface{}) error

The function is types as interface{} (could be also the alias type any).

That means you could actually give anything to that call, but the reflection capabilities of Go allow the Go SDK, at run time to check that (code here):

  • the parameter is actually a function
  • this function takes 4 parameters
  • the first parameter is of the type *context.Context
  • the second parameter is a pointer to a struct and that this struct is of the type returned by the tool initialization function (the tool context)
  • the third parameter is a pointer to a struct (the arguments of the function as used by Claude)
  • the fourth parameter is of type *types.ToolCallResult , our interface to produce an output following the MCP protocol

Those checks provide very good developer experience as they ensure that if the code pass the initialization phase, you won’t have any runtime issue due to misconfiguration.

Another interesting feature of Go are the tag annotations.

For instance, during the analysis of the third parameter above, the introspection API allows the SDK to retrieve the schema of the expected parameters for that function. This schema will be used in the MSP protocol to provide the information when the tools are listed, but the SDK will also check at run time that, when called by Claude, the parameters match this schema.