MCPサーバーの作り方 — 自作してわかった設計判断とCLIとの使い分け

15分で読める テック

「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/sdkzod の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_reportGA4の任意レポート取得
get_realtime_reportリアルタイムデータ(直近30分)
search_console_querySearch 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つあたり数行で定義できます。

ゼロからの手順をまとめるとこうなります。

  1. npm init -y && npm install @modelcontextprotocol/sdk zod
  2. McpServer を作り、server.tool() でツールを定義する
  3. JSON-RPCを流し込んでプロトコルレベルで動作確認
  4. 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コマンドで接続できます。自作はその次のステップです。


質問・リクエストを送る

記事についての質問や、取り上げてほしいテーマがあればお気軽にどうぞ。いただいた質問はブログ記事として回答し、Q&Aページで公開することがあります。