Vercel AI SDK で MCP を試す 〜その2〜 ：公式サンプルを書きかえて使う

## はじめに

ちょっと前に、主に公式ドキュメントを見ながら実装を試した Vercel AI SDK ＋ MCP の話です。

●Vercel AI SDK でシンプルな MCPクライアント・MCPホストの機能を作る（自作MCPサーバーと組み合わせる） - Qiita
　https://qiita.com/youtoy/items/22fa3faaacd7ae6dd958


### 今回のお試し

今回は以下の公式サンプルをベースとして利用し、内容を少し書きかえたりなどして使います。

●ai/examples/mcp/src/stdio at main · vercel/ai
　https://github.com/vercel/ai/tree/main/examples/mcp/src/stdio

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/9e096f00-8bb4-4686-be1b-3ea69c49a79f.png" width=65%>



## 実際に試してみる

公式の情報を見ると、[pnpm](https://github.com/pnpm/pnpm) を使った手順で試せるようです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/5398fc17-66d7-4485-9b38-66ae053fa0a4.png" width=50%>

今回は上記の pnpm は使わずにやってみました。


### 必要なパッケージなどを確認

公式サンプルのコードを流用して進めていくのですが、まずは必要なパッケージ・環境などを確認してみることにします。


#### コードの冒頭を見てみる

コードの冒頭を見て、必要なパッケージ・環境などを確認します。以下は、「client.ts」と「server.ts」のリンクと、冒頭部分のキャプチャです。

https://github.com/vercel/ai/blob/main/examples/mcp/src/stdio/client.ts

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/d406803c-4a59-48dd-8a3c-afc1ad3bc265.png" width=80%>


https://github.com/vercel/ai/blob/main/examples/mcp/src/stdio/server.ts

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/957b849f-f88d-470f-8fc4-807b84c7fb2a.png" width=80%>


上記を見ると、以下をインストールすれば良さそうです。また OpenAI の APIキーをセットする必要もありそうです。

- @ai-sdk/openai
- @modelcontextprotocol/sdk
- ai


それと dotenv がインポートされていますが、ざっと見た感じ、OpenAI の APIキーを OPENAI_API_KEY という名前でセットしておく形で良さそうです。


#### パッケージのインストール

必要なパッケージを、以下でインストールします。

```zsh
npm i @ai-sdk/openai @modelcontextprotocol/sdk ai
```

この後は、先ほどの公式サンプルを書きかえていきます。


### コードを書きかえる

元の内容は TypeScript でしたが、今回 JavaScript にして試してみました。変更後の実装内容は、この後に示します。なお、この後に出てくるサーバー側・クライアント側のファイル（server.mjs、client.mjs）は、同じフォルダ内に置く構成にしています。


#### サーバー側

以下はサーバー側です。こちらは、基本的に元のものを TypeScript から JavaScript にしただけという内容です。

```javascript: server.mjs
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const POKE_API_BASE = "https://pokeapi.co/api/v2";

const server = new McpServer({
  name: "pokemon",
  version: "1.0.0",
});

server.tool(
  "get-pokemon",
  "Get Pokemon details by name",
  { name: z.string() },
  async ({ name }) => {
    const path = `/pokemon/${name.toLowerCase()}`;
    const pokemon = await makePokeApiRequest(path);

    if (!pokemon) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to retrieve Pokemon data",
          },
        ],
      };
    }

    return {
      content: [
        {
          type: "text",
          text: formatPokemonData(pokemon),
        },
      ],
    };
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.log("Pokemon MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

async function makePokeApiRequest(path) {
  try {
    const url = `${POKE_API_BASE}${path}`;
    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
    return await response.json();
  } catch (error) {
    console.error("[ERROR] Failed to make PokeAPI request:", error);
    return null;
  }
}

function formatPokemonData(pokemon) {
  return [
    `Name: ${pokemon.name}`,
    `Abilities: ${pokemon.abilities
      .map(({ ability }) => ability.name)
      .join(", ")}`,
  ].join("\n");
}
```


#### クライアント側

以下はサーバー側です。こちらは、元のものを TypeScript から JavaScript にした後、何ヵ所か書きかえたところがあります。
※ その書きかえを行った後に、「別途書きかえた部分・無効にした部分」をコメントアウトして残しています


```javascript
import { openai } from "@ai-sdk/openai";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { experimental_createMCPClient, generateText } from "ai";
// import "dotenv/config";
import { z } from "zod";

async function main() {
  let mcpClient;

  try {
    const stdioTransport = new StdioClientTransport({
      // command: "node",
      command: "【nodeコマンドのフルパス】",
      // args: ["src/stdio/dist/server.js"],
      args: ["./server.mjs"],
      // env: { FOO: "bar" },
    });

    mcpClient = await experimental_createMCPClient({
      transport: stdioTransport,
    });

    const { text: answer } = await generateText({
      model: openai("gpt-4o-mini", { structuredOutputs: true }),
      tools: await mcpClient.tools({
        schemas: {
          "get-pokemon": {
            parameters: z.object({ name: z.string() }),
          },
        },
      }),
      maxSteps: 10,
      onStepFinish: async ({ toolResults }) => {
        console.log(`STEP RESULTS: ${JSON.stringify(toolResults, null, 2)}`);
      },
      system: "You are an expert in Pokemon",
      prompt:
        // "Which Pokemon could best defeat Feebas? Choose one and share details about it.",
        "ヒンバスを最も効果的に倒せるポケモンはどれですか？1匹選んで、その詳細を教えてください。回答は日本語で。",
    });

    console.log(`FINAL ANSWER: ${answer}`);
  } finally {
    await mcpClient?.close();
  }
}

main().catch(console.error);
```


手を加えた部分は、不要そうな部分をコメントアウトしたのと、それ以外は以下の部分です。

- プロンプトを日本語に変更
- node コマンドのフルパスを記載
   - 【書きかえた理由】 `command: "node"` にしていた時、nodeコマンドが見つからないというエラーが出たため
- server.mjs のパスを変更
   - 【書きかえた背景】 client.mjs と server.js を同じフォルダに置く構成にしていたため


### コードの実行結果

上記のコードを実行します。実行するのはクライアント側で、実行した結果は以下のとおりです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/4368f617-0c03-49c1-9a06-3f03c8a1cad8.png)

最終的出力で、回答を得ることができました。さらに「ヒンバス ⇒ ギャラドス」と変えて、再度処理を実行してみました。

それで得られた結果は以下のとおりです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/7c6cbbfb-ea3a-4662-8dfa-53d0a881df38.png)

今回も最終出力の部分で、回答を得ることができました。


## おわりに

Vercel AI SDK で MCP を使う公式サンプルを見かけたので、それを試してみました。

本当はこの続きで、以下の「Filesystem MCP Server」をサーバー側にした内容を試そうとしたのですが、かなり苦戦してしまったので、いったんここまでの内容を公開しておきます。

●servers/src/filesystem at main · modelcontextprotocol/servers
　https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem