This is a memo on how to implement an MCP server using Golang.
- Introduction
- Prerequisite: Is there an official Go MCP SDK?
- Implementing an MCP Server in Go
- Conclusion
- References
Introduction
I wanted to try building an MCP server in Go.
Here’s a quick note on how to do it.
# Environment $ go version go version go1.23.4 darwin/arm64 # mcp-go version mcp-go v0.18.0
Note: This article was translated from my original post.
Prerequisite: Is there an official Go MCP SDK?
As of April 2025, no official SDK exists yet.
There’s an ongoing discussion about creating one:
In this post, we'll use the third-party Go SDK mentioned in the discussion—mcp-go by mark3labs—to build a minimal MCP server.
Implementing an MCP Server in Go
The Target MCP Server
We'll implement a minimal MCP server with one Tool, one Resource, and one Prompt.
In Python, you could do something like this:
from mcp.server.fastmcp import FastMCP mcp = FastMCP("HelloMCP") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers""" return a + b @mcp.resource("greeting://{name}") def get_greeting(name: str) -> str: """Get a personalized greeting""" return f"Hello, {name}!" @mcp.prompt() def translation_ja(txt: str) -> str: """Translating to Japanese""" return f"Please translate this sentence into Japanese:\n\n{txt}"
Implementing the MCP server in Go
Here’s the Go implementation using mcp-go:
package main import ( "context" "fmt" "strings" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) func main() { s := server.NewMCPServer( "Minimum Golang MCP Server", "1.0.0", ) // Tool: Add operation addTool := mcp.NewTool( "add", mcp.WithDescription("Add two numbers"), mcp.WithNumber("x", mcp.Required(), ), mcp.WithNumber("y", mcp.Required(), ), ) s.AddTool(addTool, addToolHandler) // Resource: Greeting template greetingResource := mcp.NewResourceTemplate( "greeting://{name}", "getGreeting", mcp.WithTemplateDescription("Get a personalized greeting"), mcp.WithTemplateMIMEType("text/plain"), ) s.AddResourceTemplate(greetingResource, greetingResourceHandler) // Prompt: Japanese translation template translationPrompt := mcp.NewPrompt( "translationJa", mcp.WithPromptDescription("Translating to Japanese"), mcp.WithArgument("txt", mcp.RequiredArgument()), ) s.AddPrompt(translationPrompt, translationPromptHandler) // Start server with stdio if err := server.ServeStdio(s); err != nil { fmt.Printf("Server error: %v\n", err) } } func addToolHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { x := request.Params.Arguments["x"].(float64) y := request.Params.Arguments["y"].(float64) return mcp.NewToolResultText(fmt.Sprintf("%.2f", x+y)), nil } func greetingResourceHandler(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { name, err := extractNameFromURI(request.Params.URI) if err != nil { return nil, err } return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: request.Params.URI, MIMEType: "text/plain", Text: fmt.Sprintf("Hello, %s!", name), }, }, nil } // Extracts the name from a URI formatted as "greeting://{name}" func extractNameFromURI(uri string) (string, error) { const prefix = "greeting://" if !strings.HasPrefix(uri, prefix) { return "", fmt.Errorf("invalid URI format: %s", uri) } name := strings.TrimPrefix(uri, prefix) if name == "" { return "", fmt.Errorf("name is empty in URI: %s", uri) } return name, nil } func translationPromptHandler(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { txt := request.Params.Arguments["txt"] prompt := fmt.Sprintf("Please translate this sentence into Japanese:\n\n%s", txt) return mcp.NewGetPromptResult( "Translating to Japanese", []mcp.PromptMessage{ mcp.NewPromptMessage( mcp.RoleAssistant, mcp.NewTextContent(prompt), ), }, ), nil }
Full source available here:
Compared to Python, the Go version is significantly more verbose.
Let’s break down each component.
Implementing the Tool
// Tool: Add operation addTool := mcp.NewTool( "add", mcp.WithDescription("Add two numbers"), mcp.WithNumber("x", mcp.Required(), ), mcp.WithNumber("y", mcp.Required(), ), ) s.AddTool(addTool, addToolHandler) // ~~~ func addToolHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { x := request.Params.Arguments["x"].(float64) y := request.Params.Arguments["y"].(float64) return mcp.NewToolResultText(fmt.Sprintf("%.2f", x+y)), nil }
NewTool
uses the Options Pattern, where you configure the tool dynamically with ToolOption
functions.
The source code of NewTool:
// NewTool creates a new Tool with the given name and options. // The tool will have an object-type input schema with configurable properties. // Options are applied in order, allowing for flexible tool configuration. func NewTool(name string, opts ...ToolOption) Tool { tool := Tool{ Name: name, InputSchema: ToolInputSchema{ Type: "object", Properties: make(map[string]interface{}), Required: nil, // Will be omitted from JSON if empty }, } for _, opt := range opts { opt(&tool) } return tool }
The following functions (or functions that return functions) of type ToolOption
can be passed as optional arguments to NewTool
:
- WithDescription: Adds a description to the Tool
- WithBoolean: Defines a boolean argument for the Tool
- WithNumber: Defines a number argument for the Tool
- WithString: Defines a string argument for the Tool
- WithObject: Defines an object argument for the Tool
- WithArray: Defines an array argument for the Tool
The actual logic that the Tool executes is registered with AddTool
, like the addToolHandler
example in the code above.
This logic is implemented as a function of type ToolHandlerFunc.
ToolHandlerFunc
is defined like this:
// ToolHandlerFunc handles tool calls with given arguments. type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
You can retrieve parameters passed at the time of the Tool call using:
request.Params.Arguments["<param name>"]
Implementing the Resource
// Resource: Greeting template greetingResource := mcp.NewResourceTemplate( "greeting://{name}", "getGreeting", mcp.WithTemplateDescription("Get a personalized greeting"), mcp.WithTemplateMIMEType("text/plain"), ) s.AddResourceTemplate(greetingResource, greetingResourceHandler) // ~~~ func greetingResourceHandler(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { name, err := extractNameFromURI(request.Params.URI) if err != nil { return nil, err } return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: request.Params.URI, MIMEType: "text/plain", Text: fmt.Sprintf("Hello, %s!", name), }, }, nil } // Extracts the name from a URI formatted as "greeting://{name}" func extractNameFromURI(uri string) (string, error) { const prefix = "greeting://" if !strings.HasPrefix(uri, prefix) { return "", fmt.Errorf("invalid URI format: %s", uri) } name := strings.TrimPrefix(uri, prefix) if name == "" { return "", fmt.Errorf("name is empty in URI: %s", uri) } return name, nil }
- Define attributes like description and parameters for a Resource in NewResourceTemplate
- Register the Resource in AddResourceTemplate, along with the logic to return the Resource
The structure is very similar to how Tools are implemented, so the details are omitted here. Just like with Tools, Resources are dynamically configured using the Options Pattern.
NOTE: The code examples above implement a Resource Template (dynamic URI).
For regular Resources (not Resource Templates), a dedicated API is provided, so please use that instead.
Implementing the Prompt
// Prompt: Japanese translation template translationPrompt := mcp.NewPrompt( "translationJa", mcp.WithPromptDescription("Translating to Japanese"), mcp.WithArgument("txt", mcp.RequiredArgument()), ) s.AddPrompt(translationPrompt, translationPromptHandler) // ~~~ func translationPromptHandler(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { txt := request.Params.Arguments["txt"] prompt := fmt.Sprintf("Please translate this sentence into Japanese:\n\n%s", txt) return mcp.NewGetPromptResult( "Translating to Japanese", []mcp.PromptMessage{ mcp.NewPromptMessage( mcp.RoleAssistant, mcp.NewTextContent(prompt), ), }, ), nil }
- Use NewPrompt to define prompt attributes like description and arguments
- Use AddPrompt to register the prompt along with its return logic
This structure is the same as how Tools and Resources are implemented. Use the provided types to implement each prompt accordingly.
Testing the Server with MCP Inspector
Finally, we’ll test the Go MCP server using MCP Inspector.
If you're new to this tool, see my other post:
To run the server with MCP Inspector:
npx @modelcontextprotocol/inspector go run main.go
Once launched, open http://127.0.0.1:6274 in your browser and test the Tool, Resource, and Prompt.
Everything worked as expected.
Conclusion
We implemented a minimal MCP server using Go.
While more verbose than the Python SDK, it’s a good option if you're integrating with Go tools.
Hope this helps someone!
[Related Articles]
References
- GitHub - mark3labs/mcp-go: A Go implementation of the Model Context Protocol (MCP), enabling seamless integration between LLM applications and external data sources and tools.
- Proposal: official support for `modelcontextprotocol/go-sdk` · modelcontextprotocol · Discussion #224 · GitHub
- Option mode · Issue #40 · mark3labs/mcp-go · GitHub
- GitHub - bioerrorlog/hello-gomcp: The minimal Golang MCP server implementation with mcp-go.
- GitHub - bioerrorlog/hellomcp: The minimal Python MCP server implementation with MCP Python SDK.