Building a MCP Server in Go

This is a memo on how to implement an MCP server using Golang.

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:

github.com

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
}
  • Define tool attributes with NewTool
  • Register the tool and the logic using AddTool

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:


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
}

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:

en.bioerrorlog.work

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.

Calling Resource: getGreeting

Calling Prompt: translationJa

Calling Tool: add

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]

en.bioerrorlog.work

en.bioerrorlog.work

References