📦

modelcontextprotocol/go-sdk を使ってリソース・プロンプト配信のコマンドを実装してみる

に公開

はじめに

modelcontextprotocol/go-sdk が今週公開されたとのことで、golangの勉強がてらMCPサーバーの実装をしていた私にとって朗報でした👏

https://github.com/modelcontextprotocol/go-sdk

このSDKを利用して、MCPのtool, prompt, resourceの機能のうち、prompt, resourceをシングルバイナリで配布するサンプルを組んでみた(実際はClaude Codeが書きました)ので、紹介してみます。

モチベーション

最近はAI agentが乱立しており、Claude Codeならば CLAUDE.md に、GitHub Copilotならば .github/instructions 配下に、、といった具合に、agentに読み込ませるリソースやプロンプトの管理が難しく、「MCPサーバーとして配布できればどのagentからも参照しやすいのでは?」と思ったのがモチベーションです。

仮にプロンプトやリソースをmarkdownファイルで管理していくとして、それらをどう参照させるか考えた時に、「そういえばgolangにはembedという機能があったよな」と思い当たりました。

embedとは、Goのコンパイル時にファイルシステムからファイルを読み込んで、バイナリファイル内に直接埋め込む機能です。Go 1.16で導入されました。

これにより、以下のようなメリットがあります:

  • コンパイル時の埋め込み: go build 時にファイルがバイナリに組み込まれる
  • シングルバイナリ配布: 外部ファイルへの依存がなくなる
  • 高速アクセス: ファイルシステムではなくメモリからの読み込み

基本的な使い方は次の通りです:

package main

import (
    _ "embed"
    "fmt"
)

//go:embed config.json
var configData string

func main() {
    fmt.Println(configData) // config.jsonの内容が出力される
}

私は以前ElixirでNimblePublisherというライブラリを使った経験があり、これもMarkdownファイルをコンパイル時に読み込んでランタイムで記事データを参照する仕組みを採用しており、同様のアプローチを思い出した次第です。

https://github.com/dashbitco/nimble_publisher

出来上がったもの

サンプルプロジェクトをこちらに置いています。

https://github.com/koga1020/golang-embed-mcp-sample

動作の様子

Claude Codeでのサンプルです。 claude mcp add でコマンドを登録後、実際に起動すると埋め込まれたプロンプトとリソースが参照できています👏

prompt
resource

実際にsampleのプロンプトを動かしてみると、動作していることがわかります。

> /embed-mcp:sample (MCP) is running…

⏺ I can see you have a Go project that implements an MCP (Model Context Protocol) server with
  embedded resources. This is a clean example of using Go's embed directive to bundle prompt
  templates directly into the binary.

  The project structure shows:
  - An MCP server implementation that serves embedded prompts as resources
  - A sample prompt template demonstrating the embedding capability
  - Proper CLI support with command filters

  The key benefit here is that you can distribute a single binary that contains all the prompt
  templates, making deployment extremely simple - no need to manage separate files or ensure they're
   in the right place at runtime.

  The implementation uses:
  1. //go:embed to bundle the prompts directory
  2. MCP's resource system to expose these prompts
  3. Command-line filtering to selectively load prompts

  This approach is particularly useful for:
  - Distributing prompt libraries as single executables
  - Ensuring prompts are always available with the binary
  - Simplifying deployment in containerized environments

  Is there something specific you'd like to explore or modify in this implementation?

プロジェクト構造

cmd/embed-mcp/
├── prompts/          # 埋め込まれるプロンプト
│   ├── demo.md
│   └── sample.md
├── resources/        # 埋め込まれるリソース
│   ├── config.json
│   └── info.md
└── main.go           # メインエントリーポイント

実装の詳細

実装上のポイントをピックアップして解説します。

//go:embed ディレクティブの活用

メインの実装で、静的ファイルを埋め込むためのディレクティブを指定します:

cmd/embed-mcp/main.go
package main

import (
    "embed"
    // ... その他のimport
)

//go:embed prompts/*
var embeddedPrompts embed.FS

//go:embed resources/*
var embeddedResources embed.FS

func main() {
    // ... 実装
}

この //go:embed ディレクティブによって、prompts/resources/ ディレクトリ以下のファイルが ビルド時に バイナリに直接埋め込まれます。

埋め込んだファイルを参照する

m.embeddedPrompts.ReadFile() を使用してファイルにアクセスします。この時点で既にバイナリに埋め込まれているため、外部ファイルへのアクセスは発生しません。

internal/prompts/manager.go
func (m *Manager) readEmbeddedPrompt(promptName string) ([]byte, error) {
    promptPath := fmt.Sprintf("prompts/%s.md", promptName)

    content, err := m.embeddedPrompts.ReadFile(promptPath)
    if err != nil {
        return nil, fmt.Errorf("prompt not found: %s", promptName)
    }

    return content, nil
}

MCPサーバーとしての動作

AI agentからのリクエストを受け取り、埋め込まれたプロンプトやリソースを返す処理です。この辺りはSDKのサンプルままで実現できます。

サンプルはリポジトリのexamples配下で提供されています。

https://github.com/modelcontextprotocol/go-sdk/tree/main/examples

internal/prompts/manager.go
func (m *Manager) handlePrompt(_ context.Context, _ *mcp.ServerSession, params *mcp.GetPromptParams) (*mcp.GetPromptResult, error) {
    content, err := m.readEmbeddedPrompt(params.Name)
    if err != nil {
        return nil, fmt.Errorf("failed to read prompt: %w", err)
    }

    return &mcp.GetPromptResult{
        Messages: []*mcp.PromptMessage{
            {
                Role: "user",
                Content: &mcp.TextContent{
                    Text: string(content),
                },
            },
        },
    }, nil
}

フィルタリング機能の実装

コマンドラインのオプションによって、特定のプロンプトやリソースのみを有効化できる機能も組んでみています。

cmd/embed-mcp/main.go
func main() {
    var promptFilter arrayFlags
    var resourceFilter arrayFlags

    flag.Var(&promptFilter, "prompts", "Prompt names to include (can be used multiple times)")
    flag.Var(&resourceFilter, "resources", "Resource names to include (can be used multiple times)")
    flag.Parse()

    // Manager作成時にフィルターを渡す
    promptManager := prompts.NewManager(embeddedPrompts)
    resourceManager := resources.NewManager(embeddedResources)

    // フィルターに基づいてコンテンツを取得
    promptList := promptManager.GetPromptsWithFilter(promptFilter)
    resourceList := resourceManager.GetResourcesWithFilter(resourceFilter)
}

この機能により、プロジェクトに応じて必要なコンテンツのみを有効化できます。たとえばPHPプロジェクトではTypeScriptのガイドラインを除外したり、フロントエンド開発ではバックエンドのプロンプトを読み込まないといったニーズに対応できます。

# PHPプロジェクト用(TypeScript関連を除外)
./embed-mcp --prompts php

# フロントエンド開発用
./embed-mcp --prompts typescript,react

活用シナリオと今後の展望

まずは個人の範囲でClaude CodeやGitHub Copilot利用時のコンテキストの差をなくし、プロンプトの一元管理に活用していく予定です。

ゆくゆくは組織に展開し、以下のようなガイドラインを配布することで、組織全体のアウトプットのクオリティ向上に寄与できると良さそうだなと考えています。

  • 基本的なコーディングガイドライン
  • 言語・フレームワーク固有のガイドライン
  • commit、Pull Requestのガイドライン

まとめ

Goの//go:embedとMCP Go SDKを組み合わせることで、プロンプトやリソースを単一バイナリで配布できるMCPサーバーを実装しました。

シングルバイナリのポータビリティは素晴らしく、コマンドでサクッとMCPサーバーを立ち上げられるのはとても楽です。

アイデア次第でいろんな活用ができそうだと思いますので、興味がある方はぜひ参考にしてみてください。

余談

Claude Codeでの開発中、このSDKのリポジトリのdeepwiki.comをplaywright MCPで読ませながら作業するとそれらしいコードがすぐに出てきました。このテクニックは今後も使っていきたいと思います!

https://deepwiki.com/modelcontextprotocol/go-sdk

参考リンク

スタフェステックブログ

Discussion