📮

MCP の事を理解するために MCP Server を実装してみる

に公開

普段MCPを活用してみているものの仕組みをあまりよく理解できてなかったので、ちゃんと理解するために自サービスの Zipcoda API を利用する MCP Server を実装してみました。そこで得られた知見などをここに残しておきます。

Zipcoda API MCP Server

まず最初に、出来たものがこちらです。

npx で GitHub のリポジトリから直接起動する方法でインストールする形としました。例えば VS Code の場合はこのような設定になるでしょう。

"mcp": {
  "servers": {
    "zipcoda-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "github:mach3/zipcoda-mcp"
      ]
    }
  }
}

設定後、例えば :

  • 「郵便番号1000001の住所を教えて」
  • 「松風という住所の郵便番号を教えて」

などと尋ねれば Zipcoda API から引いた結果で返答してくれるはずです。

MCPってなんなんだ

MCP(Model Context Protocol)とは、端的に言うと「モデルに渡す情報(コンテキスト)をやり取りするための決まりごと」だそうです。

例えば AI に作業をお願いするうえで、あるサービスから情報を取得させようとしても、AIはそのサービスとコミュニケーションする方法を知らないので取得できません。そのための規則と方法を定めたのが MCP で、それを実装しアダプタとして機能するのが MCP Server だ、という理解をしています。

MCPはプロトコルなので、規則です。HTTPと似たような概念です。みんなはその規則に従ってMCP Serverを実装して、それを使ってAIは様々な情報ソースとコミュニケーションを取ることができます。雑な理解による説明おわり。

MCP Server 実装の選択肢

MCP Server の実装は代表的なもので2つ挙げられます。

1. 標準入出力(STDIO)による実装

  • シンプルで軽量
  • ローカルで起動するケースに適している

ローカルで起動する MCP Server 実装といえば playwright-mcp があります。これは渡すオプションによってSTDIO実装かHTTPサーバー実装(SSE)かを選択する事ができるようです。

2. HTTPサーバーによる実装

  • コストが高くなる
  • SSE, Streamable HTTP など多彩な方式から選択できる

リモートのHTTPサーバーを利用する形式は、先日ベータ版が発表された Atlassian MCP で採用されているようです。

今回の選択

今回 Zipcoda API の MCP Server を作るにあたっては、STDIO実装を選択することにしました。極めてシンプルなものだからというのが主な理由ですが、もうひとつ、HTTPサーバーをローカルで起動する場合に起動ポートについて気を使わなければいけないというデメリットもありました。(かといって、Atlassian MCP のように自分がホスティングするというのはトラフィック量の懸念もあり気が進みませんでした)

modelcontextprotocol/typescript-sdk

SDKは modelcontextprotocol/typescript-sdk を使いました。

READMEの例示の通りにやれば簡単に MCP Server が開発出来ました。STDIOならば Quick Start のコードが全てです。

import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Create an MCP server
const server = new McpServer({
  name: "Demo",
  version: "1.0.0"
});

// Add an addition tool
server.tool("add",
  { a: z.number(), b: z.number() },
  async ({ a, b }) => ({
    content: [{ type: "text", text: String(a + b) }]
  })
);

// Add a dynamic greeting resource
server.resource(
  "greeting",
  new ResourceTemplate("greeting://{name}", { list: undefined }),
  async (uri, { name }) => ({
    contents: [{
      uri: uri.href,
      text: `Hello, ${name}!`
    }]
  })
);

// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);

.tool で MCP Server に tool と呼ばれるメソッドを備えて、モデルからはその tool をコールしてコミュニケーションを行うというイメージです。

HTTPサーバー実装について

ちなみにSTDIOに舵を切る前にHTTPサーバー実装も試してみました。フレームワークは公式の例示にある express ではなく hono を採用したのですが、 hono は Fetch API ベースで書かれているためSDKにうまく接続できないようです。

が、以下の記事にあるように fetch-to-node パッケージで変換してやることで正常に動くようになりました。modelcontextprotocol/typescript-sdk による MCP Server 実装に hono を採用する場合は助けになると思います。参考にしてください。

HTTPサーバー実装版も完成はしたのですが、上で述べているように起動ポートまわりが気に入らなかったので舵を切り直しました。コードはまだ残っているので気が向いたらどこかに共有しようかと思います。

動作確認とテスト

実際に VS Code で設定してやれば動作はするのですが、それで動作確認は効率が悪いのでテストを書きます。テスト方法は、 modelcontextprotocol/typescript-sdk で MCP Client の実装ができるのでそれでもって通信していきます。

まずは Client を生成する関数です :

async function createClient() {
  const transport = new StdioClientTransport({
    command: "node",
    args: ["bin/server.js"],
  });

  const client = new Client({
    name: "test-client",
    version: "1.0.0",
  });

  await client.connect(transport);
  return { client, transport };
}

この関数で作られた Client を使ってテストを書いていきます :

  it("should search by zipcode", async () => {
    const { client, transport } = await createClient();

    try {
      const result = await client.callTool({
        name: "search_by_zipcode",
        arguments: {
          zipcode: "1000001",
        },
      });
      // 取得された result でテストを書く
    } finally {
      await transport.close?.();
    }
  });

.callTool で MCP Server の tool を呼ぶことができ、それで返ってきたデータを検証することができます。

おわりに

Zipcoda API は郵便番号から住所、あるいは住所から郵便番号を検索してリストで返すだけの無機質なサービスです。が、これがAIを介すと(気まぐれで再現性はないですが)独自の解釈やエッセンスを加えてくれて、どことなく人間味を感じます。まさか…?

Discussion