modelcontextprotocol/go-sdk を使ってリソース・プロンプト配信のコマンドを実装してみる
はじめに
modelcontextprotocol/go-sdk が今週公開されたとのことで、golangの勉強がてらMCPサーバーの実装をしていた私にとって朗報でした👏
この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ファイルをコンパイル時に読み込んでランタイムで記事データを参照する仕組みを採用しており、同様のアプローチを思い出した次第です。
出来上がったもの
サンプルプロジェクトをこちらに置いています。
動作の様子
Claude Codeでのサンプルです。 claude mcp add
でコマンドを登録後、実際に起動すると埋め込まれたプロンプトとリソースが参照できています👏
実際に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 ディレクティブの活用
メインの実装で、静的ファイルを埋め込むためのディレクティブを指定します:
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()
を使用してファイルにアクセスします。この時点で既にバイナリに埋め込まれているため、外部ファイルへのアクセスは発生しません。
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配下で提供されています。
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
}
フィルタリング機能の実装
コマンドラインのオプションによって、特定のプロンプトやリソースのみを有効化できる機能も組んでみています。
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で読ませながら作業するとそれらしいコードがすぐに出てきました。このテクニックは今後も使っていきたいと思います!
Discussion