MCPサーバーの作り方 — 自作してわかった設計判断とCLIとの使い分け
「Claude Codeに自分のデータを扱わせたい」——そう思ったとき、最初にたどり着くのがMCPサーバーの自作です。
自分はブログのアクセス分析をClaude Codeに任せるために、GA4とSearch Consoleに接続するMCPサーバーをTypeScriptで作りました。実際に運用してみると、MCPが向いているケースと、シンプルなCLIスクリプトの方が良いケースがあることに気づきました。
この記事では、MCPサーバーをゼロから作って動かすまでのプロセスを、Claude Code上の実行結果を交えて解説します。なお、MCPと並行してClaude Code CLIのツール連携機能も急速に充実しています。MCPが常に最適解ではない理由も後半で触れます。
MCPとは何か
MCP(Model Context Protocol)は、AIアシスタントと外部ツールをつなぐためのオープンプロトコルです。Anthropicが2024年に公開しました。
従来、AIに外部データを扱わせるにはAPIを叩くコードを毎回書く必要がありました。MCPはこのインターフェースを標準化し、「ツール定義」と「入出力のやりとり」を統一的なプロトコルで行えるようにします。MCPサーバーを1つ書けば、それがそのままClaude Codeの「能力」になります。
実際に作ってみる
説明だけでは実感が湧かないと思います。ゼロからMCPサーバーを作って動かすまでを、実際にClaude Code上で実行した結果とともに追いかけてみます。
プロジェクトの初期化
$ mkdir mcp-demo && cd mcp-demo
$ npm init -y
$ npm pkg set type=module
$ npm install @modelcontextprotocol/sdk zod
added 91 packages, and audited 92 packages in 1s
found 0 vulnerabilities
必要なパッケージは @modelcontextprotocol/sdk と zod の2つです。SDKがプロトコル処理を担い、zodがツールの入力スキーマ定義に使います。
最小構成のサーバー
SDK v1系では McpServer クラスと server.tool() を使うスタイルが推奨されています。旧来の Server + setRequestHandler より記述量が大幅に減ります。
// index.mjs
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "demo-server",
version: "1.0.0",
});
server.tool(
"count_chars",
"テキストの文字数をカウントする",
{ text: z.string().describe("カウント対象のテキスト") },
async ({ text }) => ({
content: [{ type: "text", text: `文字数: ${text.length}` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
server.tool() の引数は 名前, 説明, zodスキーマ, ハンドラ の4つだけです。StdioServerTransport を使い、標準入出力でClaude Codeと通信します。HTTPサーバーを立てる必要はありません。
動作確認
JSON-RPCメッセージを流し込んで、プロトコルレベルで動作を確認できます。Pythonのサブプロセスを使うと複数メッセージを順番に送りやすいです。
実際にClaude Code上で確認した結果がこちらです。
initialize(接続確立):
{
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": { "listChanged": true }
},
"serverInfo": { "name": "demo-server", "version": "1.0.0" }
},
"jsonrpc": "2.0",
"id": 1
}
tools/list(ツール一覧):
{
"result": {
"tools": [
{
"name": "count_chars",
"description": "テキストの文字数をカウントする",
"inputSchema": {
"type": "object",
"properties": {
"text": { "type": "string", "description": "カウント対象のテキスト" }
},
"required": ["text"]
}
}
]
},
"jsonrpc": "2.0",
"id": 2
}
tools/call(ツール実行):
{
"result": {
"content": [{ "type": "text", "text": "文字数: 11" }]
},
"jsonrpc": "2.0",
"id": 3
}
ここまでで、MCPサーバーの骨格は完成です。あとはこの count_chars ツールを、実際に使いたいAPIに差し替えればいいだけです。
Claude Codeに接続する
作ったサーバーをClaude Codeに認識させるには、CLIから追加するのが最も手軽です。
$ claude mcp add demo-server node /path/to/mcp-demo/index.mjs
実際に実行すると以下の出力が得られます。
Added stdio MCP server demo-server with command: node /path/to/mcp-demo/index.mjs to local config
File modified: ~/.claude.json [project: /path/to/your-project]
claude mcp list で接続状態を確認します。
$ claude mcp list
Checking MCP server health...
plugin:context7:context7: npx -y @upstash/context7-mcp - ✓ Connected
plugin:playwright:playwright: npx @playwright/mcp@latest - ✓ Connected
...
demo-server: node /path/to/mcp-demo/index.mjs - ✓ Connected
✓ Connected が表示されれば完了です。次回 claude を起動した時点から、定義したツールが使えるようになります。
スコープの選び方
claude mcp add には --scope オプションがあり、サーバーの可視範囲を制御できます。
| スコープ | 保存先 | 用途 |
|---|---|---|
local(デフォルト) | ~/.claude.json | 個人の開発用。認証情報を含むものはここ |
project | .mcp.json(プロジェクトルート) | チームで共有するサーバー。git管理される |
user | ~/.claude.json | 複数プロジェクトで使う個人ツール |
自分の場合、GA4のサービスアカウント認証が必要なので local スコープにしています。チームで使うなら project にして、認証情報は環境変数で渡す設計にするのが良いです。
実用的なMCPサーバーを作る際の設計判断
最小構成が動いたら、次は実際に役立つサーバーを作る段階です。自分がGA4/Search Console連携のMCPサーバーを作る中で学んだ設計判断を共有します。
ツールの粒度: 1ツール = 1API呼び出し
最初は「GA4のデータを全部返す」汎用ツール1つにしようかと考えました。しかし、ツールの粒度を適切に分けた方がAIの判断精度が上がります。
自分のサーバーでは4つのツールに分けました。
| ツール名 | 用途 |
|---|---|
run_ga4_report | GA4の任意レポート取得 |
get_realtime_report | リアルタイムデータ(直近30分) |
search_console_query | Search Consoleの検索パフォーマンス |
search_console_sitemaps | サイトマップの状態確認 |
「リアルタイムの状況を知りたい」→ get_realtime_report が選ばれ、「先月のSEO状況を調べて」→ search_console_query が選ばれます。複数APIをまたぐ処理はAI側に任せる方が柔軟です。
descriptionがツール選択を左右する
ツールの description はAIがどのツールを使うか判断する際の最重要情報です。
server.tool(
"run_ga4_report",
"Run a report on Google Analytics 4 data (property 254228045) to retrieve metrics and dimensions for amaino.me.",
{
metrics: z.array(z.string()).describe(
"List of metrics (e.g., 'activeUsers', 'screenPageViews', 'sessions', 'bounceRate')"
),
startDate: z.string().describe("Start date in YYYY-MM-DD format"),
endDate: z.string().describe("End date in YYYY-MM-DD format"),
},
async ({ metrics, startDate, endDate }) => { /* ... */ }
);
ポイントは2つです。
- 対象を具体的に書く: サイト名やProperty IDを含めると、AIが「どのサイトのデータか」を正確に判断できます
- パラメータの例を列挙する: 利用可能な値の例をdescriptionに書いておくと、AIが適切なパラメータを構成しやすくなります
レスポンスはテキスト形式に変換する
GA4 APIのレスポンスはネストが深く、そのまま返すとトークンを無駄に消費します。タブ区切りのテキストに変換してから返すようにしました。
const humanReadable =
`GA4 Report (property ${PROPERTY_ID})\n` +
`Period: ${startDate} to ${endDate}\n\n` +
`${headerString}\n${rows?.join("\n")}`;
return {
content: [{ type: "text", text: humanReadable }],
};
AIが読むデータなので、JSONよりもTSV形式の方がトークン効率が良く、かつ構造を十分に伝えられます。
MCPが常に最適解ではない — CLIとの使い分け
MCPサーバーを作って運用する中で気づいたことがあります。MCPが向いていないケースもあるということです。
MCPのトークンコスト
MCPサーバーを接続すると、ツール定義がシステムプロンプトに載ります。これは毎ターントークンとして消費されます。ツール4つ、各パラメータのdescription込みで、自分のGA4サーバーだけでざっくり1,000〜2,000トークン程度です。
MCPサーバーを複数接続すれば、その分だけ毎ターンのベースコストが上がります。現在の自分の環境の claude mcp list はこんな状態です。
plugin:context7:context7: npx -y @upstash/context7-mcp - ✓ Connected
plugin:playwright:playwright: npx @playwright/mcp@latest - ✓ Connected
plugin:serena:serena: uvx ... - ✓ Connected
codex: codex mcp-server - ✓ Connected
gmail: node /path/to/gmail-mcp-server/src/index.js - ✓ Connected
council: node /path/to/ai-master/dist/index.js - ✓ Connected
...
それぞれのツール定義がシステムプロンプトに載るので、何もしなくても毎ターン数千トークンのオーバーヘッドが発生しています。ここを意識しないまま「便利そうだから」とMCPサーバーを増やし続けると、じわじわとコストが膨らみます。
CLIスクリプトという選択肢
一方、Claude CodeのBashツールでCLIスクリプトを叩く方法なら、ツール定義のオーバーヘッドはゼロです。必要な時だけコマンドを実行し、その結果を受け取ります。
$ node scripts/ga4-report.mjs --metrics activeUsers,screenPageViews --days 30
Claude Codeは Bash ツールでこのコマンドを叩き、出力を読むだけ。ツール定義がシステムプロンプトを圧迫することはありません。
実はClaude Code CLI自体のツール連携機能も急速に充実していて、claude plugin install でコミュニティのプラグインを追加したり、claude agents でサブエージェントを管理したりといった仕組みが整備されています。「MCPサーバーを自作しなくても、CLIプラグインで事足りる」ケースも増えています。
使い分けの判断基準
MCPサーバーとCLIスクリプト、実際にはどう使い分ければよいでしょうか。
MCPが向いているケース:
- AIが自律的にツールを選んで呼ぶ必要がある(「ブログの状況を分析して」→ 複数ツールを組み合わせて回答)
- 頻繁に呼ばれるツール(毎ターンのオーバーヘッドを上回る利用頻度)
- パラメータの構成にAIの判断が必要(期間やメトリクスをコンテキストから推論)
CLIが向いているケース:
- 人間が「いつ・何を」実行するか判断するもの(デプロイ、マイグレーションなど)
- たまにしか使わないツール(毎ターンのトークンコストに見合わない)
- 引数が固定的で、AIに推論させる必要がない
- コミュニティのプラグインで代替できるもの
自分の場合、GA4/Search ConsoleはMCPのまま運用しています。「ブログの分析をして」という曖昧な指示から、AIが適切なメトリクスと期間を選んでデータを取得し、分析まで一気通貫でやるのはMCPでないと実現できません。
一方、Google Suggest APIを叩いてキーワードサジェストを取得する処理は、CLIスクリプトにしています。呼ぶタイミングは人間が判断し、キーワードも人間が指定するので、MCPにする必要がないからです。
まとめ
MCPサーバーの構築は、思ったほど難しくありません。SDK v1系の McpServer + server.tool() を使えば、ツール1つあたり数行で定義できます。
ゼロからの手順をまとめるとこうなります。
npm init -y && npm install @modelcontextprotocol/sdk zodMcpServerを作り、server.tool()でツールを定義する- JSON-RPCを流し込んでプロトコルレベルで動作確認
claude mcp addでClaude Codeに接続、claude mcp listで✓ Connectedを確認
設計で意識すべきポイント:
- 1ツール = 1API呼び出しの粒度にする
- descriptionは具体的に書く。AIのツール選択精度に直結する
- レスポンスはテキスト形式に変換する。JSONをそのまま返さない
- MCPかCLIかを使い分ける。毎ターンのトークンコストを意識する
まず試すなら、claude mcp add で既存のnpxパッケージをローカルに追加してみるのが手軽です。claude mcp add context7 -- npx -y @upstash/context7-mcp のように、公開されているMCPサーバーを1コマンドで接続できます。自作はその次のステップです。
- MCP公式 — Build an MCP Server
- Claude Code — Connect to tools via MCP
- @modelcontextprotocol/sdk (npm)
- Claude Code CLI reference
記事の更新をメールで受け取る
質問・リクエストを送る
記事についての質問や、取り上げてほしいテーマがあればお気軽にどうぞ。いただいた質問はブログ記事として回答し、Q&Aページで公開することがあります。