
Vercelで始めるMCPサーバー構築入門
こんにちは、豊島です。
はじめに
VercelがMCPサーバーおよびMCPクライアント(MCPサーバーを呼び出すAIチャットボット)のデプロイをサポートしている記事が出ていました。
近年、AIモデルとの連携は多くのアプリケーション開発において重要な要素となっています。その中で、AIモデルとのコミュニケーションをより柔軟かつ効率的に行うためのプロトコルとして注目されているのが「Model Context Protocol (MCP)」です。
本記事では、VercelでのMCPサーバーホスティングのメリット、サンプルの実装と検証、そして今後の展望について解説します。
Model Context Protocol (MCP) とは?
MCPは、AIモデルが外部のツールやサービスと連携するためのプロトコルです。従来のAPIとは異なり、MCPはAIが特定のタスクを達成するために必要な「ツールキット」を提供するようなイメージです。これにより、AIはより複雑な処理や、外部の情報を活用した応答生成などが可能になります。
MCPの概要は以下のブログにてわかりやすく解説しています:
VercelでMCPサーバーをホスティングするメリット
Vercelは、MCPサーバーのホスティングに特化した以下のような利点があります:
- 簡単なデプロイ
- Git連携により、ソースコードをプッシュするだけで自動的にビルドとデプロイが実行されます。
- スケーラビリティ
- サーバーレスアーキテクチャにより、アクセスに応じて自動スケールします。
- 開発支援
@vercel/mcp-adapter
によりMCPツールの実装を簡素化できます。
- Fluid computeによるコスト効率とパフォーマンス
- VercelのFluid computeを利用することで、従来のサーバーレス環境と比較して、AI推論やエージェントワークロードにおいて90%以上のコスト削減と高いパフォーマンスを実現できると報告されています。
- SSEとHTTPトランスポートのサポート
@vercel/mcp-adapter
は、従来のServer-Sent Events (SSE) トランスポートと、新しいステートレスなHTTPトランスポートの両方をサポートしています。
参考: https://vercel.com/changelog/mcp-server-support-on-vercel
サンプルアプリを編集して触ってみる
Vercel公式テンプレートをベースに、以下2つのツールをMCPとして実装しました:
roll_dice
: 指定された面数のサイコロを振るgenerate_text
: OpenAI APIを利用して、与えられたプロンプトに基づいてテキストを生成する
これらはREST APIでも十分に実現可能ですが、MCPが本領を発揮するのは、複数のツールをAIが自律的に選択・連携するようなシナリオです。
たとえば:
- 情報検索 → 要約 → ドキュメント生成
- 自然言語の曖昧な指示に応じた一連の処理の自動選定
このような高度なAIエージェント的処理を支えるインフラとして、MCPは非常に有力な選択肢です。
今回はあくまで検証を目的とした簡易的な実装ですが、MCPの概念や活用イメージを掴むための一歩として参考になれば幸いです。
リポジトリ内のserver.ts
とtest-client.mjs
を編集します
server.ts
import { createMcpHandler } from '@vercel/mcp-adapter';
import { z } from 'zod';
import OpenAI from 'openai';
import * as dotenv from 'dotenv';
import { to } from 'await-to-js';
dotenv.config({ path: '.env.local' });
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const handler = createMcpHandler(server => {
server.tool(
'roll_dice',
'Rolls an N-sided die',
{ sides: z.number().int().min(2) },
async ({ sides }) => {
const value = 1 + Math.floor(Math.random() * sides);
return { content: [{ type: 'text', text: `🎲 You rolled a ${value}!` }] };
}
);
server.tool(
'generate_text',
'Generates text based on a prompt using OpenAI API',
{
prompt: z.string(),
max_tokens: z.number().int().min(1).max(4000).optional(),
temperature: z.number().min(0).max(2).optional(),
model: z.string().optional()
},
async ({ prompt, max_tokens = 1000, temperature = 0.7, model = "gpt-3.5-turbo" }) => {
const [error, response] = await to(openai.chat.completions.create({
model,
messages: [{ role: "user", content: prompt }],
max_tokens,
temperature
}));
if (error) {
console.error("OpenAI API error:", error);
return {
content: [{
type: 'text',
text: `エラーが発生しました: ${error.message || 'Unknown error'}`
}]
};
}
return {
content: [{
type: 'text',
text: response.choices[0].message.content || "テキスト生成に失敗しました。"
}]
};
}
);
});
export { handler as GET, handler as POST, handler as DELETE };
test-client.mjs
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { to } from 'await-to-js';
const origin = process.argv[2];
const toolNameToCall = process.argv[3];
const jsonInputString = process.argv[4];
async function main() {
const transport = new SSEClientTransport(new URL(`${origin}/sse`));
const client = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {
prompts: {},
resources: {},
tools: {},
},
}
);
const [connectErr] = await to(client.connect(transport));
if (connectErr) {
console.error("Error connecting to server:", connectErr);
return;
}
console.log("Connected", client.getServerCapabilities());
if (toolNameToCall) {
let input = jsonInputString ? JSON.parse(jsonInputString) : {};
if (input.sides !== undefined) {
input.sides = Number(input.sides);
}
console.log(`Calling tool: ${toolNameToCall} with input:`, input);
const [callErr, result] = await to(client.callTool({
name: toolNameToCall,
arguments: input,
}));
if (callErr) {
console.error("Error calling tool:", callErr);
} else {
console.log("Tool call result:", result);
}
}
client.close();
}
const [mainErr] = await to(main());
if (mainErr) {
console.error(mainErr);
}
ローカルでのテスト方法
以下のコマンドでローカルMCPサーバーにSSE経由でリクエストできます:
※localhost:3000を前提としています。Vercelにデプロイ後はURLを差し替えて下さい
generate_text の実行
node scripts/test-sse-client-invoke.mjs http://localhost:3000 generate_text '{"prompt": "日本の四季について短い文章を書いてください"}'
出力例:
Connected { tools: { listChanged: true } }
Calling tool: generate_text with input: { prompt: '日本の四季について短い文章を書いてください' }
Tool call result: {
content: [
{
type: 'text',
text: '日本の四季は春、夏、秋、冬に分かれ、それぞれの季節に特徴的な風景や行事が楽しめます。春には桜が満開になり、花見が楽しめる。夏には祭りや花火大会が開催され、海や山で夏を満喫する。秋には紅葉が美しい風景を作り出し、秋祭りが開催される。冬には雪景色が広がり、温泉で温まることができる。四季折々の美しい風景や季節ごとの行事が、日本の魅力となっている。'
}
]
}
roll_dice の実行
node scripts/test-sse-client-invoke.mjs http://localhost:3000 roll_dice '{"sides": 2000}'
出力例:
Connected { tools: { listChanged: true } }
Calling tool: roll_dice with input: { sides: 2000 }
Tool call result: { content: [ { type: 'text', text: '🎲 You rolled a 69!' } ] }
Vercelへのデプロイ手順
公式のサンプルアプリをVercelにデプロイするには、以下の手順を参考にしてください。
※Vercelアカウントを事前に作成しておく必要があります。
-
テンプレートページで「Deploy」をクリック
-
プロジェクト設定画面で「Create」をクリック
-
Redisが必要になるため、任意のRedisプロバイダーを選んで「Add」
-
「Connect」をクリック
-
ビルドとデプロイ処理が実行されます
これだけのプロセスで、Vercel上でのホスティングが完了します。
デプロイ後は、環境変数の追加やGitHubとの再連携し上記のような変更なども簡単に行えます。
まとめ
今回紹介したように、@vercel/mcp-adapter による開発支援や、Fluid Compute を活用した効率的な実行環境の提供により、MCPサーバーの構築と運用はこれまでになく手軽で実用的なものになりました。
これらの機能は、エンジニアにとっての生産性向上だけでなく、プロダクトオーナーやビジネスサイドにとっても、AIを活用したサービス開発を現実的な選択肢とする大きな後押しになります。
特に、Vercelにホスティングすることで、GPTなどの外部AIと連携可能な「リモートMCP」として活用でき、実運用を前提とした構成も容易に構築できます。
まずは、Vercel上でMCPサーバーを構築・体験してみるところから始めてみてはいかがでしょうか。