このレッスンのロックを解除するには、まず「MCPの基礎とアーキテクチャ」を完了してください。前のレッスンに戻る

目標とアーキテクチャ

このレッスンでは、荷物の重量、配送先ゾーン、サービスレベルから送料見積もりを計算する、1つの構造化された機能を公開するローカルMCPサーバーを構築します。この機能は、クライアントが引数を指定して明示的に呼び出し、結果を受け取るため、MCPツールとして実装します。レッスンの最後には、MCP対応クライアントがツールを検出し、入力スキーマを確認し、呼び出しを検証したうえで、人間が読みやすいテキストと構造化データの両方を受け取れるようになります。

サーバーにはTypeScript、Node.js 20以降、公式のMCP TypeScriptサーバーパッケージを使用します。通信にはstdioトランスポートを使用します。このモデルでは、MCPクライアントが子プロセスとしてサーバーを起動し、標準入力と標準出力を介してJSON-RPCメッセージを交換します。このトランスポートは、ローカルのAIアプリケーション、コマンドラインホスト、開発ツールに適しています。リモートにデプロイして共有するサーバーでは、通常はStreamable HTTPを使用します。

リクエストのフローはシンプルです。クライアントがプロセスを起動し、MCPの初期化ハンドシェイクを完了し、利用可能なツールを一覧表示して、選択したツールを呼び出します。SDKは、ハンドラーを実行する前に渡された引数を検証します。ハンドラーは業務上の計算を実行し、MCPの結果を返します。この分離は重要です。MCPは検出と通信を定義し、配送ルールはアプリケーションコードが担います。

AIアプリケーション、stdioによるJSON-RPC接続、MCPサーバー、入力スキーマ検証、送料見積もりハンドラー、そしてアプリケーションに返される構造化された結果を示すフローチャート

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

新しいプロジェクトを作成し、サーバーSDK、Zod、TypeScript、tsxランナーをインストールします。現在のSDKがESモジュールのimportを使用しているため、パッケージはESモジュールとして設定します。tsxランナーはTypeScriptのエントリーポイントを直接実行するため、この最初の演習ではビルドツールではなくMCPに集中できます。

bash
mkdir shipping-mcp-server
cd shipping-mcp-server
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod
npm install --save-dev typescript tsx @types/node
mkdir src

package.jsonにスクリプトを追加して、起動コマンドを安定させます。serverスクリプトはstdioプロセスを実行し、inspectスクリプトはMCP Inspectorを起動して、同じプロセスをどのように起動するかを指定します。

json
{
"name": "shipping-mcp-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"inspect": "npx @modelcontextprotocol/inspector npx tsx src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/server": "^2.0.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@types/node": "^24.0.0",
"tsx": "^4.0.0",
"typescript": "^6.0.0"
}
}

インストール時には、解決される依存関係の正確なバージョンが新しくなっている可能性があります。重要なアーキテクチャ上のポイントは、新しいサーバープロジェクトでは、以前のモノリシックなSDKパッケージではなく、専用のサーバーパッケージを使用することです。パッケージのインストールに失敗した場合は、Node.jsとnpmが利用可能であること、そしてNode.jsのバージョンが20以上であることを確認してください。

サーバーの実装

次の実装で `src/index.ts` を作成します。`createServer` 関数は新しい MCP サーバーを構築し、calculate-shipping ツールを登録します。入力スキーマでは、正のパッケージ重量、3つの配送先ゾーンのいずれか1つ、2つのサービスレベルのいずれか1つを受け付けます。出力スキーマでは、クライアントが文章を解析せずに利用できる構造化された結果を定義します。

typescript
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const inputSchema = z.object({
weightKg: z.number().positive().max(50),
zone: z.enum(['domestic', 'regional', 'international']),
service: z.enum(['standard', 'express'])
});
const outputSchema = z.object({
currency: z.literal('USD'),
baseCost: z.number(),
serviceSurcharge: z.number(),
totalCost: z.number(),
estimatedDays: z.number().int()
});
function createServer(): McpServer {
const server = new McpServer({
name: 'shipping-quote-server',
version: '1.0.0'
});
server.registerTool(
'calculate-shipping',
{
title: 'Calculate Shipping Quote',
description: 'Calculate a shipping price and delivery estimate for a package',
inputSchema,
outputSchema
},
async ({ weightKg, zone, service }) => {
const ratePerKg = {
domestic: 2.5,
regional: 5,
international: 9
}[zone];
const standardDays = {
domestic: 4,
regional: 7,
international: 14
}[zone];
const baseCost = Number((5 + weightKg * ratePerKg).toFixed(2));
const serviceSurcharge = service === 'express'
? Number((baseCost * 0.6).toFixed(2))
: 0;
const totalCost = Number((baseCost + serviceSurcharge).toFixed(2));
const estimatedDays = service === 'express'
? Math.max(1, Math.ceil(standardDays / 2))
: standardDays;
const result = {
currency: 'USD' as const,
baseCost,
serviceSurcharge,
totalCost,
estimatedDays
};
return {
content: [
{
type: 'text',
text: `Shipping costs USD ${totalCost.toFixed(2)} and takes approximately ${estimatedDays} days.`
}
],
structuredContent: result
};
}
);
return server;
}
void serveStdio(createServer);
console.error('Shipping MCP server is running on stdio');

ツールの説明には、その機能が何を行うのかを記述してください。AI エージェントは、ツールを呼び出すかどうかを判断する際に、名前、説明、および生成された JSON Schema を使用するためです。スキーマは実行時の境界としても機能します。負の重量、サポート対象外のゾーン、未知のサービスは、計算ハンドラーが実行される前に拒否されます。

結果を理解する

ハンドラーは、同じ結果を2つの形式で返します。content 配列には、モデルやユーザーインターフェースが直接読み取れるテキストブロックが含まれます。structuredContent 値には、価格の表示、オプションの比較、結果の保存、別のツールへの受け渡しを必要とするアプリケーション向けに、予測可能なフィールドが含まれます。出力スキーマが登録されているため、構造化された値はそのスキーマに適合していなければなりません。

重量3キログラム、regional ゾーン、express サービスを含む入力の場合、基本料金は20ドルです。express の追加料金は12ドルで、合計32ドル、推定配送日数は4日となります。成功時の結果は、次のような論理構造になります。

json
{
"content": [
{
"type": "text",
"text": "Shipping costs USD 32.00 and takes approximately 4 days."
}
],
"structuredContent": {
"currency": "USD",
"baseCost": 20,
"serviceSurcharge": 12,
"totalCost": 32,
"estimatedDays": 4
}
}

すべての情報を1つの文に埋め込むよりも、構造化された出力の方が適しています。クライアントが正規表現を使って合計金額や配送予定を取り出す必要はありません。テキストはモデルのコンテキストとして引き続き有用であり、構造化オブジェクトは安定したアプリケーションインターフェースを提供します。

実行とテスト

プロセスが構文エラーや import エラーなしで読み込まれることを確認するため、サーバーを直接起動します。ステータスメッセージを出力した後も待機し続けるはずです。stdio サーバーは MCP クライアントからリクエストを受信する必要があるためです。サーバーが稼働し続けていることを確認したら、ターミナルの割り込みコマンドで停止します。

bash
npm start

次に、MCP Inspector を起動します。Inspector はサーバーを起動し、接続ハンドシェイクを実行して、ツールの一覧表示と呼び出しを行うためのブラウザーインターフェースを提供します。サーバーに接続し、Tools ビューを開いて calculate-shipping を選択し、3、regional、express などの有効な値を送信します。

bash
npm run inspect

正常系だけを確認するのではなく、3つの動作を検証してください。まず、ツールが想定どおりの説明と入力フィールドを備えて表示されることを確認します。次に、有効なデータで呼び出し、構造化された結果を計算ルールと照合します。最後に、マイナス2のような無効な重量や、サポート対象外のゾーンを試します。無効なリクエストはハンドラーに到達するのではなく、スキーマ検証で失敗するはずです。

ツールの検出、weightKg 3、zone regional、service express による有効な calculate-shipping 呼び出し、構造化された USD 32 の結果、およびスキーマ検証によって停止される無効な負の重量の呼び出しを示すテスト手順

よくある失敗と次のステップ

stdio における最も重大なミスは、アプリケーションのログを標準出力に書き込むことです。標準出力はプロトコルメッセージ用に予約されているため、通常のログ行が JSON-RPC ストリームを破損させ、クライアントを切断する可能性があります。ステータスメッセージで示されているように、診断情報には標準エラー出力を使用してください。この制約は自分のコードだけでなく、予期しない出力を表示する可能性のあるライブラリにも適用されます。

Inspector がプロセスを起動できない場合は、プロジェクトのルートから起動コマンドを直接実行してください。インポートエラーは通常、パッケージ名が正しくない、Node.js のバージョンが古い、または ES module の設定が不足していることを示します。ツールは表示されるものの呼び出しに失敗する場合は、送信した引数名と型を確認してください。スキーマでは数値型の weight と、正確な enum 値が求められます。数値文字列は自動的には数値と同等として扱われません。

正常に起動するサーバーが、必ずしも有用な MCP サーバーであるとは限りません。そのツールには、明確な説明、制約された入力、決定論的な動作、そしてモデルとアプリケーションの両方が容易に利用できる結果が必要です。これで、ローカルでの一連の流れが完成しました。サーバーを作成し、構造化されたツールを登録し、stdio 経由で提供し、実際の MCP クライアントで動作を確認できます。次の実践的なステップは、同じ起動コマンドを AI ホストに接続するか、ツールの契約を維持したままローカル計算をデータベースや外部サービスに置き換えることです。

レッスンの練習

レッスンのチェックポイント

学んだ内容を復習し、回答へのフィードバックを受けましょう。

0/1
回答済み · 0/1