🐱

MCP入門

に公開

本記事は、最近話題のMCPの入門記事です。
MCP(Model Context Protocol)について、以下の4ステップで紹介します。

  1. ざっくり理解する
  2. 使ってみる
  3. 深く理解する
  4. 作ってみる

初心者でも順番に読み進めれば、MCPについてざっと理解、かんたんな実装ができるようになることを目指します💪


ざっくり理解する

MCPとは、ざっくり言うと、LLMアプリと外部サービスを連携するための統一されたインターフェース(プロトコル)です。
1
LLMアプリとは、ChatGPTやClaude、Cursorなど、LLMを使用するためアプリケーションを指します。(⚠️ GPT-4oclaude-3-5-sonnetなどのLLM自体とは区別してください。)

初期のLLMアプリは、どこまでいってもすごく賢いチャットツールでしかなく、結局はテキストを返答することしかできませんでした。

そのため、LLMアプリの返答に対して何をするかは人間が判断する必要がありました。

3

この問題を最初に解決したのが、OpenAIのFunction Callingです。

Function Callingを使用することで、LLMにどのようなアクションを行うか判断させられると共に、指定したJSONレスポンスを使って、LLMアプリが直接アクションを実行できるようになりました。

4

しかし、LLMアプリ側が無数にある外部サービスに対して、いちいち連携を実装するには限界がありますし、新しいサービスが出てくる度に追加の対応が必要になってしまいます。

そこで、登場したのがLLMアプリと外部サービスを連携するための統一されたインターフェース(プロトコル)、MCPです。
外部サービスのプロバイダー(もしくは第三者)が、MCPサーバー(後で詳しく説明します)を提供することで、各LLMアプリは、何もせずとも統一されたインターフェースで外部サービスを利用できるようになるわけです👍


使ってみる

習うより、慣れろ!まずはMCPを使ってみましょう ✨

ここでは、公式ドキュメントと同様、Filesystem MCP Serverを試してみましょう。(※「MCPサーバー」など初めて出てくる用語もありますが、あとで丁寧に説明します。今はなんとなく読み進めてOKです!)

Claudeのデスクトップアプリが必要になるので、ない方はインストールをお願いします 🙏🏼

MCPサーバーの登録

まず、Filesystem MCP ServerをClaudeに登録します。

  1. アプリのメニューバー Claude > 設定 を開く

  2. はじめるを選択。
    6

    以下のパスで空の設定ファイルが作成されます。
    • macOSの場合: ~/Library/Application\ Support/Claude/claude_desktop_config.json
    • Windowsの場合: %APPDATA%\Claude\claude_desktop_config.json

  3. 作成された設定ファイルに以下のjsonを記述します。{username} はご自身のPC上のユーザー名に書き換えてください。

    {
      "mcpServers": {
        "filesystem": {
          "command": "npx",
          "args": [
            "-y",
            "@modelcontextprotocol/server-filesystem",
            "/Users/{username}/Downloads" // TODO: usernameを書き換えてください。
          ]
        }
      }
    }
    
  4. これで準備完了です✅ Claudeを再起動してみましょう。うまくいけば、🔨のアイコンがが表示され、利用できる機能群が表示されます。

    7
    7-2

  5. 動作の確認をしてみましょう!「最近ダウンロードされたファイルを表示して」と聞いてみました。するとコマンドの使用許可のモーダルが表示されるので、許可しちゃいます。

    8

    無事、ローカルのPCからダウンロードされたファイルを教えてくれました 👏🏼
    9


より深く理解する

実際に使ってみて、イメージが湧いたところで、MCPの仕組みをより深掘りしてみましょう。

コンポーネント

MCPでは、主に5つのコンポーネントが出てきます。

用語 説明
LLMアプリ ChatGPTやClaude、Cursorなど、LLMを使用するためのインターフェースを提供するアプリケーションを指します。LLM自体とは区別してください。公式ドキュメントでは、LLMホストと呼ばれています。
LLM LLMアプリが利用するLLMモデル。多くの場合は、API経由でリモートのものを利用します。
MCPクライアント MCPサーバーとMCPに従って通信するクライアント。LLMアプリ内に、MCPサーバーごとに1:1で複数存在します。
MCPサーバー MCPクライアントと外部サービスの橋渡しを行う軽量サーバー。LLMアプリのためのBFFと考えるとわかりやすい。サーバーという名前だが、LLMアプリと同じ、ローカルマシン上に立てられることに注意してください。各サービスはMCPに従って、MCPサーバーを実装することでLLMアプリとの連携を可能にします。
外部サービス PC上のファイルシステム、Gooleカレンダーの予定、Github、食べログ、Slackなど様々なサービスを指します。外部サービスは、ファイルシステムなどのローカルに存在する場合と、GithubやSlackなどリモートに存在する場合があります。

10
MCPとはより正確にはMCPクライアントとMCPサーバーの通信方式を定めたプロトコルなのです。

MCPの動作の流れ

これらのコンポーネントが、どのように連携して、アクションが実行されるのか、先ほどのファイルシステム MCPサーバーを例に整理します。

  1. LLMアプリ起動時に、ファイルシステム専用のMCPクライアントとファイルシステム専用のMCPサーバーの接続の初期化が行われます。この時MCPサーバーはプロトコルのバージョンと、機能をMCPクライアントに返答します。

    より詳しくはこちら
    1. 初期化リクエスト

      LLMアプリ起動時に、MCPクライアントは対応するMCPサーバーに初期化リクエストを送信。

    2. サーバーのプロトコルバージョンと機能で応答

      MCPサーバーはプロトコルバージョンと、利用できるアクションの一覧を応答

    3. 確認として初期化完了通知を送信

      MCPクライアントは、初期化完了通知をMCPサーバーに送信

    4. 接続完了

  2. LLMアプリはプロンプトをLLMに送ります。このとき、MCPサーバーの情報も一緒に送って、ファイルシステムが利用できることをMCPに伝えます。

  3. LLMはファイル一覧取得を使うべきと判断、利用指示(Function Calling)で返答します。

  4. LLMアプリは、内部でMCPクライアントを使って、MCPサーバーにアクションをリクエストします。

  5. MCPサーバーはファイルシステムに対して、一覧取得のリクエストを送ります。

  6. ファイルシステムは結果を返します。

  7. MCPサーバーは結果をMCPクライアントに返却します。

  8. LLMアプリは、LLMクライアントから受けっとった結果を、再度LLMに結果を投げます

  9. LLMは実行結果を見て、よしなに成形。LLMアプリに返します。(あるいはここで、さらにツールの利用指示を出します。)

以上です。全体の流れを抽象化して、1枚の図にまとめると以下のようになります。

外部サービスがリモートにある場合でも、仕組みは全く同じです。
この場合でも、MCPサーバーはローカルに立つことに注意が必要です⚠️
リモートの場合は4. アクション実行がAPIを叩くのがほとんどになるかと思います。


作ってみる

大まかな流れがわかったところで、いよいよMCPサーバー/クライアントを自作してみましょう。
完成したコードはこちらに置いてあります。
https://github.com/WombatTechnology/cat-image-mcp

MCPサーバーを作る

猫も杓子もMCPということで、猫の画像を返すMCPサーバーを実装してみます。
デフォルトだと、Claudeは猫の画像を要求しても味のあるイラストしか返してくれません。

猫の画像の取得にはTheCatAPIを使用しました。
事前にAPI Keyを取得しておいてください。

  1. レポジトリを準備します。

    # プロジェクト用の新しいディレクトリを作成
    mkdir cat_image
    cd cat_image
    
    # 新しいnpmプロジェクトを初期化
    npm init -y
    
    # 依存関係をインストール
    npm install @modelcontextprotocol/sdk zod
    npm install -D @types/node typescript
    
    # ファイルを作成
    mkdir src
    touch src/index.ts
    
  2. 以下のようなコードを書きます

    index.ts
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { fetchCatImage } from "./fetchCatImage.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    
    // Create server instance
    const server = new McpServer({
      name: "cat-image",
      version: "1.0.0",
    });
    
    server.tool("get-cat-image", "Get a random cat image",
      async () => {
        const data = await fetchCatImage();
        return {
          content: [
            {
              type: "image",
              data,
              mimeType: "image/jpeg",
            },
          ],
        };
      });
    
    async function main() {
      const transport = new StdioServerTransport();
      await server.connect(transport);
      console.error("Cat Image MCP Server running on stdio");
    }
    
    main().catch((error) => {
      console.error("Fatal error in main():", error);
      process.exit(1);
    });
    
    fetchCatImage.ts
     export const fetchCatImage = async (): Promise<string> => {
       const API_KEY = process.env.CAT_API_KEY;
       if (!API_KEY) throw new Error("CAT_API_KEY environment variable is not set");
       const response = await fetch(`https://api.thecatapi.com/v1/images/search?limit=1&api_key=${API_KEY}`);
       const data = await response.json();
       const imageResponse = await fetch(data[0].url);
       const arrayBuffer = await imageResponse.arrayBuffer();
       const base64 = Buffer.from(arrayBuffer).toString('base64');
       return base64
     }
    

    いくつかポイントをご紹介します。

    まず、最初にMcpServerのインタンスを作成します。

    // サーバーのインスタンスを作成
    const server = new McpServer({
      name: "cat-image",
      version: "1.0.0",
    });
    

    次に、サーバーに機能を定義します。今回はサーバーで何らかの処理を行うToolsを定義します。

    server.tool(
    	"get-cat-image", // 名前
    	"Get a random cat image", // 説明文
    	// 引数の定義
      {
        limit: z.number().optional().default(1),
      },
      async ({ limit }) => {
        const data = await fetchCatImage(limit);
        return {
          content: [
            {
              type: "image",
              data,
              mimeType: "image/jpeg",
            },
          ],
        };
      });
    

    最後に起動処理を書いて、完了です 👏🏼

    async function main() {
    	// 接続処理
      const transport = new StdioServerTransport();
      await server.connect(transport);
      console.error("Cat Image MCP Server running on stdio");
    }
    
    main().catch((error) => {
      console.error("Fatal error in main():", error);
      process.exit(1);
    });
    

動作確認

実装できたので、動作を確認してみましょう。

  1. まず、実装したサーバーをビルドします。
 $ tsc && chmod 755 build/index.js
  1. 続いて、cat_imageclaude_desktop_config.json に登録します。{path/to} の部分は適宜、お使いの環境ものに修正してください。
{
  "mcpServers": {
+    "cat_image": {
+       "command": "{path/to}/bin/node",
+       "args": ["{path/to}/build/index.js"],
+	    "env": {
+	       "CAT_API_KEY": "{取得したAPIKeyをここに}"
+	    }
+    }
  }
}
  1. Claudeを再起動してみましょう。🔨を押して、利用可能なMCPツールに追加されていればOKです。
    a
    b

  2. 「猫の画像をちょうだい」と言ってみましょう。見事Claudeが猫の写真を返してくれるようになりました🐈❤️
    c

MCPクライアントを作る

最後に簡単なMCPクライアントを実装してみましょう。

  1. まずはレポジトリを準備します。

    mkdir mcp-client-typescript
    cd mcp-client-typescript
    
    npm init -y
    npm install @anthropic-ai/sdk @modelcontextprotocol/sdk dotenv
    npm install -D @types/node typescript
    
    touch index.ts
    
  2. package.jsonを修正

    {
      "type": "module",
      "scripts": {
        "build": "tsc && chmod 755 build/index.js"
      }
    }
    
  3. tsconfigを設定

    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "Node16",
        "moduleResolution": "Node16",
        "outDir": "./build",
        "rootDir": "./",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
      },
      "include": ["index.ts"],
      "exclude": ["node_modules"]
    }
    
  4. API経由でANTHOROPICのAPIを使用するので、API Keyを環境変数に設定します。

    $ echo "ANTHROPIC_API_KEY=<your key here>" > .env
    $ echo ".env" >> .gitignore
    
  5. クライアントを実装します

    index.ts
    index.ts
    import { Anthropic } from "@anthropic-ai/sdk";
    import {
      MessageParam,
      Tool,
    } from "@anthropic-ai/sdk/resources/messages/messages.mjs";
    import { Client } from "@modelcontextprotocol/sdk/client/index.js";
    import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
    import readline from "readline/promises";
    import dotenv from "dotenv";
    import terminalImage from "terminal-image";
    
    dotenv.config();
    
    const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
    if (!ANTHROPIC_API_KEY) {
      throw new Error("ANTHROPIC_API_KEY is not set");
    }
    
    class MCPClient {
      private mcp: Client
      private anthoripic: Anthropic
      private transport: StdioClientTransport | null = null
      private tools: Tool[] = []
    
      constructor() {
        this.anthoripic = new Anthropic({
          apiKey: ANTHROPIC_API_KEY,
        })
        this.mcp = new Client({
          name: "mcp-client",
          version: "0.1.0",
        })
      }
    
      async connectToServer(serverScriptPath: string) {
        try {
          this.transport = new StdioClientTransport({
            command: process.execPath,
            args: [serverScriptPath],
          })
          this.mcp.connect(this.transport)
    
          const toolsResult = await this.mcp.listTools()
          this.tools = toolsResult.tools.map((tool) => ({
            name: tool.name,
            description: tool.description,
            input_schema: tool.inputSchema,
          }))
    
        } catch (error) {
          console.log("Failed to connect to MCP server: ", error);
          throw error
        }
      }
    
      async processQuery(query: string) {
        const messages: MessageParam[] = [
          {
            role: "user",
            content: query,
          },
        ];
    
        const response = await this.anthoripic.messages.create({
          model: "claude-3-5-sonnet-20240620",
          max_tokens: 1000,
          messages,
          tools: this.tools
        })
    
        const finalText: string[] = []
    
        for (const content of response.content) {
          if (content.type === "text") {
            // 返答がテキストの場合
            finalText.push(content.text)
          } else if (content.type === "tool_use") {
            // ツールを実行
            const toolName = content.name
            const toolArgs = content.input as { [x: string]: unknown } | undefined;
            console.log("【ツールの実行】", toolName, toolArgs)
            const result = await this.mcp.callTool({
              name: toolName,
              arguments: toolArgs,
            }) as { content: { type: string; data: string }[] }
            if (result.content) {
              const imageData = result.content[0].data;
              const buffer = Buffer.from(imageData, 'base64');
              console.log(await terminalImage.buffer(buffer));
            }
          }
        }
        return finalText.join("\n")
      }
    
      async chatLoop() {
        const rl = readline.createInterface({
          input: process.stdin,
          output: process.stdout,
        })
    
        try {
          console.log("Welcome to MCP Client! Type 'exit' to quit.")
          while (true) {
            const query = await rl.question("You: ")
            if (query.toLowerCase() === "quit") {
              break
            }
    
            const response = await this.processQuery(query)
            console.log("MCP: ", response)
          }
        } catch (error) {
          console.error("Error: ", error)
        } finally {
          rl.close()
        }
      }
    
      async cleanup() {
        await this.mcp.close()
      }
    }
    
    async function main() {
      if (process.argv.length < 3) {
        console.log("Usage: node index.ts <path_to_server_script>");
        return;
      }
      const mcpClient = new MCPClient();
      try {
        await mcpClient.connectToServer(process.argv[2]);
        await mcpClient.chatLoop();
      } finally {
        await mcpClient.cleanup();
        process.exit(0);
      }
    }
    
    main()
    

    こちらもいくつか重要なポイントをご紹介します。
    まず、connectToServer メソッドで、サーバーとの初期化接続処理を行います。
    この時、利用できるツールの一覧を取得しています。

    async connectToServer(serverScriptPath: string) {
      try {
        // 接続
        this.transport = new StdioClientTransport({
          command: process.execPath,
          args: [serverScriptPath],
        })
        this.mcp.connect(this.transport)
    
    		// ツールの一覧を取得
        const toolsResult = await this.mcp.listTools()
        this.tools = toolsResult.tools.map((tool) => ({
          name: tool.name,
          description: tool.description,
          input_schema: tool.inputSchema,
        }))
    
      } catch (error) {
        console.log("Failed to connect to MCP server: ", error);
        throw error
      }
    }
    

    processQuery メソッドでは、LLMを通じたツールの呼び出しを行っています。
    返信がtool_useの場合は、レスポンスから画像を取得して表示しています。

      async processQuery(query: string) {
        const messages: MessageParam[] = [
          {
            role: "user",
            content: query,
          },
        ];
    
        // LLMにツールと一緒にメッセージを送る
        const response = await this.anthoripic.messages.create({
          model: "claude-3-5-sonnet-20240620",
          max_tokens: 1000,
          messages,
          tools: this.tools
        })
    
        const finalText: string[] = []
    
        for (const content of response.content) {
          if (content.type === "text") {
            // 返答がテキストの場合
            finalText.push(content.text)
          } else if (content.type === "tool_use") {
            // ツールを実行
            const toolName = content.name
            const toolArgs = content.input as { [x: string]: unknown } | undefined;
    
            const result = await this.mcp.callTool({
              name: toolName,
              arguments: toolArgs,
            }) as { content: { type: string; data: string }[] }
    
            if (result.content) {
              // 画像データを表示
              const imageData = result.content[0].data;
              const buffer = Buffer.from(imageData, 'base64');
              console.log(await terminalImage.buffer(buffer));
            }
          }
        }
        return finalText.join("\n")
      }
    

動作確認

では、こちらも動作確認してみましょう!
まずビルドします。

$ npm run build

クライアントを起動します。この時第一引数にサーバーのパスを指定してください。

$ node build/index.js /path/to/server/build/index.js    

画質が荒いですが、見事猫の画像を表示できました 👏🏼


まとめ

今後、MCPによって、LLMアプリは、チャットツールから何でもできるスーパーアプリへと進化していく可能性が高そうです。次は、あなたのサービスやAPIをMCPでつないでみましょう!

より詳しく知りたい方は、今回参考にした公式ドキュメントもご覧ください。
https://www.anthropic.com/news/model-context-protocol

Discussion