BioErrorLog Tech Blog

試行錯誤の記録

MCPサーバーをGoで実装する

Go言語でMCPサーバーを実装する方法の備忘録です。

はじめに

MCPサーバーをGoで実装したくなりました。

やり方のメモを残します。

# 作業環境
$ go version
go version go1.23.4 darwin/arm64

# mcp-goバージョン
mcp-go v0.18.0

The English translation of this post is here.

前提: Go言語の公式MCP SDKはあるのか?

本記事執筆時点(2025/04)ではまだありません。

公式のGo言語MCP SDKを作ろうという議論は起きていますが、まだ議論中の段階です。

今回は上記discussionでも言及されている、サードパーティのGo SDKの中では最もメジャーなmark3labsのmcp-goを使って、ミニマムなMCPサーバーを実装してみます。

MCPサーバーをGoで実装する

実装するMCPサーバー

最小限のTool/Resource/Promptを実装するミニマムなMCPサーバーを実装します。

Python SDKであれば下記のように実装できるものを、Goで実装していきます。

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}"

GoでのMCPサーバー実装

上記のようなMCPサーバーを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
}

ソースコード全体はこちら:

github.com

Pythonで実装するよりもかなりコード量が多くなってしまいましたね。

それではTool/Resource/Promptの実装をそれぞれ見ていきます。

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でToolのdescriptionや引数などの属性を定義
  • AddToolで実際の処理ロジックと併せてToolを登録

というのが大まかな構成です。

NewToolはいわゆるOptions Patternになっており、ToolOption型の関数を必要に応じて動的に渡すことでToolの定義を設定していきます。

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
}

NewToolにオプション引数として渡すToolOption型の関数(を返す関数)には、下記が用意されています:


実際にToolが実行する処理は、上記コード例のaddToolHandlerのようにAddToolで登録します。

この処理はToolHandlerFunc型の関数として実装します。 ToolHandlerFuncはこのような形をした型です:

// ToolHandlerFunc handles tool calls with given arguments.
type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)

Tool呼び出し時に渡されたパラメータは、request.Params.Arguments["<param name>"]のようにして取得できます。

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
}

構成はToolの実装とよく似ていますので詳細は省きますが、ResourceでもTool同様にOptions Patternに沿って動的にResourceの定義を設定していきます。

※ 上記コード例はResource Template(dynamic URI)を実装しています。 (Resource Templateではない)Resourceには、それ用のAPIが用意されているのでそちらを使用してください。

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
}
  • NewPromptでPromptのdescriptionや引数などの属性を定義
  • AddPromptでPrompt返却ロジックと併せてPromptを登録

これもToolやResourceの実装と構成は同じです。 それぞれ用意された型を利用してPromptを実装していきます。

MCP InspectorでMCPサーバーをテスト実行する

最後に、Goで実装した上記MCPサーバーをMCP Inspectorでテスト実行していきます。

MCP Inspectorの使い方については別途記事にまとめているので、このツールが初見の方はこちらもご参照ください:

www.bioerrorlog.work


Goで実装したMCPサーバーをMCP Inspectorで起動:

npx @modelcontextprotocol/inspector go run main.go 

無事MCP Inspectorが起動したら、ブラウザからhttp://127.0.0.1:6274を開いてMCPサーバーで実装したTool/Resource/Promptの各機能を叩いてみます。

Resource: getGreetingを呼び出した様子

Prompt: translationJaを呼び出した様子

Tool: add を呼び出した様子

無事、Goで実装したMCPサーバーが想定通りのレスポンスを返していることが確認できました。

おわりに

今回はMCPサーバーをGoで実装してみました。

Python SDKに比べるとコード量も多く煩雑ですが、Go製のツールと組み合わせてMCPサーバーを実装するのに役に立ちそうです。

以上、どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

参考