Go で自作 MCP ツール
明けましておめでとうございます。2025 年末に着手した自作ツールとして、ローカルで実行できる MCP Server を Go 言語(以下 golang)で自作してみました。
MCP (Model Context Protocol) に準拠していれば、VS Code + Copilot や Claude Desktop などの MCP クライアントから使えると知ったからです。
MCP の理解を深めるには「シンプルな機能実装から始める」のがベストと考え、1 機能のみを提供する MCP サーバーを考えました。
MCP ツールの機能: UTF-8 の文字列を反転して返すだけ
主な仕様
-
MCP サーバー情報 :
-
サービス名 : "
text-mirror" -
サービス内容 : "
Text mirroring/reversing tool" -
トランスポート・タイプ :
stdio(プロセス間通信)- ローカル利用のため、HTTP や SSE は未実装
-
使用 Go ライブラリ :
github.com/modelcontextprotocol/go-sdk/mcp- 公式 の SDK Go モジュール @ GitHub (2026/01/06 現在)
- SDK Ver: v1.1.0
- 対応 MCP Spec: 2025-06-18
- 公式 の SDK Go モジュール @ GitHub (2026/01/06 現在)
-
サービス名 : "
-
ツール情報 :
-
ツール名 :
mirror-
ツール内容 : "
Reverses the given UTF-8 text"
-
ツール内容 : "
-
使用 Go ライブラリ :
github.com/rivo/uniseg-
ZWJ シーケンス(
🙂🙃🙂👩💻👨💻のような組み合わせ絵文字)も、それぞれ 1 ユニットとして扱える高速パッケージ
-
ZWJ シーケンス(
-
ツール名 :
-
MCP クライアント :
- MCP 準拠かつ
stdio対応のクライアント全般 - 動作検証環境 : Visual Studio Code v1.102 + Copilot Chat
- MCP 準拠かつ
-
開発環境 :
- macOS (Tahoe 26.2, Mac mini M4, 16GB)
- Go 1.25.5
-
開発時の心得 :
- 拡張性を視野に入れつつ、シンプルで Testable なコードにすること
ユニット・テストを含めた完全なコードは GitHub で公開しています。
TL; DR (今北産業)
- MCP は既存技術を組み合わせた「AI とツールをつなぐインターフェース」の仕様です
- まずは「動いた」から始め、「なぜそう書くのか」までを理解したい人向けの記事です
- Go で最小の MCP Server を実装しつつ、SDK・仕様・設計上の勘所を実コードで整理してみました
目次
基本コード
何はともあれ、まずは最もシンプルなコードから。
package main
import (
"context"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/rivo/uniseg"
)
func main() {
// mcp.Server のインスタンスをメタデータ付きで生成
server := mcp.NewServer(
&mcp.Implementation{
Name: "text-mirror",
Title: "Text mirroring tool",
Version: "1.0.0",
},
&mcp.ServerOptions{},
)
// "mirror" ツール (handleMirror) を登録
mcp.AddTool(server, &mcp.Tool{
Name: "mirror",
Description: "Reverses the given UTF-8 text",
}, handleMirror)
// server を 'stdio' トランスポート・モードで実行(入力待機開始)
err := server.Run(context.Background(), &mcp.StdioTransport{})
if err != nil {
log.Fatal("server error:", err)
}
}
// MirrorInput は "mirror" ツールの入力スキーマを定義します。
type MirrorInput struct {
Text string `json:"text" jsonschema:"UTF-8 text to be mirrored"`
}
// MirrorOutput は "mirror" ツールの出力スキーマを定義します。
type MirrorOutput struct {
Text string `json:"text" jsonschema:"Mirrored text"`
}
// handleMirror は input.Text を書記素クラスタ(単一の文字として認識できるグルーピング)
// を考慮しながら、文字列を反転します。
func handleMirror(
_ context.Context,
_ *mcp.CallToolRequest,
input MirrorInput,
) (*mcp.CallToolResult, MirrorOutput, error) {
reversed := uniseg.ReverseString(input.Text)
return nil, MirrorOutput{Text: reversed}, nil
}
-
mcp.Serverのインスタンスを生成する (MCP のサービス情報も定義) -
mcp.AddToolでツールを登録する(MCP Server のツール情報も登録) -
server.Runで MCP サーバーを起動し、stdinからの入力を待つ
上記は「MCP サーバーの最小骨格」で、これをビルドするだけで動きます。この記事の TS; DR セクションでは、これをベースに MCP の仕組みを解説しています。
しかし、今後さらに肉付け(ツールを増やすなどの拡張を)することを考えると、もう少し骨太(Testable で、メンテナンスしやすい状態)にする必要があります。
骨太コード
以下は、本番寄りに構成した骨太コードです。やっていることは同じなのですが、今後の拡張性を視野に、抽象化やテスト性を高める工夫をしています。
いささか長いので main() --> run() --> newServer() --> handleReverse() の流れを念頭に、斜め読みするだけでも何をしているのか把握できると思います。
「とりあえず基本コードで、実際に VS Code で動かしたい」方は、次項目までスキップしちゃってください。
// Package main is a tiny MCP (Model Context Protocol) service and tool. It
// simply mirrors (reverses) UTF‑8 text while preserving grapheme clusters.
//
// This repository implements a minimal MCP server and a single `mirror` tool
// to help me (the author) learn MCP basics and to build something that at
// minimum works with VSCode's Copilot (via `stdio` transport).
//
// * Latest code see: https://github.com/KEINOS/mcp-text-mirror/
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"runtime/debug"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/rivo/uniseg"
)
// Logger configuration.
const (
envNameDebug = "MCP_TEXT_MIRROR_DEBUG_LOG" // env var to enable debug logging. the value is the log path
fileLogDefault = false // set to true to enable debug logging to a file by default
logName = "text-mirror.log"
logDir = "." // default directory (current directory)
logFlag = os.O_APPEND | os.O_CREATE | os.O_WRONLY
logPerm = os.FileMode(0o644)
)
// Service metadata.
const (
serviceName = "text-mirror"
serviceVersion = "(devel)" // default version if not set in build info
serviceTitle = "Text mirroring/reversing tool"
revisionLen = 7 // short revision length for display
toolName = "mirror"
toolDescription = "Reverses the given UTF-8 text"
)
// CustomLogger is the minimal interface needed for fatal logging.
type CustomLogger interface {
Fatal(v ...any)
Print(v ...any)
}
// Predefined errors.
var errNilContext = errors.New("given context is nil")
// Dependency injection points to ease testing.
var (
// logger is used to log fatal errors. Tests can replace it.
logger CustomLogger = newLogger(IsDebugMode(), GetLogPath())
// defaultCtx is the context used to run the server which is
// context.Background() by default, but tests can override it.
defaultCtx = context.Background()
// debugReadBuildInfo is a copy of debug.ReadBuildInfo function.
// Tests can replace it.
debugReadBuildInfo = debug.ReadBuildInfo
// runServer is the function that runs the MCP server. Tests can replace it.
// It will error if given context is nil.
runServer = func(ctx context.Context, server *mcp.Server) error {
if ctx == nil {
return errNilContext
}
return server.Run(ctx, &mcp.StdioTransport{})
}
)
// ============================================================================
// main
// ============================================================================
func main() {
// defaultCtx may be overridden in tests.
exitOnError(run(defaultCtx))
}
// IsDebugMode returns whether debug mode is enabled. If true then logging to a
// file is enabled.
// By default, it return fileLogDefault constant value.
// If 'MCP_TEXT_MIRROR_DEBUG_LOG' env variable is set to a non-empty value, the
// value is used as the log path and debug mode is enabled.
func IsDebugMode() bool {
if os.Getenv(envNameDebug) != "" {
return true
}
return fileLogDefault
}
// GetLogPath returns the path to the log file. If 'MCP_TEXT_MIRROR_DEBUG_LOG'
// environment variable is set to a non-empty value, it returns the value as the
// log path.
func GetLogPath() string {
logPath := filepath.Join(logDir, logName)
envLogPath := os.Getenv(envNameDebug)
if envLogPath != "" {
logPath = envLogPath
}
return filepath.Clean(logPath)
}
// GetServiceVersion returns the service version string based on build info.
// If the build info is not available, it returns "unknown (devel)".
func GetServiceVersion() string {
version := serviceVersion // default version (devel)
revision := "unknown"
info, ok := debugReadBuildInfo()
if ok {
// version
if info.Main.Version != "" {
version = info.Main.Version
}
// revision
for _, s := range info.Settings {
if s.Key == "vcs.revision" && s.Value != "" {
revision = s.Value
break
}
}
// Found version. E.g.: v1.0.0 (abcdef0)
if version != serviceVersion {
return fmt.Sprintf("%s (%s)", version, revision[:min(len(revision), revisionLen)])
}
}
return fmt.Sprintf("%s %s", revision[:min(len(revision), revisionLen)], version)
}
// ----------------------------------------------------------------------------
// Helper functions
// ----------------------------------------------------------------------------
// run starts the MCP server and returns any error encountered.
func run(ctx context.Context) error {
server := newServer()
// Run server with a transport that uses standard IO. Mock this in tests.
err := runServer(ctx, server)
if err != nil {
return wrapError(err, "MCP server failed to run")
}
return nil
}
// newServer constructs and configures an MCP server with the mirror tool.
func newServer() *mcp.Server {
server := mcp.NewServer(
&mcp.Implementation{
Name: serviceName,
Title: serviceTitle,
Version: GetServiceVersion(),
},
&mcp.ServerOptions{}, //nolint:exhaustruct // use default options
)
// Initialize with zero values then set required fields (avoid exhaustruct
// linter error)
toolInfo := new(mcp.Tool)
toolInfo.Name = toolName
toolInfo.Description = toolDescription
// Add tool automatically and force tools to conform to the MCP spec.
mcp.AddTool(server, toolInfo, handleReverse)
return server
}
// newLogger creates a default logger.
//
// If toFile is true, it logs to the given path. Otherwise, it logs to standard
// error. If the log file cannot be opened, it silently falls back to logging to
// standard error.
//
// NOTE: The log file is intentionally kept open for the lifetime of the process.
func newLogger(toFile bool, path string) *log.Logger {
out := os.Stderr
if toFile {
path = filepath.Clean(path)
osFile, err := os.OpenFile(path, logFlag, logPerm)
if err == nil {
out = osFile
}
}
logger := log.New(out, "", log.LstdFlags|log.LUTC)
return logger
}
// debugLog logs the given values if debug mode is enabled.
func debugLog(v ...any) {
if IsDebugMode() {
logger.Print(v...)
}
}
// wrapError returns nil if err is nil.
// Otherwise it wraps the error with given message. If args are provided, it
// formats the message with them.
func wrapError(err error, msg string, args ...any) error {
if err == nil {
return nil
}
if len(args) > 0 {
msg = fmt.Sprintf(msg, args...)
}
return fmt.Errorf("%s: %w", msg, err)
}
// exitOnError logs the error and terminates the process (or panics in tests).
// If err is nil, it does nothing.
func exitOnError(err error) {
if err != nil {
logger.Fatal("Error:", err)
}
}
// ============================================================================
// 'reverse' tool handler
// ============================================================================
// MirrorInput is the input for the mirror tool.
type MirrorInput struct {
Text string `json:"text" jsonschema:"UTF-8 text to be mirrored"`
}
// MirrorOutput is the output from the mirror tool.
type MirrorOutput struct {
Text string `json:"text" jsonschema:"Mirrored text"`
}
// handleReverse returns (meta, output, error) per MCP tool handler contract.
// The returned output contains the reversed/mirrored input text.
//
// If the context is canceled, it returns an error.
// This tool doesn’t care who called it, so the CallToolRequest parameter is
// unused.
func handleReverse(
ctx context.Context,
_ *mcp.CallToolRequest,
input MirrorInput,
) (*mcp.CallToolResult, MirrorOutput, error) {
err := ctx.Err()
if err != nil {
return nil, MirrorOutput{}, wrapError(err, "request canceled")
}
// This is the core function of this tool: reverses the input text.
// If cancellation during the process (reversal) is needed, consider using
// `select` with `ctx.Done()` channel in a loop over grapheme clusters.
outputText := uniseg.ReverseString(input.Text)
// log if debug mode is enabled (fileLogDefault = true or env var is set)
debugLog("LOG: original text:", input.Text, "=> mirrored text:", outputText)
return nil, MirrorOutput{Text: outputText}, nil
}
主な使い方
本記事は VS Code + Copilot Chat での仕組みですが、Claude Desktop でも大きくは違わないと思います。
-
上記コードをビルドして実行バイナリを作成
go build -o text-mirror ./...
-
後述の VS Code MCP 設定(
mcp.json)にバイナリの絶対パスを追記し、VS Code を再起動 -
VS Code の拡張機能一覧で「MCP サーバー・インストール済み」に
text-mirrorが追加されていることを確認 -
チャットでツールを使った文字列反転を指示してみる(例)
Reverse this text using text-mirror: 🙂🙃🙂👩💻👨💻123Use the mirror tool to reverse "こんにちは"mirror-text を使って次のテキストをミラー反転してください:🙂🙃🙂👩💻👨💻123

チャットのキャプチャ(この Copilot は筆者向けにカスタムされています)
VS Code の MCP 設定
-
VS Code で
F1キーを押し、"MCP: Open User Configuration" を検索して MCP のユーザー構成を開く -
空の
mcp.jsonが開くので、以下を追記(既存エントリーがある場合は"servers"に"text-mirror"のエントリー部のみを追記)して保存mcp.json{ "servers": { "text-mirror": { "command": "/full/path/to/text-mirror", "env": { "MCP_TEXT_MIRROR_DEBUG_LOG": "/full/path/to/text-mirror.log" } } } }-
"/full/path/to/text-mirror"はビルドしたバイナリの絶対パスに置き換える -
"/full/path/to/text-mirror.log"はログ出力先の絶対パスに置き換える- ログが不要なら
"env"エントリーを削除
- ログが不要なら
- 変更は VS Code 再起動で適用
- 上記は最低限の設定。詳細は VS Code ドキュメントを参照
-
TS; DR (kwsk)
MCP の仕様とバージョニング
MCP は既存技術のマッシュアップであるため、仕様に準拠さえすれば独自実装できます。
問題は、その仕様です。MCP は新しいプロトコルなので、常に新しい仕様や仕様の修正が行われます。MCP の仕様バージョンも日付ベースになっていることからも伺えます。
MCP の仕様バージョニングは breaking-changes、つまり「後方互換性のない変更」が最後に行われた日付を YYYY-MM-DD 形式でバージョンとします。逆に言えば、変更が後方互換性を維持する限り、プロトコルが更新されてもバージョンは増加しません。
これにより、独自実装でも相互運用性を維持しながら段階的な改善が可能になるのですが、この変化に合わせてロジックを修正していくのも大変です。
そこで、MCP の公式 Go ライブラリ(Go-SDK)を使うメリットが出てきます。
- MCP の仕様を網羅している
- 公式なので追随のコラボレーション率が高い
- MCP に準拠したサーバーとクライアントが用意されている
- 実装が楽
- JSON-RPC の内容を、構造体のオブジェクトとしてパース済みのデータに変換してくれる
- データ処理が楽
-
Fail-fast 指向のプロジェクトに向いている
- 仕様の変化にテストの時点で気づける
もちろん、バージョン固定+独自実装で安定性を求めることもできますが、この記事では公式 SDK を全面的に活用します。
MCP の役者
これまで「MCP サーバー」や「MCP クライアント」と、シレッと言っていましたが、MCP の主要な構成要素は「ホスト」「クライアント」「サーバー」です。
-
MCP ホスト(MCP Host):
- 例)VS Code 本体(Copilot Chat / MCP 機構を含む)
- どの MCP クライアントやサーバーを使うかを決め、取得したコンテキストやツール結果を AI へ渡す「司令塔」的な役割
-
MCP クライアント(MCP Client):
- 例) VS Code 内部の MCP クライアント実装
- MCP サーバーと実際に通信し、MCP ホストが利用するためのコンテキストを受け取る実務担当的な役割。VS Code の拡張機能というより、VS Code 側の MCP ランタイムに近い
-
MCP サーバー(MCP Server):
- 例) 自作した MCP サーバープログラム
- ツールやコンテキストを提供する側。VS Code から見ると「外部の便利な道具箱」や「情報源」という役割
VS Code + Copilot で使う場合、MCP ホストの VS Code が MCP クライアントも内包しているため、体感的に VS Code が MCP クライアントに見えます。しかし、実際には VS Code は MCP ホストとして動作していることに留意します。
MCP の言語
MCP クライアントと MCP サーバー間のデータは JSON-RPC 2.0 形式でやり取りされます。
要するに JSON 形式です。サーバー操作(リモート先のメソッド実行)に特化した構造を持ったものが JSON-RPC です。
JSON を「日本語」に例えると、JSON-RPC は「である調」のようなイメージで、MCP の「業界用語」を使って初めてクライアントとサーバーが意思疎通できます。この共通ルールを「プロトコル」と呼びます。
MCP の P が Protocol なのも、共通ルールに沿っていれば通信できるためです。
また、この共通ルールには「伝達手段」も定められています。人間で言えば「口頭」「メール」「ビデオ会議」といったイメージです。
MCP の通信
上記の JSON データの伝達経路を MCP では transport と呼び、大きく 3 種類あります。
-
stdio
MCP クライアントがサーバーを子プロセスとして起動し、標準入出力でやり取りする方式。UNIX 的な通信で、ローカル実行向け。設定や設計が最小限で済む -
http
MCP サーバーを HTTP サーバーとして起動し、リクエスト/レスポンスで送受信する方式。RESTful で、既存の Web インフラと相性が良い。リモート配置やロードバランスが容易 -
sse(Server-Sent Events)
クライアントが HTTP 接続を張りっぱなしにし、サーバーからのイベントを一方向ストリームで受け取る方式。長時間処理や逐次結果の返却に向いており、「サーバーから喋り続ける」実況タイプの通信
いずれの経路であっても共通言語は JSON-RPC です。本記事は「シンプルな機能で、動くものからスタートする」ことが目的なので、transport には stdio(標準入出力)を利用します。
MCP の公式ライブラリ(SDK)を使う場合、MCP サーバーを実行する際に指定するだけです。
err := server.Run(context.Background(), &mcp.StdioTransport{})
MCP サーバーの役割
MCP サーバーは、以下の 3 つの機能を MCP クライアントに提供します。
-
Tools- クライアントの AI が必要に応じて呼び出すことができる「実行可能な機能」です。演算処理、外部 API の呼び出し、ファイル操作などを行います
-
Resources- コンテキスト提供を目的とした「読み取り専用の情報源」です。ドキュメントやデータ構造などを参照できます
-
Prompts- あらかじめ定義された指示テンプレートです。AI に対して、ツールやリソースの使い方を含めた「作業指針」を与えます
最後の「プロンプト」は、詳しい使い方を AI に伝えるものなので分かりやすいのですが、「ツール」と「リソース」の使い分けに迷うことがあります。
例えば「辞書引きツール」を考えた場合、ツールとリソースの両方に該当するためです。
ポイントは「AI が戻り値をどう使うか」になります。
- 「ツール」の戻り値は「正しい答え」として扱う
- 「リソース」の戻り値は「参考情報」として扱う
この記事では「ツール」の実装に特化して話を進めます。
MCP サーバーの仕組み(stdio 型)
MCP サーバーやツールの実装の前に、VS Code で MCP サーバーを stdio 経由で動かす際の「全体的な流れ」をまとめます。
-
準備
- MCP クライアント(VS Code)は起動時に MCP 設定(
mcp.json)を読み込む
- MCP クライアント(VS Code)は起動時に MCP 設定(
-
起動
- AI Chat でツール利用の指示があると、MCP 設定から適切なツールを推論し、MCP サーバーを起動(サブプロセスとして起動)する
-
待機
- MCP サーバーは標準入出力(
stdio)でツール(関数・メソッド)呼び出しを待ち受ける
- MCP サーバーは標準入出力(
-
リクエスト
- VS Code は命令データ(JSON-RPC)を MCP サーバーの標準入力(
stdin)に送る
- VS Code は命令データ(JSON-RPC)を MCP サーバーの標準入力(
-
処理とレスポンス
- MCP サーバーはリクエストを処理し、テキストを反転(ここでは
handleReverseを実行)して、その結果を標準出力(stdout)経由で返す
- MCP サーバーはリクエストを処理し、テキストを反転(ここでは
-
表示
- VS Code は返されたレスポンスを受け取り、ユーザーに表示する(3 に戻る)
MCP サーバーの作成
mcp.NewServer() のコンストラクタを使って mcp.Server を生成します。
必要なのは第 1 引数の Name(サーバー名)と Version(サーバーのバージョン)フィールドですが、Title(サーバーの機能説明)も明記することをオススメします。
// mcp.Server のインスタンスをメタデータ付きで生成
server := mcp.NewServer(
&mcp.Implementation{
Name: "text-mirror", // サーバーの表示名
Title: "Text mirroring tool", // UI や人間向けの表記
Version: "1.0.0", // サーバーのバージョン
},
&mcp.ServerOptions{},
)
-
mcp.Implementation.Name:- クライアント側のサーバーの一覧で表示される名前です。
Titleが空の場合は、この値が使われます。内部的にmcp_text-mirrorのようにmcp_の接頭辞付きで管理され、AI の解釈にも利用されます
- クライアント側のサーバーの一覧で表示される名前です。
-
mcp.Implementation.Title:- 基本的に UI や人間がサーバーの用途を判断するための情報です。この情報を AI が使うかはクライアント側に依存しますが、どちらにも伝わる端的な説明が望ましいです
-
mcp.Implementation.Version:- サーバーのバージョンです。デバッグや再現性の検証の際は重要になります
第 1 引数を nil で渡してしまうとパニックになるので注意します。
また、第 2 引数の mcp.ServerOptions はサーバーの挙動をコントロールするオプションです。
本記事では利用していませんが、接続しているクライアントに渡す「追加プロンプト」や、ツール数が多い場合のページ長などが指定できます。詳しい設定項目は公式ドキュメントを参照してください。
MCP サーバーのツール
MCP ツールは、ただの関数
MCP では、サーバーが提供できる実行機能、その機能名と説明文をセットで「ツール」と呼びます。
この実行機能は「関数」(もしくはメソッド)を指すのですが、サーバーのインスタンスに登録することで、ツールとして提供することができます。
Web API で言う、ホストが「サーバー」で、OpenAPI 的な情報付きのエンドポイントが「ツール」であると考えると、わかりやすいかもしれません。
ツールの登録
以下は自作関数 handleMirror を、ツールとして server に登録する例です。
// "mirror" ツール (handleMirror) を登録
mcp.AddTool(server, &mcp.Tool{
Name: "mirror",
Description: "Reverses the given UTF-8 text",
}, handleMirror)
この時、mcp.Tool.Description の内容が重要な役割を果たします。
この説明文をもとに「どのツールを利用するか」を AI が推論(判断)するためです。
「優しすぎる説明は AI Agent を甘やかす」という言い回しがあるのですが、曖昧な記載や「大は小を兼ねる」的な書き方だと何にでも使ってしまいます。
- ❌ : "Manipulates text" --> ⭕️ : "Reverses the given UTF-8 text"
かといって説明的過ぎると、トコロテン式に忘れる AI も多いため、端的かつ利用目的が明確な記載にします。
以下のように AI は解釈すると考えれば、記述の仕方も見えてくると思います。
"mcp_text-mirror_mirror" tool reverses the given UTF-8 text.
なお、日本語の説明文でも動く事は多いのですが、大半の AI は英語ベースであるため、一旦翻訳処理が入るぶん「日本語はトークン数を多く利用する可能性がある」ことを念頭に入れる必要があります。
ツールの型(関数型の構文)
肝心のツールの実装ですが、mcp.ToolHandlerFor 型に準拠している関数として実装する必要があります。
type mcp.ToolHandlerFor[In, Out any] func(
ctx context.Context, // サーバーからのコンテキスト
request *CallToolRequest, // クライアントからの付加情報
input In, // クライアントからの入力情報
) (
result *CallToolResult, // ツールからの付加情報
output Out, // ツールからの出力情報
err error, // エラーの有無
)
つまり 3 つの引数を受け取り、3 つの値を返す関数を実装するだけです。
以下が実装のサンプルです。引数を受け取って、処理して、型にハメて返しているだけです。
func handleMirror(
_ context.Context,
_ *mcp.CallToolRequest,
input MirrorInput,
) (*mcp.CallToolResult, MirrorOutput, error) {
// 反転実行
reversed := uniseg.ReverseString(input.Text)
return nil, MirrorOutput{Text: reversed}, nil
}
ここでは、引数の input と戻り値の output の型が重要になります。
mcp.ToolHandlerFor の構文を見てもらいたいのですが、In と Out に注目してください。ジェネリックな any 型ではあるものの、入力と出力は JSON にパースできるものである必要があります。
このプロジェクトでは In と Out 用に以下のように型を定義しています。フィールドのタグに json と jsonschema が定義されているのがわかります。
// MirrorInput は "mirror" ツールの入力スキーマを定義します。
type MirrorInput struct {
Text string `json:"text" jsonschema:"UTF-8 text to be mirrored"`
}
// MirrorOutput は "mirror" ツールの出力スキーマを定義します。
type MirrorOutput struct {
Text string `json:"text" jsonschema:"Mirrored text"`
}
実は、この jsonschema の説明文が意外に重要となります。
と言うのも、MCP クライアント側がリクエストとレスポンスの推論に必要な説明文として使われるからです。ここでも、短く、かつ明確に内容を伝えないと意図する使い方を AI がしてくれません。
- ❌ : "input text" --> ⭕️ : "UTF-8 text to be mirrored"
- ❌ : "output text" --> ⭕️ : "Mirrored text"
他の引数について
簡易実装サンプルで context.Context と *mcp.CallToolRequest の引数を(_ で)利用していないことにお気づきでしょう。
func handleMirror(
_ context.Context,
_ *mcp.CallToolRequest,
input MirrorInput,
) (*mcp.CallToolResult, MirrorOutput, error) {
// 反転実行
reversed := uniseg.ReverseString(input.Text)
return nil, MirrorOutput{Text: reversed}, nil
}
context.Context は、わかりやすいです。ツールが重い処理を行う場合、何かしらの理由でサーバー側から処理をキャンセルしたい場合などに利用します。
対して、*mcp.CallToolRequest はクライアント側から受け取ったデータで、処理を実行する際の参考情報が渡されます。具体的には以下のような内容です。
{
"Session": {},
"Params": {
"_meta": {
"progressToken": "19f3e785-2d33-48f4-92a5-0123456789ab",
"vscode.conversationId": "4ef961a3-71db-4f94-a4be-ab0123456789",
"vscode.requestId": "25b8c083-03c5-4c5b-b271-abcdef012345"
},
"name": "mirror",
"arguments": {
"text": "こんにちは"
}
},
"Extra": null
}
VS Code の会話 ID(conversationId)やリクエスト ID(requestId)から、Chat のスレッド情報が含まれていることがわかります。
今回のような、クライアントとサーバーが 1 対 1 の場合は、特に必要のない情報であるため、引数からデータを受け取っていません。
しかし、複数のクライアントがサーバーに接続する場合、特に http や sse では重要な情報になってきます。
このように、Go-SDK のおかげで、MCP サーバーの実装は難しくありません。しかし実際に開発すると、いくつか詰まるポイントがありました。
苦労したポイント
デバッグ用のログ出力
E2E テスト(MCP サーバーを実際に VS Code から動かしてみるテスト)時、print デバッグが使えませんでした。MCP クライアントから実際に受け取る情報をチョロっと見たかったのです。
と言うのも、VS Code のプロセス内で実行されるため、fmt.Print などの stdout/stderr 出力が MCP クライアントに吸収されます。ログ出力がレスポンスに混入すると JSON-RPC のパースエラーが発生したり、出力が握りつぶされるのです。
対策として、環境変数 MCP_TEXT_MIRROR_DEBUG_LOG でログファイルのパスを指定できるようにしました。
func debugLog(v ...any) {
if IsDebugMode() {
logger.Print(v...)
}
}
exhaustruct リンターとの戦い
golangci-lint の exhaustruct リンターは構造体の全フィールド明示的初期化を要求します。MCP SDK の構造体はフィールドが多く、素直に初期化すると冗長になります。
対策として、new() でゼロ値初期化してから必要なフィールドのみ設定しました。
toolInfo := new(mcp.Tool)
toolInfo.Name = toolName
toolInfo.Description = toolDescription
テスタビリティの確保
この記事の MCP サーバーは stdio(標準入出力)を使うため、通常のユニットテストが難しいです。サーバー起動やコンテキスト管理など外部依存が多くあります。
対策として、依存性注入(DI)を活用しました。グローバル変数として関数やコンテキストを保持し、テスト時には差し替えてエラーの発生をコントロールできるようにしています。
var (
logger CustomLogger = newLogger(IsDebugMode(), GetLogPath())
defaultCtx = context.Background()
runServer = func(ctx context.Context, server *mcp.Server) error {
// ...
}
)
グローバル変数を使ったモンキーパッチング な DI は、プロジェクトが大きくなるとテスト時に値(変数や関数の状態)が読みにくくなるため、多用は危険です。しかし、このプロジェクトは 1 ユーザー ↔︎ 1 サーバーの利用を前提としているため、テストのし易さを優先しました。
まとめ
GitHub に置いたコードが最終形ですが、最低限の機能を提供するも、拡張性があり、堅牢な実装のサンプルになったと思います。
今後の課題
- HTTP/SSE トランスポートの実装でリモート利用に対応する
- 複数ツールの追加でより実用的なサーバーを作ってみる
- Claude Desktop など他の MCP クライアントでの動作検証
所感
2023〜2024 年に流行った RAG(検索拡張生成)は、いまや昔。その後(2024 年後半から)急速に普及した MCP は、AI エージェントが外部ツールを活用するための標準プロトコルになりつつあります。
2025 年にソフトバンクの孫さんが「組織内で 10 億個の AI エージェントを稼働させる」という「千手観音プロジェクト」構想を発表したのをきっかけに、「AI チャットと戯れて満足している場合ではない」と危機意識を持ちました。
筆者は Go 使いなので Ollama でローカル AI を動かす方法は把握したものの、MCP はいまいち理解できていませんでした。AI エージェントも「GitHub Copilot みたいなもんでしょ?」程度の認識。
遅れを取ってはいけないとシンプルな実装から始めたところ、MCP の仕組みが理解でき「おお、これは使えるかも」と実感しました。
しかし同時に、「インターネットが歩んできた道」と同じ道を高速に歩んでいるとも思いました。
インターネット創世記、個人でもホームページを持つのが普通になると同時に情報が溢れ、その索引として Yahoo! が生まれました。後にマンパワーによる索引作成の限界に対して Google が生まれました。
MCP も同じように乱立するのは、作っていて感じました。
そして組織や社会のように専門化・細分化していく——「ジャンルごとにまとめた MCP 群を取りまとめる AI エージェント」があり、その上に「目的にあった AI エージェントをマネージングする AI エージェント」といったレイヤー構造が必要だろうな、という実感です。
おそらく RAG もオワコンではなく、いまの RSS/ATOM のような情報収集ツールの 1 つとして残っていく気もします。IT 業界が好きな「技術のマッシュアップ」です。
また、セキュリティに関しても同様です。
ブラウザの拡張機能や無料アプリが「個人情報を別サーバーに送信していた」「AI 搭載というのでマシンのファンがハーハーと頑張っていると思ったら、暗号資産のマイニングをしていただけ」といったリスクと同じリスクが MCP にもある(悪意ある機能を簡単に埋め込める)と、自分で作ってみて実感しました。
孫さんの構想は「ツールの大量生産」という印象を持っていたのですが、むしろ「自分たちが安心して利用できるものを、自分たちで作る・用意する」という意味が根底にあると理解できたのも、自作の MCP サーバーを作って良かったと思いました。
参考リンク
- MCP 公式ドキュメント
- MCP Go SDK @ GitHub
- VS Code MCP ドキュメント
- uniseg パッケージ @ GitHub
- 本記事のソースコード @ GitHub
