はじめに
AI委員会メンバー兼フロントエンドチーム EM のこん(@k0n_karin)です!今回はazukiazusa さんの記事で Vercel AI SDK を使った MCP クライアントの実装が紹介されていたので、Nuxt で同じようなものを実装してみました!
元記事は Next.js で実装されていましたが、普段 Nuxt を使っている身としては「Nuxt でもできるのかな?」と気になったので実際に試してみました。
Vercel AI SDK は@ai-sdk/vue
を提供しており、Vue でも使うことができます。
この記事を書くまで MCP は Cline などから使うものの、実際に MCP がどのように動いているのかを知らないまま雰囲気で使っていました。今回の記事を通して MCP の理解を深めることができ、MCP 開発のハードルを下げることができたと感じています。
実際に作ったプロジェクト構成
MCP サーバーも作りたいので Nuxt を使います。 プロジェクト全体の構成はこんな感じです。
mcp-client-nuxt/ ├── app.vue ├── components/ │ └── Chat.vue ├── server/ │ ├── api/ │ │ └── chat.post.ts │ ├── mcp/ │ │ ├── dice.ts │ │ └── index.ts │ └── routes/ │ └── mcp.ts ├── assets/ ├── nuxt.config.ts └── package.json
AI SDK に渡すモデルは 社内で使っている Azure Open AI の o3-mini を例に使います。モデルはなんでも OK です。
完成したコードは GitHub に置いてあります。
まずは AI とお話するところから作る
早速コードを書いていきましょう。スタイルは省略のため tailwindcss を使います。css の設定は省略するのでリポジトリをご参照下さい。
npm create nuxt@latest mcp-client-nuxt # Nuxt Modulesは何も入れなくてOKです npx nuxi module add tailwindcss npm i ai @ai-sdk/azure @ai-sdk/vue zod
components/Chat.vue
<script setup lang="ts"> import { useChat } from "@ai-sdk/vue"; const { messages, input, handleSubmit, status } = useChat(); </script> <template> <div class="flex flex-col w-full max-w-md py-24 mx-auto gap-4"> <h1 class="text-2xl font-bold">Chat with AI</h1> <div class="flex flex-col gap-4"> <div v-for="message in messages" :key="message.id" class="p-3 rounded-lg"> <div :class=" message.role === 'user' ? 'bg-blue-100 dark:bg-blue-900 ml-auto' : 'bg-gray-100 dark:bg-gray-800' " class="p-3 rounded-lg max-w-xs" > <div class="text-sm font-semibold mb-1"> {{ message.role === "user" ? "You" : "AI" }} </div> <div class="whitespace-pre-wrap"> {{ message.content }} </div> </div> </div> </div> <form @submit.prevent="handleSubmit" class="flex flex-col gap-2"> <input v-model="input" type="text" class="w-full p-3 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-800" placeholder="メッセージを入力..." :disabled="status !== 'ready'" /> <button type="submit" class="px-4 py-2 text-white bg-blue-500 rounded-lg hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed" :disabled="status !== 'ready'" > {{ status !== "ready" ? "送信中..." : "送信" }} </button> </form> </div> </template>
server/api/chat.post.ts
// 使うLLMによってパッケージが変わります。今回はAzure import { createAzure } from "@ai-sdk/azure"; import { streamText } from "ai"; export default defineEventHandler(async (event) => { // unjs/h3の関数でボディを取得 const { messages } = await readBody(event); const azure = createAzure({ apiKey: process.env.AZURE_OPENAI_API_KEY, resourceName: "kon-test", // Azureで作ったリソース構成の名前 }); const result = streamText({ model: azure("o3-mini"), // Azureでデプロイしたモデル名 messages, }); return result.toDataStreamResponse(); });
これだけで基本的なチャットアプリが完成します。npm run dev
で立ち上げてみましょう。
MCP サーバー・クライアントを作る
では次に MCP サーバー・クライアントを作っていきましょう。
MCP サーバーを作る
まずは MCP サーバーのエンドポイントから作ります。今回はサイコロを振る MCP サーバーを Streamable HTTP で作成します。
まずは MCP サーバー用のパッケージを追加します。
npm i @modelcontextprotocol/sdk
server/mcp/dice.ts
import { z } from "zod"; export const getDiceRoll = (sides: number) => { const roll = Math.floor(Math.random() * sides) + 1; console.log({ roll }); return roll; }; export const diceToolConfig = { name: "getDiceRoll", description: "Roll a dice with a specified number of sides and return the result.", inputSchema: { sides: z.number().min(1).describe("Number of sides on the die"), }, handler: ({ sides }: { sides: number }) => { const roll = getDiceRoll(sides); return { content: [ { type: "text" as const, text: roll.toString(), }, ], }; }, };
server/mcp/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { diceToolConfig } from "../mcp/dice"; export const mcpServer = new McpServer({ name: "example-server", version: "1.0.0", }); mcpServer.tool( diceToolConfig.name, diceToolConfig.description, diceToolConfig.inputSchema, diceToolConfig.handler );
次に MCP サーバーのクライアントとの通信部分を実装します。
server/routes/mcp.ts
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { mcpServer } from "../mcp"; export default defineEventHandler(async (event) => { const { res, req } = event.node; const body = await readBody(event); try { // Streamable HTTPトランスポートの初期化 const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // ステートレスにする }); // MCPサーバーと接続 await mcpServer.connect(transport); await transport.handleRequest(req, res, body); res.on("close", () => { transport.close(); mcpServer.close(); }); } catch (error) { console.error("Error handling MCP request:", error); if (!res.headersSent) { res.statusCode = 500; return { jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }; } } });
MCP クライアントを作る
次は MCP クライアント を作っていきます。こちらも Streamable HTTP で通信できるようにします。
server/api/chat.post.ts
import { createAzure } from "@ai-sdk/azure"; import { experimental_createMCPClient, streamText } from "ai"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; export default defineEventHandler(async (event) => { const { messages } = await readBody(event); const azure = createAzure({ apiKey: process.env.AZURE_OPENAI_API_KEY, resourceName: "kon-test", }); // MCPクライアントの接続先を設定して初期化 const url = new URL("http://localhost:3000/mcp"); const mcpClient = await experimental_createMCPClient({ transport: new StreamableHTTPClientTransport(url), }); // ツールの初期化 const tools = await mcpClient.tools(); const result = streamText({ model: azure("o3-mini"), messages, tools, // ツールの登録 onFinish: () => { mcpClient.close(); // 最後にクライアントへの接続を閉じる }, }); return result.toDataStreamResponse(); });
チャットの方も MCP サーバーのレスポンスの表示に対応させます。useChat
が受け取るメッセージの構造がしっかり構造化されているのがポイントです。
components/Chat.vue
<script setup lang="ts"> import { useChat } from "@ai-sdk/vue"; const { messages, input, handleSubmit, status } = useChat(); </script> <template> <div class="flex flex-col w-full max-w-md py-24 mx-auto gap-4"> <h1 class="text-2xl font-bold">Chat with AI</h1> <div class="flex flex-col gap-4"> <div v-for="message in messages" :key="message.id" class="p-3 rounded-lg"> <div :class=" message.role === 'user' ? 'bg-blue-100 dark:bg-blue-900 ml-auto' : 'bg-gray-100 dark:bg-gray-800' " class="p-3 rounded-lg max-w-xs" > <div class="text-sm font-semibold mb-1"> {{ message.role === "user" ? "You" : "AI" }} </div> <div class="whitespace-pre-wrap"> <!-- MCPクライアント 版では parts の処理を追加 --> <template v-if="message.parts"> <div v-for="(part, i) in message.parts" :key="`${message.id}-${i}`" > <div v-if="part.type === 'text'">{{ part.text }}</div> <div v-else-if="part.type === 'tool-invocation'" class="bg-yellow-50 dark:bg-yellow-900 p-2 rounded mt-2" > <!-- MCPサーバーのレスポンスを表示させる --> <div class="text-xs font-mono text-gray-600 dark:text-gray-400" > 🛠️ {{ part.toolInvocation.toolName }} </div> <div v-if="part.toolInvocation.state === 'result'" class="mt-1" > {{ part.toolInvocation.result.content?.[0]?.text || JSON.stringify(part.toolInvocation.result) }} </div> <div v-else class="text-xs text-gray-500">実行中...</div> </div> </div> </template> <template v-else> {{ message.content }} </template> </div> </div> </div> </div> <!-- 入力フォームは基本版と同じため省略 --> </div> </template>
では画面から MCP クライアントを使って MCP サーバーを実行してみましょう。
これだけで、チャットが MCP クライアント化し、MCP サーバーを自由に使えるようになります!
やってみた感想と今後やってみたいこと
@ai-sdk/vue
のおかげで、Nuxt でも Next.js とほぼ同じ感覚でチャット UI を実装できることがわかりました。Vercel AI SDK のエコシステムが充実していて、フレームワークを選ばずに同じような体験ができるのは素晴らしいですね。
MCP クライアント・サーバーの実装も@modelcontextprotocol/sdk
のお陰でかなり簡単に実装できることもわかりました。自分の中で MCP 開発のハードルが下がったなと感じました。
今後は社内向けの MCP ツールから試作して、最終的にプロダクトに組み込めると最高だなと思っています。取り組みが早い企業ではすでに内製 MCP を使ってゴリゴリ開発している企業もあるようです。ヤプリも負けられないですね。