01はじめに
この記事の対象読者
- AIエージェントに外部のAPIやデータソースを接続したいエンジニア
- MCPという名前は聞いたことがあるが、仕組みがよくわからないと感じている方
- TypeScript SDKを使って自前のMCPサーバーを動かしてみたい方
TypeScriptの基本的な読み書きができることを前提にしています。
async/await や npm の操作に慣れていると、コード例をよりスムーズに追えます。
実行環境
- Node.js: v20.18.1
- TypeScript: v5.4.5
@modelcontextprotocol/sdk: v1.0.0- Claude Desktop(接続テスト用)または任意のMCPホスト
この記事で得られること
- MCPのアーキテクチャ(ホスト・クライアント・サーバー)の理解
tools/resources/promptsという3つの機能の使い分け- TypeScript SDKを使って最小構成のMCPサーバーを動かす手順
- Claude Desktopからサーバーに接続して動作確認する方法
- 実運用で考慮すべき設計上のポイント
(関連記事: ツール呼び出し(Function Calling)の実装パターン)
02MCPが解決しようとした課題
AIエージェントを実プロダクトに組み込もうとすると、ほぼ必ず直面する問題があります。 「外部のデータやツールをどうエージェントに渡すか」という問題です。
たとえば、エージェントに「Notionのページを読む」「GitHubのIssueを取得する」「社内DBにクエリを投げる」といった能力を持たせようとすると、これまではそれぞれ独自の方法で実装する必要がありました。 OpenAIのFunction Calling、Anthropicのtool_use、各ホストアプリ固有のプラグイン仕様など、接続先が変わるたびに書き直しが発生していました。
MCP(Model Context Protocol)は、この「ツールとモデルの間の接続」を標準化するためのオープンプロトコルです。 Anthropicが2024年11月に仕様を公開し、その後複数のAIベンダーやOSSプロジェクトが対応を始めています。
プロトコルを標準化することで、一度MCPサーバーとして実装したツールは、MCPに対応したどのホストからでも利用できるようになります。 データベース接続、外部APIの呼び出し、ファイルシステムへのアクセスなど、繰り返し実装していた部分を共通の仕組みに乗せられるのが狙いです。
ただし、MCPはまだ進化中のプロトコルです。 2026年6月時点の仕様と実装を基準に解説しますが、将来のバージョンで変更が入る可能性があります。 筆者が動作確認した範囲を示しながら進めます。
03MCPのアーキテクチャ全体像
MCPは「ホスト」「クライアント」「サーバー」という3つのコンポーネントで構成されています。 それぞれの役割を整理してから、コードに入ります。
flowchart TD
subgraph HOST["ホスト(Host)\n例: Claude Desktop, VS Code, カスタムアプリ等"]
CLIENT["MCPクライアント(MCP Client)\n1クライアント = 1サーバー接続"]
end
subgraph SERVERPROC["MCPサーバープロセス"]
SERVER["MCPサーバー(MCP Server)\ntools / resources / prompts を提供する"]
end
CLIENT -- "JSON-RPC 2.0(stdio / Streamable HTTP)" --> SERVER
ホスト(Host)
ホストはエンドユーザーが直接操作するアプリケーションです。 Claude Desktop、VS Code(GitHub Copilot)、あるいは自作のエージェントアプリがホストに該当します。 ホストはMCPクライアントを1つ以上内包し、複数のMCPサーバーと同時に接続を保つことができます。
クライアント(Client)
クライアントはホスト内部に存在するコンポーネントで、MCPサーバーとの通信を担います。 1つのクライアントが1つのサーバーと1対1で接続します。 クライアントはサーバーが提供する機能(tools / resources / prompts)を取得し、ホストのLLMセッションから利用できる形に変換します。
サーバー(Server)
サーバーは機能を提供する側のプロセスです。 ファイルシステムへのアクセス、外部APIの呼び出し、データベースへのクエリなど、LLMが自分では実行できない操作をサーバーとして切り出します。 サーバーはMCPプロトコルに従って起動し、クライアントからのリクエストに応じて処理を実行します。
通信トランスポート
MCPサーバーとクライアントは、JSON-RPC 2.0プロトコルで通信します。 トランスポート層には現在2種類が標準として定義されています。
- stdio: サーバーをサブプロセスとして起動し、標準入出力経由で通信する。ローカル実行に適しています。
- Streamable HTTP: HTTPエンドポイントに対してリクエストを送り、レスポンスを通常のHTTPまたはSSEストリームで受け取る。ネットワーク越しの接続や複数クライアントへの対応に適しています。
なお、旧仕様では「HTTP+SSE」という別のトランスポートが存在しましたが、現行仕様では Streamable HTTP に置き換えられています。後方互換のために対応しているホスト実装もありますが、新規実装では Streamable HTTP を使うことを推奨します。
最小構成のサンプルではstdioを使います。 本番環境では用途に応じて Streamable HTTP を選ぶことが多いでしょう。
043つの機能(tools / resources / prompts)
MCPサーバーが提供できる機能は3種類に分類されています。 それぞれ性質が異なります。
tools(ツール)
tools はLLMがアクションを実行するために呼び出す関数です。
Function Callingと同じ概念で、モデルが「このツールをこの引数で実行してほしい」と要求を出し、サーバーが実際の処理を行います。
代表的な用途は以下の通りです。
- 外部APIの呼び出し(天気情報の取得、メールの送信など)
- データベースへの読み書き
- ファイルの作成・更新
- 計算や変換処理
tools はモデルが能動的に呼び出すものなので、副作用(外部への書き込みなど)を伴う処理に使います。
resources(リソース)
resources はサーバーが保持するデータをクライアントに公開する仕組みです。
URIで識別され、LLMはリソースを読み取ることでコンテキストを補完できます。
代表的な用途は以下の通りです。
- ファイルの内容(設定ファイル、ドキュメントなど)
- データベースのテーブル一覧やスキーマ
- 外部サービスのリソース(GitHubのファイル、Notionのページなど)
resources は基本的に読み取り専用です。
モデルが自発的に「関連しそうなリソースを取得する」ために使うこともありますが、実際にいつ・どのリソースを読むかの制御はホストやクライアントの実装に依存します。
prompts(プロンプト)
prompts はサーバーが提供するプロンプトテンプレートです。
特定のタスクに最適化されたシステムプロンプトやユーザープロンプトをサーバー側で管理し、クライアントが利用できる形で公開します。
代表的な用途は以下の通りです。
- 特定ドメインのタスク向け指示テンプレート(「コードレビュー用」「障害対応用」など)
- 引数を受け取って動的に生成するプロンプト
- チームで共有する標準化されたプロンプト
3種類の使い分けをまとめると次のようになります。
| 機能 | 目的 | 副作用 | 代表的な用途 |
|---|---|---|---|
| tools | アクション実行 | あり | API呼び出し、DB書き込み |
| resources | データ公開 | なし | ファイル読み取り、スキーマ参照 |
| prompts | テンプレート提供 | なし | タスク特化プロンプト管理 |
実装を始めるにあたって、まず tools を押さえておけば多くのユースケースをカバーできます。
resources と prompts は必要になった段階で追加する、という順番でも問題ありません。
05自作MCPサーバーの最小構成(TypeScript SDK)
ここからは実際にコードを書きながら進めます。 まず動くものを作り、その後で機能を追加していく流れにします。
プロジェクトのセットアップ
新しいディレクトリを作成し、必要なパッケージをインストールします。
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node ts-node
npx tsc --init
tsconfig.json を以下の内容に更新します。
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
package.json のスクリプトとモジュール設定を更新します。
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node --esm src/index.ts"
}
}
最小構成のサーバー(ツール1つだけ)
src/index.ts を作成します。
まず「現在時刻を返す」だけのシンプルなツールから始めます。
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// サーバーインスタンスを作成する
const server = new Server(
{
name: "my-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// ツール一覧を返すハンドラを登録する
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_current_time",
description:
"現在の日時をISO 8601形式で返します。タイムゾーンを指定できます。",
inputSchema: {
type: "object",
properties: {
timezone: {
type: "string",
description:
"タイムゾーン名。例: 'Asia/Tokyo', 'UTC'。省略するとUTCを使います。",
},
},
required: [],
},
},
],
};
});
// ツール実行のハンドラを登録する
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_current_time") {
const timezone = (args?.timezone as string) ?? "UTC";
try {
const now = new Date();
const formatted = now.toLocaleString("ja-JP", {
timeZone: timezone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
return {
content: [
{
type: "text",
text: `現在時刻(${timezone}): ${formatted}`,
},
],
};
} catch (error) {
// タイムゾーン名が無効な場合のエラーを返す
return {
content: [
{
type: "text",
text: `エラー: タイムゾーン "${timezone}" は無効です。"Asia/Tokyo" や "UTC" のような IANA タイムゾーン名を指定してください。`,
},
],
isError: true,
};
}
}
// 存在しないツール名が呼ばれた場合
throw new Error(`Unknown tool: ${name}`);
});
// stdioトランスポートで起動する
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
// stdioサーバーはstderrにログを出力する(stdoutはプロトコル通信に使うため)
console.error("MCP server started on stdio");
}
main().catch((error) => {
console.error("Failed to start server:", error);
process.exit(1);
});
ビルドして起動できるか確認します。
npm run build
# ビルドが成功すると dist/index.js が生成される
# エラーがなければ出力はない
起動テストをします。
node dist/index.js
MCP server started on stdio
stdioでの動作はこれで確認できました。 実際のリクエスト/レスポンスはMCPクライアント(ホスト)側から行います。
06Claude Desktopへの接続
動作確認として、Claude Desktopに接続します。 Claude Desktopはローカルで動くMCPサーバーをstdio経由で呼び出す機能を持っています。
設定ファイルの場所
Claude Desktopの設定ファイルは以下の場所にあります。
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
設定ファイルの記述
claude_desktop_config.json を編集します。
ファイルが存在しない場合は新規作成します。
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"]
}
}
}
/path/to/my-mcp-server/dist/index.js はビルド後のファイルの絶対パスに置き換えてください。
相対パスは動作しないことがあるため、絶対パスで指定することを推奨します。
接続確認
設定を保存したあと、Claude Desktopを再起動します。 チャット画面のツールバーに「ツール」アイコンが表示されていれば、MCPサーバーが認識されています。
Claude Desktopに次のようなメッセージを送ると、サーバーのツールが呼ばれます。
東京の現在時刻を教えてください。
期待されるやり取りのイメージは以下の通りです。
ユーザー: 東京の現在時刻を教えてください。
Claude: (内部でget_current_timeツールを呼び出す)
ツール引数: { "timezone": "Asia/Tokyo" }
ツール結果: 現在時刻(Asia/Tokyo): 2026/06/11 15:32:10
Claude: 東京の現在時刻は 2026年6月11日 15:32:10(JST)です。
07ツールを追加する:外部APIの呼び出し
最小構成が動いたところで、外部APIを呼び出すツールを追加します。 ここでは Open-Meteo の天気APIを使います。 APIキーが不要で試しやすいためです(2026年6月時点でフリープランあり)。
src/index.ts のツール定義とハンドラを拡張します。
// ... 前略(インポートとサーバー初期化は同じ)
// ツール一覧を返すハンドラ(2つのツールを追加)
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_current_time",
description:
"現在の日時をISO 8601形式で返します。タイムゾーンを指定できます。",
inputSchema: {
type: "object",
properties: {
timezone: {
type: "string",
description:
"タイムゾーン名。例: 'Asia/Tokyo', 'UTC'。省略するとUTCを使います。",
},
},
required: [],
},
},
{
name: "get_weather",
description:
"指定した緯度・経度の現在の天気情報を返します。Open-Meteo APIを使用します。",
inputSchema: {
type: "object",
properties: {
latitude: {
type: "number",
description: "緯度(-90〜90)。例: 35.6895(東京)",
},
longitude: {
type: "number",
description: "経度(-180〜180)。例: 139.6917(東京)",
},
},
required: ["latitude", "longitude"],
},
},
],
};
});
// 天気情報を取得する関数(ツールハンドラから呼び出す)
async function fetchWeather(
latitude: number,
longitude: number
): Promise<string> {
const url = new URL("https://api.open-meteo.com/v1/forecast");
url.searchParams.set("latitude", latitude.toString());
url.searchParams.set("longitude", longitude.toString());
url.searchParams.set("current", "temperature_2m,weather_code,wind_speed_10m");
url.searchParams.set("timezone", "auto");
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`Weather API error: ${response.status} ${response.statusText}`);
}
const data = (await response.json()) as {
current: {
temperature_2m: number;
weather_code: number;
wind_speed_10m: number;
time: string;
};
current_units: {
temperature_2m: string;
wind_speed_10m: string;
};
};
const { current, current_units } = data;
return [
`観測時刻: ${current.time}`,
`気温: ${current.temperature_2m}${current_units.temperature_2m}`,
`天気コード: ${current.weather_code}(WMO Weather Code)`,
`風速: ${current.wind_speed_10m}${current_units.wind_speed_10m}`,
].join("\n");
}
// ツール実行のハンドラ(2つのツールに対応)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_current_time") {
// ... 前と同じ実装
}
if (name === "get_weather") {
const latitude = args?.latitude as number;
const longitude = args?.longitude as number;
if (typeof latitude !== "number" || typeof longitude !== "number") {
return {
content: [
{
type: "text",
text: "エラー: latitude と longitude を数値で指定してください。",
},
],
isError: true,
};
}
try {
const result = await fetchWeather(latitude, longitude);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `天気情報の取得に失敗しました: ${message}`,
},
],
isError: true,
};
}
}
throw new Error(`Unknown tool: ${name}`);
});
// ... 後略(main関数は同じ)
ビルドして接続を確認します。
npm run build
# Claude Desktopを再起動してから試す
Claude Desktopに送るメッセージの例です。
東京(緯度35.6895、経度139.6917)の現在の天気を教えてください。
期待されるツール呼び出しの結果です。
観測時刻: 2026-06-11T15:00
気温: 26.4°C
天気コード: 2(WMO Weather Code)
風速: 8.2 km/h
08resources(リソース)を実装する
tools の次は resources を追加します。
ここでは、サーバーが管理する「設定値の一覧」をリソースとして公開する例を示します。
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// サーバーの capabilities に resources を追加する
const server = new Server(
{
name: "my-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {}, // resourcesを有効にする
},
}
);
// リソースデータ(実際の用途ではDBやファイルから読み込む)
const configData: Record<string, string> = {
"app/max_retries": "3",
"app/timeout_seconds": "30",
"app/log_level": "info",
};
// リソース一覧を返すハンドラ
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: Object.keys(configData).map((key) => ({
uri: `config://${key}`,
name: key,
description: `設定値: ${key}`,
mimeType: "text/plain",
})),
};
});
// リソースの内容を返すハンドラ
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
// "config://app/max_retries" → "app/max_retries" に変換する
const key = uri.replace("config://", "");
const value = configData[key];
if (value === undefined) {
throw new Error(`Resource not found: ${uri}`);
}
return {
contents: [
{
uri,
mimeType: "text/plain",
text: value,
},
],
};
});
// ... ツールのハンドラとmain関数は前と同じ
リソースのURIは scheme://identifier の形式で設計します。
今回は config:// というスキームを使いましたが、実際のユースケースに合わせて file://、db://、github:// のような名前にすることも多いです。
09prompts(プロンプト)を実装する
prompts を使うと、特定タスク向けのプロンプトテンプレートをサーバー側で管理できます。
コードレビュー用のプロンプトを例にします。
import {
GetPromptRequestSchema,
ListPromptsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// サーバーの capabilities に prompts を追加する
const server = new Server(
{ name: "my-mcp-server", version: "1.0.0" },
{
capabilities: {
tools: {},
resources: {},
prompts: {}, // promptsを有効にする
},
}
);
// プロンプト一覧を返すハンドラ
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "code_review",
description:
"コードレビュー用のプロンプトテンプレートです。レビュー観点を引数で指定できます。",
arguments: [
{
name: "focus",
description:
"レビューの重点観点。例: 'security', 'performance', 'readability'",
required: false,
},
],
},
],
};
});
// プロンプトの内容を返すハンドラ
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "code_review") {
const focus = (args?.focus as string) ?? "general";
const focusInstructions: Record<string, string> = {
security: "セキュリティの観点(認証・認可・入力検証・機密情報の扱い)を中心にレビューしてください。",
performance: "パフォーマンスの観点(計算量・メモリ使用・不要な処理・キャッシュの可能性)を中心にレビューしてください。",
readability: "可読性の観点(命名・関数の責務・コメントの有無・ネストの深さ)を中心にレビューしてください。",
general: "バグの可能性・可読性・パフォーマンス・セキュリティのバランスを取りながらレビューしてください。",
};
const instruction =
focusInstructions[focus] ?? focusInstructions["general"];
return {
description: `コードレビュー(${focus})`,
messages: [
{
role: "user",
content: {
type: "text",
text: `以下のコードをレビューしてください。\n\n${instruction}\n\n指摘は「問題点」「理由」「改善案」の3点セットで示してください。`,
},
},
],
};
}
throw new Error(`Unknown prompt: ${name}`);
});
prompts は tools と異なり、LLMがアクションを実行するためではなく、会話を特定の方向に誘導するためのものです。
ホストの実装によってはプロンプトの選択UIをユーザーに提供することもあります。
10エラーハンドリングの設計
MCPサーバーの実装でよくハマるのが、エラーの返し方です。 MCPのエラーには2種類の性質があります。
プロトコルレベルのエラー
サーバー実装のバグや想定外のリクエストに起因するエラーです。
throw new Error(...) を使うと、JSON-RPC 2.0のエラーレスポンスとしてクライアントに伝わります。
これはプロトコルレベルのエラーであり、クライアント側でエラー処理が必要になります。
// 存在しないツールが呼ばれた場合はthrowする(プロトコルエラー)
throw new Error(`Unknown tool: ${name}`);
ツール実行レベルのエラー
ツールの処理中に起きた「ビジネスロジック上のエラー」です。
たとえば「APIキーが無効」「対象リソースが見つからない」といったケースです。
これは isError: true を含むレスポンスとして返すのが設計上の正しい扱いです。
// ツール実行中のエラーはisError: trueで返す
return {
content: [
{
type: "text",
text: `APIキーが無効です。設定を確認してください。`,
},
],
isError: true,
};
isError: true で返すと、LLMはエラー内容を把握した上で「リトライを提案する」「ユーザーに確認を求める」といった対応を自分で選択できます。
throw すると通信レベルのエラーになり、LLMが内容を把握できないことがあります。
使い分けの目安をまとめると次の通りです。
| 状況 | 返し方 |
|---|---|
| 存在しないツール名が渡された | throw new Error(...) |
| ツール引数の型が期待と違う | isError: true で返す |
| 外部APIのレスポンスがエラー | isError: true で返す |
| サーバー内部の予期せぬ例外 | throw または isError: true(状況依存) |
11実運用で考慮すること
最小構成を動かした上で、実運用を意識するといくつかの観点が浮かびます。
認証・認可
MCPサーバーはデフォルトでは認証の仕組みを持っていません。 stdioでローカル実行する場合はOSのプロセス権限が境界になります。 HTTP SSEで外部公開する場合は、APIキー検証やOAuthなどの仕組みをトランスポート層で別途実装する必要があります。
環境変数の管理
APIキーやDB接続文字列はコードに直書きせず、環境変数から読み込む設計が基本です。
const apiKey = process.env.MY_SERVICE_API_KEY;
if (!apiKey) {
// 起動時に必須環境変数のチェックを行う
console.error("Error: MY_SERVICE_API_KEY is not set");
process.exit(1);
}
Claude Desktopの設定ファイルでも環境変数を渡せます。
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"],
"env": {
"MY_SERVICE_API_KEY": "your-api-key-here"
}
}
}
}
ログ出力
stdioトランスポートでは、stdout はプロトコル通信に専用されています。
デバッグログを console.log で書くとプロトコルが壊れるため、必ず console.error または stderr に書きます。
// 正しい: stderrに書く
console.error("[debug] tool called:", name, args);
// 誤り: stdoutに書くとプロトコルが壊れる
// console.log("[debug] tool called:", name, args);
HTTP SSEトランスポートを使う場合はこの制約はありませんが、ログライブラリを使って標準化しておくと後々の管理が楽になります。
ツールのinputSchemaを丁寧に書く
MCPの inputSchema は、LLMがツールを正しく使うための唯一の仕様書です。
Function Callingと同様に、description が曖昧だとモデルが意図しない引数を渡してくることがあります。
良いスキーマ記述の例を示します。
// 曖昧な例(避けたい)
{
name: "search",
description: "検索します。",
inputSchema: {
type: "object",
properties: {
q: { type: "string" },
n: { type: "number" },
},
required: ["q"],
},
}
// 具体的な例(推奨)
{
name: "search_documents",
description:
"ドキュメントをキーワードで全文検索し、関連度の高い順に結果を返します。",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "検索キーワード。複数単語はスペース区切りで指定します。",
},
max_results: {
type: "number",
description: "返す結果の最大件数。1〜50の整数。省略すると10を使います。",
default: 10,
},
},
required: ["query"],
},
}
(関連記事: ツール呼び出し(Function Calling)の実装パターン)
12BizPlanでのMCP活用について
筆者たちが開発している BizPlan(事業計画エージェント)では、外部データソースとの接続にMCPを活用しています。 具体的な内部実装の詳細は公開していませんが、設計思想として一点紹介します。
エージェントが利用するツールをMCPサーバーとして切り出すことで、「エージェントの推論ロジック」と「外部接続の実装」を分離できました。 外部サービスのAPI仕様が変わった際も、MCPサーバー側の修正だけで対応でき、エージェント本体のプロンプトや推論フローには手を入れずに済んでいます。
この分離がどの程度のメンテナンス改善につながるかは、システムの規模や変更頻度に依存します。 小規模なプロジェクトでは過剰設計になることもあるため、チームの状況に合わせて判断することを推奨します。
13まとめ
MCPはエージェントと外部ツールの接続を標準化するプロトコルです。 ホスト・クライアント・サーバーという3層の構造と、tools・resources・promptsという3種類の機能が基本です。
TypeScript SDKを使えば、比較的少ないコードで最小構成のMCPサーバーを立ち上げることができます。 今回のサンプルの流れをまとめると以下の通りです。
@modelcontextprotocol/sdkをインストールし、Serverインスタンスを作成するListToolsRequestSchemaハンドラでツール定義を返すCallToolRequestSchemaハンドラでツール処理を実装するStdioServerTransportで起動し、Claude Desktopの設定ファイルに登録する- 必要に応じて
resources・promptsを追加する
MCPはまだ仕様が変化している段階でもあります。 公式リポジトリのリリースノートを定期的に確認しながら、使っているSDKのバージョンを追随していくことを推奨します。
14参考文献
- Model Context Protocol 公式ドキュメント — 仕様の一次情報。アーキテクチャ・トランスポート・各機能の詳細が掲載されています。
- MCP TypeScript SDK(GitHub: modelcontextprotocol/typescript-sdk) — 本記事で使用したSDK。READMEにサンプルコードがあります。
- MCP サーバー実装例集(GitHub: modelcontextprotocol/servers) — ファイルシステム・GitHub・PostgreSQLなどの公式リファレンス実装。
- Open-Meteo API ドキュメント — 本記事のサンプルで使用した天気API。APIキー不要のフリープランがあります。
- JSON-RPC 2.0 仕様 — MCPの通信プロトコルの基盤仕様。

