冷凍食品のピザのイラスト

TypeScript で MCP サーバーを実装し、Claude Desktop から利用する

MCP(Model Context Protocol)とはアプリケーションが LLM にコンテキストを提供する方法を標準化するプロトコルです。MCP を使用することで、LLM は外部ツールやサービスからコンテキストを取得するだけでなく、コードの実行やデータの保存など、さまざまなアクションを実行できるようになります。この記事では MCP サーバーを TypeScript で実装する方法を紹介します。

MCP(Model Context Protocol)とはアプリケーションが LLM にコンテキストを提供する方法を標準化するプロトコルです。多くの LLM ではユーザーに適切な回答を提供するために追加のコンテキスト情報を必要とします。例えば、今日の天気の情報をユーザーから求められたとしても LLM が学習したデータにはその情報は含まれていないため、正確な回答ができません。このような状況では LLM は天気情報を取得する API の呼び出しを要求し、その結果をコンテキストとして提供することで正確な回答を得られるようになります。

外部からコンテキストを渡す手段として Function Calling を思い出した方も多いかもしれません。Function Calling は天気や株価を取得するだけのような単純な API 呼び出しを行う場合には十分であると言えます。しかし Function Calling の実装は LLM ごとに異なるため、スケーラビリティの制約があります。

MCP は標準化された方法でツールを呼び出すことができるため、複数のツールを組み合わせて複雑なワークフローを構築することが容易になります。より詳細な MCP と Function Calling の違いについては以下の Reddit スレッドを参照してください。

https://www.reddit.com/r/ClaudeAI/comments/1h0w1z6/model_context_protocol_vs_function_calling_whats/

LLM は MCP を通じて以下のことが可能になります。

  • 外部ツールやサービスからコンテキストを取得する
  • コードを実行する
  • データを保存・読み込みする
  • 外部 API と連携する

これにより、LLM は単なる質問応答システムから、実世界のタスクを実行できるアプリケーションやサービスへと進化します。

現在、Google や GitHub, Slack などの多くのサービスが MCP 仕様に準拠したサーバーを提供しています。例えば Google Calendar の MCP サーバーを利用すれば、旅行の計画を立てる際に既存の予定を考慮した計画を提案し、さらにその新しい予定を直接 Google Calendar に登録することも可能になるでしょう。

利用可能な MCP サーバーの一覧は MCP マーケットプレイスmodelcontextprotocol/servers: Model Context Protocol Servers で確認できます。実に多くの MCP サーバーが公開されており、盛り上がりを見せていることがわかるでしょう。

この記事では、まず既存の MCP サーバーを Claude Desktop から利用する方法を解説し、その後で独自の MCP サーバーを TypeScript で実装する手順を紹介します。

ホストから MCP サーバーを利用する

MCP サーバーを利用する前に、MCP のアーキテクチャについて理解しておきましょう。MCP は次の 3 つの主要コンポーネントで構成されています。ホストは複数のサーバーに接続できるクライアントサーバーアーキテクチャに従います。

  • ホスト:ユーザーが操作する LLM アプリケーション(Claude Desktop や Cline など)
  • MCP クライアント:ホストアプリケーション内でサーバーとの 1 対 1 の接続を確立するコンポーネント
  • MCP サーバー:クライアントにコンテキスト、ツール、プロンプトを提供するサービス

出典: modelcontextprotocol.io/docs/concepts/architecture

Claude Desktop をインストールする

この記事ではホストとして Claude Desktop を使用します。以下の URL から Claude Desktop をインストールできます。

https://claude.ai/download

お使いの OS に応じたバージョンを選択してください(Linux は現在サポートされていません)。すでに Claude Desktop をインストールしている場合は、最新バージョンであることを確認してください。

MCP サーバーを追加する

MCP サーバーを利用するためには、Claude Desktop の設定を編集して MCP サーバーを追加する必要があります。

Note

この記事では macOS の Claude Desktop を使用しています。Windows バージョンでは手順が異なる場合があります。

手順は以下の通りです。

  1. Claude Desktop を起動し、メニューバーの「Claude」→「Settings...」を選択
  2. 左側のメニューから「Developer」を選択
  3. 「Edit Config」ボタンをクリック
  4. エクスプローラーが開くので claude_desktop_config.json ファイルをテキストエディタで開く

今回は GitHub の MCP サーバー を追加してみましょう。このサーバーを利用すると、Claude が GitHub リポジトリのファイル操作やイシュー作成などを行ったり、コードを検索した結果を元に質問に答えたりできます。

設定ファイルの mcpServers キーに GitHub サーバーの設定を追加します。

{
  "mcpServers": {
    "github": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "GITHUB_PERSONAL_ACCESS_TOKEN",
        "mcp/github"
      ],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
      }
    }
  }
}

Note

GitHub の MCP サーバーを利用するには、アクセスしたいリポジトリの権限を持つ Personal Access Token を事前に作成し、<YOUR_TOKEN> の部分に置き換えてください。また、Docker Desktop がインストールされていることも確認してください。

設定を保存したら Claude Desktop を再起動します。正常に MCP サーバーが追加されると、設定画面の「Developer」メニューに「github」が表示されます。

MCP サーバーを利用する

MCP サーバーを追加すると、Claude Desktop がそのサーバーが提供するツールを利用できるようになります。チャット画面の🔨アイコンを見ると 17 個のツールが利用可能であると表示されています。

🔨アイコンをクリックするとツールの一覧が表示されます。Issue にコメントをする「add_issue_comment」やブランチを作成する「create_branch」などが表示されています。

実際に Claude に質問をしてみましょう。以下のような指示を与えてみます。

「sapper-blog-app(このブログのソースコードが含まれているリポジトリです)に古いライブラリがあるか調べてください。存在する場合には Issue を作成してください」

Claude は質問を回答するためにツールが必要であると判断した場合、ユーザーにツールの使用を許可するように求めてきます。この場合、まず「search_repository」ツールを使用して「sapper-blog-app」リポジトリを検索しようとしています。

「Allow Once」をクリックすると、Claude はリポジトリ構造を分析し、モノレポであることを理解した上で各パッケージの package.jsonget_file_contents ツールで取得します。

分析完了後、create_issue ツールを使用して古いライブラリが見つかったことを報告する Issue を自動作成しました。

TypeScript で独自の MCP サーバーを実装する

MCP の仕組みをより深く理解するため、独自の MCP サーバーを TypeScript で実装してみましょう。今回はシンプルな例として「サイコロを振る」機能を提供する MCP サーバーを作成します。MCP サーバーはホストからの何面のサイコロを振るかというリクエストを受け取り、ランダムにサイコロの目を生成して返すというシンプルなものです。

MCP サーバーの基本概念

MCP サーバーは主に以下の 3 種類の機能を提供できます。

  1. リソース:MCP サーバーがクライアントに提供するデータ(ファイル内容、データベースレコードなど)

    • 各リソースは file:///path/to/file.txtpostgres://database/table などの URI で識別される
  2. ツール:外部システムとのインタラクションを可能にするアクション(ファイルの操作、計算の実行など)

  3. プロンプト:特定のタスク実行のためのプロンプト(コードレビューの方法など)

MCP サーバーは上記の機能を name, description, arguments などのプロパティを持つ JSON オブジェクトとして定義します。MCP クライアントはこれらの情報を見て提供されている機能が何であるかを理解し、タスクを実行するために利用すべきかどうかを判断します。例えばツールの場合には以下のような構造を持ちます。

{
  name: string;          // ツールごとに一意の名前
  description?: string;  // 人間が読める説明
  inputSchema: {         // ツールのパラメータを JSON スキーマで定義
    type: "object",
    properties: { ... }
  }
}

今回実装するサイコロツールは、サイド数(面の数)を入力として受け取り、1 からその数字までのランダムな整数を返す単純なツールです。完成すれば以下のような機能定義を持つことになります。

{
  "name": "getDiceRoll",
  "description": "Roll a dice with a specified number of sides and return the result.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "sides": {
        "type": "integer",
        "description": "The number of sides on the dice."
      }
    }
  }
}

プロジェクトのセットアップ

まずは新しいプロジェクトを作成し、必要なパッケージをインストールします。

mkdir mcp-dice-roller
cd mcp-dice-roller
 
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript vitest

@modelcontextprotocol/sdk は MCP サーバーを TypeScript で実装するための SDK です。zod はスキーマバリデーションライブラリで、MCP サーバーのスキーマを定義するために使用します。

package.json を次のように編集します。

package.json
{
  "name": "mcp-dice-roller",
  "version": "1.0.0",
  "main": "src/server.ts",
  "type": "module",
  "bin": {
    "diceRoller": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js",
    "test": "vitest"
  },
  "files": [
    "build"
  ]
}

最後にプロジェクトのルートに tsconfig.json を作成します。

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

サイコロツールの実装

はじめに src/index.ts を作成し、MCP サーバーを初期化します。以下のように McpServer クラスをインポートし、nameversion を指定してインスタンスを作成します。

src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 
// サーバーインスタンスの作成
export const server = new McpServer({
  name: "DiceRoller",
  version: "0.1.0",
});

server.tool() メソッドを使用してツールを定義します。

src/index.ts
import { z } from "zod";
server.tool(
  "getDiceRoll", // ツールの名前
  "Roll a dice with a specified number of sides and return the result.",  // ツールの説明
  // ツールの引数を定義するスキーマ
  { sides: z.number().min(1).describe("Number of sides on the die") },
// ツールが呼び出されたときに実行される関数
  async ({ sides }) => {
    // 1から指定された面数までのランダムな整数を生成
    const roll = Math.floor(Math.random() * sides) + 1;
    
    return {
      content: [
        {
          type: "text",
          text: roll.toString(),
        },
      ],
    };
  }
);

server.tool() メソッドの第 1 引数にはツールの名前を指定します。第 2 引数にはツールの説明と第 3 引数の入力スキーマは省略可能です。

入力スキーマは zod を使用して定義します。zod は TypeScript の型をスキーマとして利用できるため、型安全にスキーマを定義できます。ここでは slides が 1 以上の整数であることを検証するスキーマを定義しています。.describe() メソッドでスキーマの説明を追加することで、ホストアプリケーションがツールの利用方法を理解するのに役立ちます。

ツールが呼び出されたときに実行される関数は非同期関数として定義します。引数にはスキーマで定義した値が渡されます。戻り値は content プロパティを持つオブジェクトで、ツールの実行結果を返します。

ツールの呼び出しをテストする

サーバーを起動して呼び出してみる前に、ローカルでテストをしてみましょう。InMemoryTransport を使用することでメモリ上でクライアントとサーバーを接続できます。以下のコードを src/index.test.ts に追加します。

src/index.test.ts
import { describe, it, expect } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { server } from "./index.js";
 
describe("getDiceRoll", () => {
  it("ランダムにサイコロを振った結果を返す", async () => {
    // テスト用クライアントの作成
    const client = new Client({
      name: "test client",
      version: "0.1.0",
    });
 
    // インメモリ通信チャネルの作成
    const [clientTransport, serverTransport] =
      InMemoryTransport.createLinkedPair();
      
    // クライアントとサーバーを接続
    await Promise.all([
      client.connect(clientTransport),
      server.connect(serverTransport),
    ]);
 
    // 6面サイコロを振る
    const result = await client.callTool({
      name: "getDiceRoll",
      arguments: {
        sides: 6,
      },
    });
 
    // 結果が1-6の範囲の数字であることを確認
    expect(result).toEqual({
      content: [
        {
          type: "text",
          text: expect.stringMatching(/^[1-6]$/),
        },
      ],
    });
  });
});

npm run test でテストを実行しましょう:

npm run test
 
 src/index.test.ts (1 test) 3ms
 getDiceRoll > ランダムにサイコロを振った結果を返す
 
 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  11:47:24
   Duration  356ms (transform 68ms, setup 0ms, collect 83ms, tests 3ms, environment 0ms, prepare 43ms)

テストが成功しました。これでサイコロを振るツールが正常に動作することが確認できました。

サーバーを起動する

最後にサーバーを起動するために main() 関数を定義します。stdio Transport は、標準の入力および出力ストリームを介した通信を可能にします。

src/index.ts
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  // 標準出力をするとサーバーのレスポンスとして解釈されてしまうので、標準エラー出力に出力する
  console.error("MCP Server running on stdio");
}
 
main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

npm run build でビルドを実行し、./build/index.js を実行できるか確認します。

npm run build
 
node ./build/index.js

MCP Server running on stdio と表示されればサーバーが正常にサーバーが起動しています。サーバーが起動することが確認できたら、タスクを終させても大丈夫です。

MCP サーバーをクライアントから呼び出す

それでは自作した diceRoller サーバーを Claude Desktop から呼び出してみましょう。GitHub の MCP サーバーを追加したときと同様に、claude_desktop_config.json に以下の JSON を追加します。

{
  "mcpServers": {
    "diceRoller": {
      "command": "node",
      "args": [
        "/absolute/path/to/your/mcp-dice-roller/build/index.js"
      ]
    }
  }
}

Warning

NVM や Volta などの Node.js バージョン管理ツールを使用している場合には [error] spawn node ENOENT というエラーが表示されるようです。この場合には commandnode をバージョン管理ツールのフルパスに置き換えることで解決できる可能性があります。volta の場合には /Users/username/.volta/bin/node です。https://github.com/modelcontextprotocol/servers/issues/64

Claude Desktop を再起動し、MCP サーバーが追加されていることを確認します。🔨アイコンをクリックして「getDiceRoll」が追加されているはずです。

サイコロを振るツールを実行してみましょう。「1 ~ 10 のうちランダムな 1 つの数字を返してください」と尋ねてみます。すると「getDiceRoll」ツールが実行が求められました。入力スキーマを理解しており、{"sides": 10} という引数で実行を試みていることがわかります。ツールの使用を許可すると、サーバーからは「8」という結果が返ってきました。この結果を元に Claude は「8」を選択したようです。

まとめ

  • MCP(Model Context Protocol)はアプリケーションが LLM にコンテキストを提供する方法を標準化するプロトコル
  • MCP を使用することで、LLM は外部ツールやサービスからコンテキストを取得するだけでなく、コードの実行やデータの保存など、さまざまなアクションを実行できる
  • MCP はホスト、クライアント、サーバーの 3 つのコンポーネントから構成される
  • MCP サーバーはリソース、ツール、プロンプトを提供する。クライアントはこれらの情報を元にタスクを実行するために利用すべきかどうかを判断する
  • Claude Desktop を使用して MCP サーバーを利用するには claude_desktop_config.jsonmcpServers キーを追加する
  • ツールは Claude が回答を生成するために必要だと判断した場合に実行される

参考

記事の理解度チェック

以下の問題に答えて、記事の理解を深めましょう。

MCP(Model Context Protocol)の主な目的は何ですか?

  • LLMの学習データを標準化すること

    もう一度考えてみましょう

  • アプリケーションがLLMにコンテキストを提供する方法を標準化すること

    正解!

    MCPは、アプリケーションがLLMにコンテキストを提供する方法を標準化するプロトコルです。

  • AIモデルのトレーニング方法を標準化すること

    もう一度考えてみましょう

  • 複数のLLMモデルを同時に実行する方法を提供すること

    もう一度考えてみましょう

MCPサーバーのツールを実装する際、関数の戻り値として必要なプロパティは?

  • content

    正解!

    ツールの実行結果は、contentプロパティを持つオブジェクトとして返す必要があります。

  • result

    もう一度考えてみましょう

  • output

    もう一度考えてみましょう

  • response

    もう一度考えてみましょう