🚀

Model Context Protocol (MCP) の実装をLLMに聞きながら雑に学ぶ

に公開

初学の内容をざっくり把握しながら学習を進めるのにLLMを使うと楽だよねという例

前提

  • MCPがなにかはざっくり分かっているところからのスタート
  • 呼び出したらHelloWorld的な内容を実装するところまでをゴール
  • 言語的なものはある程度分かっている前提

1.とりあえずどこで何を見れば良いのかを確認

o3で各ページの概要を確認

このページの内容を取得して要約してください
https://modelcontextprotocol.io/

MPCの概念とドキュメントのINDEX的な内容
今回実装するのはMCP ServerであるのForServerDeveloperを見れば良さそう

このページの内容を取得して要約してください
https://modelcontextprotocol.io/quickstart/server

チュートリアルの流れに沿って環境構築を進め
言語別のサンプルを参考にする。

補足としてTypeScriptを使うのでSDKも見ておく

このページの内容を取得して要約してください
https://github.com/modelcontextprotocol/typescript-sdk

これはコードメインなので自分で見たほうが早かった・・・

2.ひと通り見たあとで知識の補完

SDKのサンプルなどを見るとリソース、ツール、プロンプトの概念があるので概念確認
とりあえず下記と配下のページをさらっと見たうえでo3で補完する
https://modelcontextprotocol.io/docs/concepts/architecture

modelcontextprotocolに関して、リソース、ツール、プロンプトの概念を説明して。
  1. Resources(リソース)
    • 目的: サーバが保持するデータを URI で公開し、クライアントが必要に応じて読ませることで、RAG やコード参照などに利用できるようにする。

  2. Tools(ツール)
    • 目的: LLM に アクション を実行させる ― 外部 API 呼び出し、コード実行、システム操作など。

  3. Prompts(プロンプト)
    • 目的: 共通タスク用の テンプレート / ワークフロー をサーバがカタログ化し、ユーザーが簡単に呼び出せるようにする。

みたいなイメージとのことで
とりあえずToolsとして実装することにする。

追加で聞いたこと

クライアント側からサーバー側のToolを呼び出すかどうかは何で決まる?

Tool 定義(name / description / inputSchema など)を読んだ LLM が、推論過程で tool-call メッセージを生成するかどうかで決まる。

それぞれの具体的な例を挙げて

まとめ

  1. Resources で どのデータをモデルに読ませるか をアプリが制御。
  2. Prompts で 質問テンプレート をユーザーが選択。
  3. モデルは会話の途中で Tools を発火し、外部 API や DB を叩いて結果を取得 → 回答を強化。

概ねConcepts配下のDocumentを流し見た内容と比べて違和感はなし。

それぞれの入力値として取りうる値の差

まとめ

  1. Resources ― URI と小さな引数 しか渡さない。実体は後でストリーム読込。
  2. Tools ― JSON-Schema で許す限り何でも渡せるが、入れ過ぎはモデル負荷大。
  3. Prompts ― テンプレート穴埋め用の 少数スカラー/URI に留め、長文は Resource へ。

みたいな回答だった(実際はもっと具体的な内容あり)
これも概ねConcepts配下のDocumentを流し見た内容と比べて違和感なし。

念の為下記を要約

このページの内容を取得して解説してください
https://modelcontextprotocol.io/docs/concepts/tools

Toolsの実装の話

  • tools/list:MPCサーバーがなんの機能を備えているかを公開
  • tools/cal:機能の実行

の2エンドポイントを実装する必要がある。
スキーマ定義の内容、実装の参考コードが有るのでこれに沿えば良い。

スキーマはこんな感じにすることにすることに

{
    name: "say_hello",
    description: "挨拶のメッセージを返します",
    inputSchema: {
      type: "object",
      properties: {
        name: { type: "string" }
      },
      required: ["name"]
    }
}

3.実装

とりあえず最低限のライブラリ。
ここからは概ね手を動かすだけ。

{
  "name": "mcp-sample",
  "version": "1.0.0",
  "description": "MCP server for sample",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsc -w & node --watch dist/index.js",
    "test": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js --config=jest.config.cjs"
  },
  "keywords": [
    "mcp",
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.10.2",
  },
  "devDependencies": {
    "@types/node": "^20.10.5",
    "typescript": "^5.3.3"
  },
  "engines": {
    "node": ">=16"
  }
}

Implementing toolsを参考にコードを書く。というかほぼそのまま。

SDK側のサンプルはMPCServerクラスを使っているけど一旦サンプルに沿ってServerクラスを使う。
各クラスの所在はgithubを見る。

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ListToolsRequestSchema,CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";

async function main() {
  const server = new Server({
    name: "example-server",
    version: "1.0.0"
  }, {
    capabilities: {
      tools: {}
    }
  });

  // Define available tools
  server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
      tools: [{
        name: "say_hello",
        description: "挨拶のメッセージを返します",
        inputSchema: {
          type: "object",
          properties: {
            name: { type: "string" }
          },
          required: ["name"]
        }
      }]
    };
  });

  // Handle tool execution
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    if (request.params.name === "say_hello") {
      const args = request.params.arguments;
      return {
        content: [{
          type: "text",
          text: "hello " + args?.name
        }]
      };
    }
    throw new Error("Tool not found");
  });

  const transport = new StdioServerTransport();
  await server.connect(transport);

  process.on("SIGINT", () => {
    server.close();
    process.exit(0);
  });
}

main().catch(console.error);

4.動作確認

GithubにMCP Inspector.があるよって書いてるのでこれでデバッグ

ts側をbuidしてから起動

% npx @modelcontextprotocol/inspector node dist/index.js
Starting MCP inspector...
⚙️ Proxy server listening on port 6277
🔍 MCP Inspector is up and running at http://127.0.0.1:6274 🚀

Connect => List Tools => say_hello => Run Toolで実行

成功!

5.Claude Desktopにつなぐ

claude_desktop_config.jsonに下記を入力

とりあえず機能名を入力すると疎通はした

使われるかどうかは、Tool 定義(name / description / inputSchema など)を読んだ LLM が、推論過程で tool-call メッセージを生成するかどうかで決まる。ということらしいが、多分この定義だと使いどころが無いのか直接呼びだす以外起動しなかった。

おわり

まとめ

手放しでLLMにぶん投げるのはわりと迷子になるけど

  1. LLMで概要把握
  2. ドキュメント等1次ソースをざっくり攫う
  3. LLMで情報補完
  4. 手を動かす

みたいなサイクルである程度固めるステップを挟むと
程よく手が抜けて楽

株式会社ソニックムーブ

Discussion