2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Golang】最もシンプルな MCP Server を Go で実装してみる

2
Last updated at Posted at 2026-01-02

Go で自作 MCP ツール

明けましておめでとうございます。2025 年末に着手した自作ツールとして、ローカルで実行できる STDIO 型のシンプルな MCP Server を Go 言語(以下 golang)で自作してみました。

MCP ツール "mirror" の機能
UTF-8 の文字列を反転して返すだけ

と言うのも、「MCP (Model Context Protocol) に準拠していれば、VS Code + Copilot や Claude Desktop などの MCP クライアントから使える」と、知ってはいたものの、いささか理解できていなかったからです。

きっかけは、Blender の MCP です。AI Chat 経由で Blender の操作や 3D モデルの生成ができると話題になっており、今後どのようなアプリでも操作系 API が公開されれば(AutoPilot 機能などを使い)試行錯誤を含めて自動化できる波が来そうだと感じたからです。

そこで、MCP の理解を深めるには「シンプルな機能実装から始める」のがベストと考え、1 機能のみを提供する MCP サーバーを考えました

一旦、動くものを素直に組んで、機能を変えずにテスト性やメンテナンス性を高めるリファクタリングをした中で得た知見を共有したいと思います。

主な仕様
  • MCP サーバー情報 :
    • サービス名 : "text-mirror"
    • サービス内容 : "Text mirroring/reversing tool"
    • トランスポート・タイプ : stdio (プロセス間通信)
      • ローカル利用のため、HTTPS(旧 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
      • github.com/rivo/uniseg
        • ZWJ シーケンス🙂🙃🙂👩‍💻👨‍💻 のような組み合わせ絵文字)も、それぞれ 1 ユニットとして扱える高速パッケージ
  • ツール情報 :
    • ツール名 : mirror
      • ツール内容 : "Reverses the given UTF-8 text"
  • MCP クライアント :
    • MCP 準拠かつ stdio 対応のクライアント全般
    • 動作検証環境 : Visual Studio Code v1.102 + Copilot Chat
  • 開発環境 :
    • macOS (Tahoe 26.2, Mac mini M4, 16GB)
    • Go 1.25.5
  • 開発時の心得 :
    • 拡張性を視野に入れつつ、シンプルで Testable なコードにすること

TL; DR (今北産業)

  1. MCP は既存技術を組み合わせた「AI とツールをつなぐインターフェースの仕様」です
  2. Golang で最小の MCP Server を実装しつつ、SDK・仕様・設計上の勘所を実コードで整理してみました
  3. VS Code + Copilot を使っているものの、MCP が気になる読者向けです。まずは「動いた」から始め、「なぜそう書くのか」までを理解できるようにまとめてみました

本記事は VS Code + Copilot Chat での MCP を取り上げていますが、Claude Desktop でも大きくは違わないと思います。

目次


基本コード

何はともあれ、まずは最もシンプルなコードから。

依存ライブラリのインストール
go mod init example/mcp-text-mirror
go get "github.com/modelcontextprotocol/go-sdk/mcp"
go get "github.com/rivo/uniseg"
main.go(簡易版)
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
}
  1. mcp.Server のインスタンスを生成する (MCP のサービス情報も定義)
  2. mcp.AddTool でツールを登録する(MCP Server のツール情報も登録)
  3. server.Run で MCP サーバーを起動し、stdin からの入力を待つ

上記は「MCP サーバーの最小骨格」で、これをビルドするだけで動きます。この記事の TS; DR セクションでは、これをベースに MCP の仕組みを解説しています。

骨太コード

以下は、今後の拡張を視野に「抽象化」や「テスト性」を高めた骨太コードです。

やっていることは基本コードと同じなので、「とりま、基本コードで実際に VS Code で動かしてみたい」と言う方は、次項目の「使い方」までスキップしちゃってください。

いささか長いので main() --> run() --> newServer() --> handleReverse() の流れを念頭に、斜め読みするだけでも何をしているのか把握できると思います。

骨太コード(堅牢版コード)

堅牢性について

今後さらに筋肉を付ける(ツールを増やすなどの拡張をする)ことを考えると、もう少し Testable でメンテナンスしやすい状態にしておく、つまり基本設計を骨太にしておく必要があると感じました。

とはいえ、ローカル利用向けの実装です。複数ユーザー向けに求められる「大量アクセスに耐えることや常時稼働を前提とした強さ」といった「壊れない設計」の意味ではなく、この骨太コードでは「テストしやすく、変更や機能追加を安心して(壊さずに)行える設計」を「堅牢性」と考えています

より堅牢にする案があれば、遠慮なく PR ください

main.go(堅牢版)
// 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.
  // It will error if given context is nil. Tests can replace it.
  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 で使ってみる方法を先に提示します。

個人的に、まず動くことを確認し、そこから理屈を逆算していく方が、理解が深まるように感じるからです。

  1. 上記コードをビルドして実行バイナリを作成

    • go build -o text-mirror ./main.go

  2. 後述の VS Code MCP 設定mcp.json)にバイナリの絶対パスを追記し、VS Code を再起動

  3. VS Code の拡張機能一覧で「MCP サーバー・インストール済み」に text-mirror が追加されていることを確認

    スクリーンショット 2026-01-08 13.45.36.png
    軽量モデルの場合、MCP に対応できないものもあるので注意

  4. チャットのモデルを "Agent" に選択

    スクリーンショット 2026-01-07 14.07.53.png
    軽量モデルの場合、MCP に対応できないものもあるので注意

  5. チャットでツールを使った文字列反転を指示してみる(プロンプト例)

    • Reverse this text using text-mirror: 🙂🙃🙂👩‍💻👨‍💻123
    • Use the mirror tool to reverse "こんにちは"
    • mirror-text を使って次のテキストをミラー反転してください:🙂🙃🙂👩‍💻👨‍💻123
    スクリーンショット 2026-01-01 20.55.00.png
    チャットのキャプチャ(この Copilot は筆者向けにカスタムされています)

VS Code の MCP 設定

  1. VS Code で F1 キーを押し、"MCP: Open User Configuration" を検索して MCP のユーザー構成を開く

  2. 空の 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"
          }
        }
      }
    }
    

TS; DR (kwsk)

MCP (Model Context Protocol) は「AI の USB みたいなもの」と言った説明を良く聞きます。AI エージェントをパソコンと仮定すると、MCP は便利な USB 周辺機器(デバイス)に相当するイメージだからです。

言い得て妙だとは思うのですが、体感的には Apple/Intel の Thunderbolt の域かなぁ、とも思います。確かに Anthropic が制定した MCP は、デファクト・スタンダード(事実上の標準)ではあるものの、USB のように淘汰され、誰でも使い方がピンと来るものではないからです。

個人的に、その原因は「コンテキスト」の用語の理解にあると感じます。

コンテキストとは

"Context" は、日本語で「文脈」「文の前後関係」「事情」「背景」「状況」などといった意味があります。

しかし、「モデルの文脈プロトコル」と言われてもピンと来ません。

Gopher(Go 使い)なら ctx := context.Background() と普段使いするものの、「文脈」と言われると「え?でも cancel() とか、外部操作系的なイメージなんだけど」とモヤっとします。

コンテキストは、con-text で構成され、con- の接頭辞は「一緒に」とか「」などの意味を持ち、text は 「組み上げる」「編む」「作る」「構成する」の *teks- から来ています。

つまり "context" は、何かを「実行」「判断」「作成」する際に添えられた裏付け情報のような意味合いになります。「主」ではないが「副」としての補完情報です。

ここまで理解して、やっと「モデルの文脈プロトコル」は「モデルの文脈解析に必要な裏付け情報」ということであり、MCP は「学習モデル(主に AI エージェント)が、この情報をやり取りする際のフォーマット(やり取りの仕方やデータの形式)を定めた規格」という意味にたどり着きます。

(╯°□°)╯\ワカラーン !/︵ ┻━┻

MCP の仕様とバージョニング

MCP は既存技術のマッシュアップであるため、仕様に準拠さえすれば独自実装できます

問題は、その仕様です。MCP は新しいプロトコルなので、常に新しい仕様や仕様の修正が行われます。MCP の仕様バージョンも日付ベースになっていることからも伺えます。

MCP の仕様バージョニングは breaking-changes、つまり「後方互換性のない変更」が最後に行われた日付を YYYY-MM-DD 形式でバージョンとします。

逆に言えば、変更が後方互換性を維持する限り、プロトコルが更新されてもバージョンは増加しません。

これにより、独自実装でも相互運用性を維持しながら段階的な改善が可能になるのですが、この変化に合わせてロジックを修正していくのも大変です。

2026/02/16 追記:実際に、この記事の実装をした直後に、後述する transport のうち HTTPSSE の が廃止されました。

本記事の stdio には影響しないものの、今後も最新仕様は変わる可能性があります。そのため、この記事もすぐに古くなるでしょう。

公式ライブラリのメリット

最初は「勉強のため」と、API のクライアント実装も独自実装しようと考えていました。しかし、途中から既存の HTTP リクエスト方法に JSON(JSON-RPC)のパースをするだけと気づきました。

仕様変更の追随や、実装のエッジ・ケースまでテストすることを考えると、この部分の実装に注力するのは「車輪の再発明」以前に「MCP の学習目的から外れてしまう」と感じました。

そこで、MCP の公式 Go ライブラリ(Go-SDK)を使うメリットが出てきます。

  1. MCP の仕様を網羅している
    • 公式なので追随のコラボレーション率が高い
  2. MCP に準拠したサーバーとクライアントが用意されている
    • 実装が楽
  3. JSON-RPC の内容を、構造体のオブジェクトとしてパース済みのデータに変換してくれる
    • データ処理が楽
  4. Fail-fast 指向のプロジェクトに向いている
    • 仕様の変化にテストの時点で気づける

もちろん、バージョン固定+独自実装で安定性を求めることもできますが、「安定 = 安全」ではないのでトレードオフになります。この記事では、素直に公式 SDK(Go-SDK)を活用します。

問題は Go-SDK のバージョンが、「どの MCP 仕様に準拠しているか」が、README.md を参照しないと、わからないことです。

この記事では執筆時点で Go-SDK 1.1.0 であるため、MCP 2025-06-18 (互換: 2025-06-18, 2025-03-26, 2024-11-05) に準拠した MCP Server ということになります。

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 クライアントを同等に扱いますが、実際には VS Code は MCP ホストとして動作していることに留意ください。

MCP の言語

MCP クライアントと MCP サーバー間のデータは JSON-RPC 2.0 形式でやり取りされます。

要するに JSON 形式です。サーバー操作(リモート先のメソッド実行)に特化した構造を持ったものが JSON-RPC です。

JSON を「日本語」に例えると、JSON-RPC は「である調」のようなイメージで、MCP の「業界用語」を使って初めてクライアントとサーバーが意思疎通できます。

このような、2 者間の共通ルールをプログラム業界では「プロトコル」と呼びます。MCP の P が Protocol なのも、共通ルールに沿っていれば通信できるためです。

また、この共通ルールには「伝達手段」も定められています。人間で言えば「口頭」「メール」「ビデオ会議」といったイメージです。

MCP の通信

上記の JSON データの伝達経路を MCP では transport と呼び、大きく 2 種類あります(2025/11/25 までは 3 種類でした)。

  1. stdio
    MCP クライアントがサーバーを子プロセスとして起動し、標準入出力でやり取りする方式。UNIX 的な通信で、ローカル実行向け。設定や設計が最小限で済む

  2. Streamable HTTP
    HTTP POST/GET を用いて 1 つのエンドポイント(MCP endpoint)を提供する client-server 通信方式。
    これは以前のバージョン(2024-11-05)の HTTP と SSE のトランスポートを置き換えるものであるが、Origin の検証が必須になっただけでなく、https による接続を推奨するなど、セキュリティ重視の項目が追加(明文化)されている。とは言え、内容的には公開 Web サービスなら、いずれも常識的な内容である。明文化しないと責任転嫁してくるユーザーが増えた & ガバガバな公開 MCP が乱立したため、統廃合された

以前のバージョン(2024-11-05)で使えていた Transport は以下の 2 つです。バージョン 2025-11-25 で使えなくなりました。

  1. http
    MCP サーバーを HTTP サーバーとして起動し、リクエスト/レスポンスで送受信する方式。RESTful で、既存の Web インフラと相性が良い。リモート配置やロードバランスが容易
  2. sseServer-Sent Events
    クライアントが HTTP 接続を張りっぱなしにし、サーバーからのイベントを一方向ストリームで受け取る方式。長時間処理や逐次結果の返却に向いており、「サーバーから喋り続ける」実況タイプの通信

いずれの経路(トランスポート)であっても共通言語は JSON-RPC です。本記事は「シンプルな機能で、動くものからスタートする」ことが目的なので、transport には stdio(標準入出力)を利用します。

MCP の公式ライブラリ(Go-SDK)を使う場合、MCP サーバーを実行する際に指定するだけです。

err := server.Run(context.Background(), &mcp.StdioTransport{})

stdio はローカル利用に優れていますが、リモート接続や長時間処理が必要(別マシンでの稼働や出力量が多い)場合は Streamable HTTP(旧 HTTPSSE)を検討する必要が出てきます。

その場合であっても、Go-SDK を使うと以下のように MCP サーバーのインスタンスを http サーバーのハンドラに割り当てるだけです。

http transport の例
const (
    host = "localhost"
    port = "8000"
)

func main() {
    server := mcp.NewServer(
        &mcp.Implementation{
            Name:    "text-mirror",
            Title:   "Text mirroring tool",
            Version: "1.0.0",
        },
        &mcp.ServerOptions{},
    )

    mcp.AddTool(server, &mcp.Tool{
        Name:        "mirror",
        Description: "Reverses the given UTF-8 text",
    }, handleMirror)

    // Streamable HTTP のハンドラ
    handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server {
      return server
    }, nil)

    // いわゆる普通の HTTP サーバーにハンドラを割り当てて起動
    url := fmt.Sprintf("%s:%d", host, port)

    err := http.ListenAndServe(url, handler)
    if err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}

func handleMirror(ctx context.Context, req *mcp.CallToolRequest, params *GetTimeParams) (*mcp.CallToolResult, any, error) {
    // ここに機能を実装
}

MCP サーバーの役割

MCP サーバーは、以下の 3 つの機能を MCP クライアントに提供することができます。

  1. Tools
    • クライアントの AI が必要に応じて呼び出すことができる「実行可能な機能」です。演算処理、外部 API の呼び出し、ファイル操作などを行います
  2. Resources
    • コンテキスト提供を目的とした「読み取り専用の情報源」です。ドキュメントやデータ構造などを参照できます
  3. Prompts
    • あらかじめ定義された指示テンプレートです。AI に対して、ツールやリソースの使い方を含めた「作業指針」を与えます

最後の「プロンプト」は、詳しい使い方を AI に伝えるものなので分かりやすいのですが、「ツール」と「リソース」の使い分けに迷うことがあります。

例えば「辞書引きツール」を考えた場合、ツールとリソースの両方に該当するためです。

ポイントは「AI が戻り値をどう使うか」になります。

  • 「ツール」の戻り値は「正しい答え」として扱う
  • 「リソース」の戻り値は「参考情報」として扱う

この記事では「ツール」の実装に特化して話を進めます。

MCP サーバーの仕組み(stdio 型)

MCP サーバーやツールの実装の前に、VS Code で MCP サーバーを stdio 経由で動かす際の「全体的な流れ」をまとめます。

  1. 準備
    • MCP クライアント(VS Code)は起動時に MCP 設定(mcp.json)を読み込む
  2. 起動
    • AI Chat でツール利用の指示があると、MCP 設定から適切なツールを推論し、MCP サーバーを起動(サブプロセスとして起動)する
  3. 待機
    • MCP サーバーは標準入出力(stdio)でツール(関数・メソッド)呼び出しを待ち受ける
  4. リクエスト
    • VS Code は命令データ(JSON-RPC)を MCP サーバーの標準入力(stdin)に送る
  5. 処理とレスポンス
    • MCP サーバーはリクエストを処理し、テキストを反転(ここでは handleReverse を実行)して、その結果を標準出力(stdout)経由で返す
  6. 表示
    • VS Code は返されたレスポンスを受け取り、ユーザーに表示する(3 に戻る)

MCP サーバーの作成

mcp.NewServer() のコンストラクタを使って mcp.Server を生成します。

最低限必要な情報は、第 1 引数の mcp.ImplementationName(サーバー名)と Version(サーバーのバージョン)フィールドですが、Title(サーバーの機能説明)も明記することをオススメします。

サーバーの作成
// mcp.Server のインスタンスをメタデータ付きで生成
server := mcp.NewServer(
    &mcp.Implementation{
        Name:    "text-mirror",         // サーバーの表示名
        Version: "1.0.0",               // サーバーのバージョン
        Title:   "Text mirroring tool", // UI や人間向けの表記(オプション)
    },
    &mcp.ServerOptions{},
)
  1. mcp.Implementation.Name:
    • クライアント側のサーバーの一覧で表示される名前です。内部的に mcp_text-mirror のように mcp_ の接頭辞付きで管理され、AI の解釈にも利用されます
  2. mcp.Implementation.Version:
    • 自作サーバーのバージョンです。デバッグや再現性の検証の際は重要になります
  3. mcp.Implementation.Title:
    • オプション項目で UI や人間がサーバーの用途を表示・判断するための情報です。この値を AI が使うかはクライアント側に依存しますが、VS Code は利用していない感じです。念の為、人間・AI どちらにも伝わる端的な説明が望ましいと思います。なお、この項目が空の場合は、Name の値が使われます

mcp.NewServer の第 1 引数を nil で渡してしまうとパニックになるので注意します。

また、第 2 引数の mcp.ServerOptions はサーバーの挙動をコントロールするオプションです。

本記事では利用していませんが、接続しているクライアントに渡す「追加プロンプト」や、ツール数が多い場合のページ長などが指定できます。詳しい設定項目は公式ドキュメントを参照してください。

MCP サーバーのツール

MCP ツールは、ただの関数

MCP では、以下の 3 つをセットで「ツール」と呼びます。

  1. サーバーが提供できる実行機能
  2. その機能名
  3. 機能の説明文

この実行機能は「関数」(もしくはメソッド)を指すのですが、サーバーのインスタンスに登録することで、ツールとして提供することができます。

Web API で言う、ホストが「サーバー」で、OpenAPI 的な情報付きのエンドポイントが「ツール」であると考えると、わかりやすいかもしれません。

ツールの登録

以下は、自作の handleMirror 関数を "mirror" ツールとして 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.
スクリーンショット 2026-01-08 13.31.35.png
VS Code での見え方

なお、日本語の説明文でも動く事は多いのですが、大半の AI は英語ベースです。つまり、英文の方が「モデルの迷いを減らす」という点を留意します。

ツールの型(関数型の構文)

肝心のツールの実装ですが、mcp.ToolHandlerFor 型に準拠している関数として実装する必要があります。

ToolHandlerFor の構文
type mcp.ToolHandlerFor[In, Out any] func(
        ctx context.Context,      // サーバーからのコンテキスト
        request *CallToolRequest, // クライアントからの付加情報
        input In,                 // クライアントからの入力情報
    ) (
        result *CallToolResult, // ツールからの付加情報
        output Out,             // ツールからの出力情報
        err error,              // エラーの有無
    )

つまり 3 つの引数を受け取り、3 つの値を返す関数を実装するだけです。

以下が実装のサンプルです。引数を受け取って、処理して、型にハメて返しているだけです。

handleMirror()
func handleMirror(
    _ context.Context,
    _ *mcp.CallToolRequest,
    input MirrorInput,
) (*mcp.CallToolResult, MirrorOutput, error) {
    // 反転実行
    reversed := uniseg.ReverseString(input.Text)

    return nil, MirrorOutput{Text: reversed}, nil
}

ここでは、第3引数の input と、第2戻り値の型(MirrorInput 型と MirrorOutput 型)が重要になります。

先の mcp.ToolHandlerFor の構文を見てもらいたいのですが、InOut に注目してください。

ToolHandlerFor の構文
type mcp.ToolHandlerFor[In, Out any] func(
        ctx context.Context,      // サーバーからのコンテキスト
        request *CallToolRequest, // クライアントからの付加情報
        input In,                 // クライアントからの入力情報
    ) (
        result *CallToolResult, // ツールからの付加情報
        output Out,             // ツールからの出力情報
        err error,              // エラーの有無
    )

ジェネリックな any 型ではあるものの、実は入力(In)と出力(Out)の型は JSON にパースできるものである必要があります。

このプロジェクトでは InOut 用に以下のように型を定義しています。フィールドのタグに jsonjsonschema が定義されているのがわかります。

In
// MirrorInput は "mirror" ツールの入力スキーマを定義します。
type MirrorInput struct {
    Text string `json:"text" jsonschema:"UTF-8 text to be mirrored"`
}
Out
// 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 の引数を(_ で)利用していないことにお気づきでしょう。

handleMirror()
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 の場合は、特に必要のない情報であるため、引数からデータを受け取っていません。

しかし、複数のクライアントがサーバーに接続する場合、特に Streamable HTTP トランスポートを使う場合は、チャットの統合性を保つためにも重要な情報になってきます。

このように、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...)
    }
}

開発中の任意のタイミング(箇所)でのデバッグでない場合は、ロガーをミドルウェアとして登録する方法もあります。デプロイ後のトラブル・シューティングなどに活用できます。

server := mcp.NewServer(
    &mcp.Implementation{
        Name:    "text-mirror",
        Title:   "Text mirroring tool",
        Version: "1.0.0",
    },
    &mcp.ServerOptions{},
)

// Add MCP-level logging middleware.
server.AddReceivingMiddleware(createMyLoggingMiddleware())

exhaustruct リンターとの戦い

golangci-lintexhaustruct リンターは構造体の全フィールド明示的初期化を要求します。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 クライアントでの動作検証
  • Ollama を使った MCP クライアント実装と MCP サーバーの利用(なんちゃって AI エージェントの実装)

所感

2023〜2024 年に流行った RAG(検索拡張生成)は、いまや昔。その後(2024 年後半から)急速に普及した MCP は、AI エージェントが外部ツールを活用するための標準プロトコルになりつつあります。

2025 年にソフトバンクの孫さんが「組織内で 10 億個の AI エージェントを稼働させる」という「千手観音プロジェクト」構想を発表したのをきっかけに、「AI チャットと、ちちくりあって満足している場合ではない」と危機意識を持ちました。

筆者は Go 使いなので Ollama でローカル AI を動かす方法は把握したものの、「MCP」はいまいち理解できていませんでした。「AI エージェント」も「AI Chat のパーサーみたいなもんでしょ?」程度の認識。

遅れを取ってはいけないとシンプルな実装から始めたところ、MCP の仕組みが理解でき「おお、これは使えるかも」と実感しました。

しかし同時に、「インターネットが歩んできた道」と同じ道を高速に歩んでいるとも思いました。

インターネット創世記、個人でもホームページを持つのが普通になると同時に情報が溢れ、その索引として Yahoo! が生まれました。

後にマンパワーによる索引作成の限界に対して Google が生まれ、読み手より SEO(検索エンジン嗜好)な記事と格闘する現実(イタチごっこ)があります。

いつの時代も「嘘を嘘と見抜ける力」が求められており、それに疲弊している人が増えているなか、MCP も同じように乱立し、キュレーター的なものが必要になる可能性は作っていて感じました。

組織や社会のように専門化・細分化していく——「ジャンルごとにまとめた MCP 群を取りまとめる AI エージェント」があり、その上に「目的にあった AI エージェントをマネージングする AI エージェント」といったレイヤー構造が必要だろうな、という実感です。

おそらく RAG もオワコンではなく、いまの RSS/ATOM のような情報収集ツールの 1 つとして残っていく気もします。IT 業界が好きな「技術のマッシュアップ」です。

また、セキュリティに関しても同様です。

ブラウザの拡張機能や無料アプリが「個人情報を別サーバーに送信していた」「AI 搭載というのでマシンのファンがハーハーと頑張っていると思ったら、暗号資産のマイニングをしていただけ」といったリスクと同じリスクが MCP にもある(悪意ある機能を簡単に埋め込める)と、自分で作ってみて実感しました。

孫さんの構想は「ツールの大量生産」という印象を持っていたのですが、むしろ「自分たちが安心して利用できるものを、自分たちで作る・用意する」という意味が根底にあると理解できたのも、自作の MCP サーバーを作って良かったと思いました。

参考リンク

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?