MCPサーバを自分で作ってMCPの仕組みを理解する
はじめに
Claude DesktopなどのAIエージェントには「MCP(Model Context Protocol)」という仕組みがあり、外部サービスとの連携が可能です。しかし、その仕組みを理解することは難しく、私も当初は設定しても正常に動作せず苦労しました。その時勉強したことを以下の記事として作成していました。
Claudeと会話する中で、ふとしたことからMCPを自分で作ってみることとなり、それでMCPの仕組みの理解が進みました。
本記事では、MCPサーバーを自作することで、MCPの仕組みを理解することを目指します。
詳しいことは公式サイトを読み解けばいいのですが、最初の入口としてお役に立てればと思います。
この記事で作るもの
- Hello World MCP - 最小構成のMCPサーバーで基本的な仕組みを理解する
- 為替換算 MCP - 外部APIと連携するMCPサーバーの例
- SQL Server MCP - データベースと連携するMCPサーバーの例
対象読者
- ClaudeでMCPを使ったことがある方
- MCPの基本的な仕組みを理解したい方
- 自分でMCPサーバーを作ってみたい方
本記事の前提環境
- Node.js v18以上
- TypeScript
- Claude Desktop
言語は何でもいいようですが、基本的にはAnthropicとコミュニティがSDKを提供しているTypeScript、JavaScript、Pythonを使うことが多いと思われます。
1. MCPサーバーの基本構造
1.1 MCPとは
MCP(Model Context Protocol)は、AIエージェント(Claude等)が外部サービスにアクセスするための標準プロトコルです。
MCPの役割:AI単体ではできない「外部API呼び出し」「DB接続」などの外部サービスの使用を、MCPサーバーが橋渡しします。
1.2 最小構成のプロジェクト「Hello World」でMCPの仕組みを理解する
外部サービスを使用せず、とりあえず"Hello World"を返すだけのMCPサーバー作成し、MCPの仕組みを理解します。
MCPサーバーを作成するには、以下のファイルが最低限必要です。TypeScriptの使用方法を理解する必要がありますが、本記事での詳細な説明は省略します。
mcp-hello/
├── package.json # プロジェクト設定・依存関係
├── tsconfig.json # TypeScript設定
└── src/
└── index.ts # MCPサーバー本体
1.3 package.json
{
"name": "mcp-hello",
"version": "1.0.0",
"description": "Hello World MCP Server",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}
ポイント:
MCPというよりはTypeScriptの設定ですが、以下のSDK、ライブラリを使っているのがポイントです。
-
@modelcontextprotocol/sdk- MCP公式SDK -
zod- 入力パラメータのバリデーションライブラリ
1.4 tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
これもTypeScriptでMCPサーバーを作るための設定で、MCP自体とは直接関係ありません。
1.5 MCPサーバーの雛形(src/index.ts)
以下がMCPの動作や構成を決める大事な本体です。
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// ========================================
// 1. サーバーの作成
// ========================================
const server = new McpServer({
name: "mcp-hello", // サーバー名(識別用)
version: "1.0.0", // バージョン
});
// ========================================
// 2. ツールの登録
// ========================================
server.registerTool(
"greet", // ツール名
{
description: "名前を受け取って挨拶を返します", // 説明
inputSchema: { // 入力パラメータ定義
name: z.string().describe("挨拶する相手の名前"),
},
},
async ({ name }) => { // 実際の処理
// 現在時刻に応じた挨拶を生成
const hour = new Date().getHours();
let greeting: string;
if (hour >= 5 && hour < 12) {
greeting = "おはようございます";
} else if (hour >= 12 && hour < 18) {
greeting = "こんにちは";
} else {
greeting = "こんばんは";
}
}
);
// ========================================
// 3. サーバーの起動
// ========================================
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("mcp-hello server started!");
}
main().catch(console.error);
1.6 各要素の説明
McpServer
const server = new McpServer({
name: "mcp-hello",
version: "1.0.0",
});
MCPサーバーの本体です。このインスタンスにツールを登録していきます。
registerTool
server.registerTool(
"ツール名", // Claudeが呼び出す際の識別子
{
description: "...", // Claudeがツールを選ぶ判断材料
inputSchema: {...}, // 入力パラメータの定義(Zod形式)
},
async (引数) => { // 実際の処理
return {
content: [{ type: "text", text: "結果" }],
};
}
);
AIが使用するツールの定義です。
descriptionは重要です。AIはこの説明を読んで、どのツールを使うべきか判断します。
必要に応じて複数定義できます。
複数登録する場合はserver.registerToolブロックを繰り返し記述します。
Zod によるスキーマ定義
例:
inputSchema: {
name: z.string().describe("挨拶する相手の名前"),
age: z.number().describe("年齢"),
isActive: z.boolean().describe("有効フラグ"),
}
Zodは入力パラメータの型と説明を定義するライブラリです。不正な入力を自動的にブロックしてくれます。
StdioServerTransport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("mcp-hello server started!");
AIとの通信方法を定義します。StdioServerTransportは標準入出力(stdin/stdout)でJSON-RPCメッセージをやり取りします。
最後の「Console.error」ですが、公式サイトの説明ではconsole.logなどで標準出力(stdout)には決して書き込まないようにとのことです。JSON-RPCメッセージを破損させMCPサーバーを動作不能にするそうです。
MCPサーバーにおけるログ記録
2. MCPが動作する仕組み
2.1 MCPサーバーのプログラムをコンパイル
コマンドプロンプト(Windowsの場合)で一連のプログラムが置いているルートフォルダ(mcp-hello)に移動して、「npm run build」を実行します。
コンパイルされたMCPサーバーの実行ファイルが作成されます。
2.2 claude_desktop_config.json
Claude Desktopでの例ですが、MCPサーバーの登録は、以下の設定ファイルで行います。
Windows: %APPDATA%\Claude\claude_desktop_config.json
Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"mcp-hello": {
"command": "node",
"args": ["C:\\path\\to\\mcp-hello\\dist\\index.js"]
}
}
}
ポイント:
-
command- 実行するコマンド -
args- コマンドライン引数 -
env- 環境変数(DB接続情報など)
詳しい設定方法は以前別の記事に書きました。
2.1 起動タイミング
Claude Desktopを例にすると、MCPサーバーは、Claude Desktop起動時に自動的に起動されます。
┌──────────────────────────────────────────┐
│ Claude Desktop 起動時の処理 │
│ │
│ 1. claude_desktop_config.json を読み込む │
│ │
│ 2. mcpServers に定義された各サーバーを起動 │
│ │
│ "mcp-hello" → node dist/index.js │
│ "別のMCP" → 別の node dist/index.js │
│ ・・・ │
│ │
│ 3. 標準入出力で接続を確立 │
│ 4. 利用可能なツール一覧を取得 │
└──────────────────────────────────────────┘
2.3 AIエージェントとの連携フロー
ユーザーが「山田さんにあいさつして」と入力した場合:
┌─────────────────────────────────────────────────────────────────┐
│ 1. ユーザー入力 │
│ 「山田さんにあいさつして」 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Claude が判断 |
│ 利用可能なツール一覧から description を読んで判断 │
│ 「このタスクには greet ツールが適切だ」 │
│ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. MCPサーバーにリクエスト送信 │
│ { │
│ "tool": "greet", │
│ "arguments": { "name": "山田" } │
│ } │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. MCPサーバーが処理 │
│ - 結果をテキストに整形 │
│ - Claudeに返却 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Claude がユーザーに回答 │
│ 「こんにちは、山田さん!MCPサーバーからの挨拶です。」 │
└─────────────────────────────────────────────────────────────────┘
いかがでしょうか。
ファイルの構成や決まり文句的に記述方法を守れば、あとは自由に作成することができます。
以下は少し実践的な例を挙げます。
3. 例1:外部API連携:為替換算MCPの作成
3.1 概要
外部の為替レートAPIを呼び出して、通貨換算を行うMCPサーバーを作成します。
MCPサーバー → HTTP Request → exchangerate-api.com → JSON Response
→ JSON Response → レートを取得して計算 → 結果をテキストで返す
3.2 使用するAPI
exchangerate-api.com の無料APIを使用します。
使用イメージ:
GET https://api.exchangerate-api.com/v4/latest/USD
Response:
{
"rates": {
"JPY": 157.50,
"EUR": 0.92,
...
}
}
現時点で、最新バージョンではアカウント登録が必須となっていますが、v4がまだ存在していてアカウントも不要でした。当記事でこだわるところではないのでv4を使用しています。
3.3 実装コード
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "mcp-exchangerate",
version: "1.0.0",
});
server.registerTool(
"convert_currency",
{
description: "為替レートを取得して通貨を換算します。例: USD→JPY",
inputSchema: {
from: z.string().describe("換算元の通貨コード(例: USD, EUR, JPY)"),
to: z.string().describe("換算先の通貨コード(例: USD, EUR, JPY)"),
amount: z.number().describe("換算する金額"),
},
},
async ({ from, to, amount }) => {
try {
// ========================================
// 外部APIを呼び出し(これがMCPの真価)
// ========================================
const apiUrl = `https://api.exchangerate-api.com/v4/latest/${from.toUpperCase()}`;
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`APIエラー: ${response.status}`);
}
const data = await response.json();
const rate = data.rates[to.toUpperCase()];
if (!rate) {
throw new Error(`通貨コード "${to}" が見つかりません`);
}
// ========================================
// 結果をテキストに整形して返す
// ========================================
const converted = amount * rate;
return {
content: [
{
type: "text" as const,
text: `${amount.toLocaleString()} ${from.toUpperCase()} = ${converted.toLocaleString(undefined, { maximumFractionDigits: 2 })} ${to.toUpperCase()}(レート: ${rate})`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "不明なエラー";
return {
content: [{ type: "text" as const, text: `エラー: ${errorMessage}` }],
isError: true,
};
}
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("mcp-exchangerate server started!");
}
main().catch(console.error);
3.4 ポイント解説
外部API呼び出し
const response = await fetch(apiUrl);
const data = await response.json();
Node.js 18以降では fetch() がネイティブで使えます。これでHTTPリクエストを送信し、JSONレスポンスを取得します。
エラーハンドリング
try {
// 処理
} catch (error) {
return {
content: [{ type: "text", text: `エラー: ${errorMessage}` }],
isError: true, // エラーフラグ
};
}
isError: true を設定すると、Claudeはエラーとして認識し、適切に対応します。
3.5 動作確認
Claude Desktopで以下のように話しかけます:
- 「500ユーロは何ドル?」
- 「100ドルを日本円に換算して」
ユーザーが「100ドルを円に換算して」と入力した場合:
┌─────────────────────────────────────────────────────────────────┐
│ 1. ユーザー入力 │
│ 「100ドルを円に換算して」 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Claude が判断 │
│ 「このタスクには convert_currency ツールが適切だ」 │
│ │
│ 利用可能なツール一覧から description を読んで判断 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. MCPサーバーにリクエスト送信 │
│ { │
│ "tool": "convert_currency", │
│ "arguments": { "from": "USD", "to": "JPY", "amount": 100 } │
│ } │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. MCPサーバーが処理 │
│ - 外部APIを呼び出し │
│ - 結果をテキストに整形 │
│ - Claudeに返却 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Claude がユーザーに回答 │
│ 「100ドルは約15,750円です(レート: 157.50)」 │
└─────────────────────────────────────────────────────────────────┘
4. 例2:データベース連携:SQL Server MCPの作成
4.1 概要
SQL Serverに接続してクエリを実行するMCPサーバーを作成します。
AIにテーブル構造やサンプルデータを理解させるためにコピペする手間が省けます。
MCPサーバー → mssqlライブラリ → SQL Server → 結果セット
→ 結果をテキストテーブル形式に整形 → Claudeに返す
SQL ServerがAPIなどを用意していないので、DBクライアントライブラリを使って直接接続します。
SQLを投げて帰ってきた結果を整形して返すだけです。
4.2 追加の依存関係
npm install mssql
npm install --save-dev @types/mssql
TypeScriptでSQL Serverを扱うためのライブラリと型定義をインストールします。
4.3 実装コード
例ではSELECT文結果、テーブル一覧、テーブル構造取得の3つのツールを実装しています。
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import sql from "mssql";
// ========================================
// DB接続設定(環境変数から取得)
// ========================================
const dbConfig: sql.config = {
server: process.env.MSSQL_HOST || "localhost",
database: process.env.MSSQL_DATABASE || "master",
user: process.env.MSSQL_USER || "",
password: process.env.MSSQL_PASSWORD || "",
options: {
encrypt: false,
trustServerCertificate: true,
},
};
const server = new McpServer({
name: "mcp-sql",
version: "1.0.0",
});
// ========================================
// ツール1: execute_query - SQLクエリを実行
// ========================================
server.registerTool(
"execute_query",
{
description: "SQL Serverにクエリを実行して結果を取得します。SELECT文のみ許可。",
inputSchema: {
query: z.string().describe("実行するSQLクエリ(SELECT文のみ)"),
},
},
async ({ query }) => {
// 安全性チェック: SELECT文のみ許可
const trimmedQuery = query.trim().toUpperCase();
if (!trimmedQuery.startsWith("SELECT")) {
return {
content: [
{
type: "text" as const,
text: "エラー: SELECT文のみ実行可能です。",
},
],
isError: true,
};
}
try {
// DB接続
const pool = await sql.connect(dbConfig);
// クエリ実行
const result = await pool.request().query(query);
const rows = result.recordset;
await pool.close();
if (rows.length === 0) {
return {
content: [{ type: "text" as const, text: "結果は0件でした。" }],
};
}
// ========================================
// 結果をテキストテーブル形式に整形
// ========================================
const columns = Object.keys(rows[0]);
let output = "";
output += columns.join("\t| ") + "\n";
output += columns.map(() => "---").join("\t| ") + "\n";
for (const row of rows) {
const values = columns.map((col) => {
const val = row[col];
if (val === null) return "(NULL)";
if (val instanceof Date) return val.toISOString();
return String(val);
});
output += values.join("\t| ") + "\n";
}
output += `\n(${rows.length}件の結果)`;
return {
content: [{ type: "text" as const, text: output }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "不明なエラー";
return {
content: [{ type: "text" as const, text: `SQLエラー: ${errorMessage}` }],
isError: true,
};
}
}
);
// ========================================
// ツール2: list_tables - テーブル一覧を取得
// ========================================
server.registerTool(
"list_tables",
{
description: "データベース内のテーブル一覧を取得します",
inputSchema: {},
},
async () => {
try {
const pool = await sql.connect(dbConfig);
const result = await pool.request().query(`
SELECT TABLE_SCHEMA, TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
ORDER BY TABLE_SCHEMA, TABLE_NAME
`);
await pool.close();
const rows = result.recordset;
let output = "テーブル一覧:\n\n";
for (const row of rows) {
output += `- ${row.TABLE_SCHEMA}.${row.TABLE_NAME}\n`;
}
output += `\n(${rows.length}テーブル)`;
return {
content: [{ type: "text" as const, text: output }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "不明なエラー";
return {
content: [{ type: "text" as const, text: `エラー: ${errorMessage}` }],
isError: true,
};
}
}
);
// ========================================
// ツール3: describe_table - テーブル構造を取得
// ========================================
server.registerTool(
"describe_table",
{
description: "指定したテーブルのカラム情報を取得します",
inputSchema: {
tableName: z.string().describe("テーブル名(例: dbo.Users)"),
},
},
async ({ tableName }) => {
try {
const pool = await sql.connect(dbConfig);
const parts = tableName.split(".");
const schema = parts.length > 1 ? parts[0] : "dbo";
const table = parts.length > 1 ? parts[1] : parts[0];
const result = await pool
.request()
.input("schema", sql.VarChar, schema)
.input("table", sql.VarChar, table)
.query(`
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table
ORDER BY ORDINAL_POSITION
`);
await pool.close();
const rows = result.recordset;
let output = `テーブル: ${schema}.${table}\n\n`;
output += "カラム名\t| 型\t| NULL許可\n";
output += "---\t| ---\t| ---\n";
for (const row of rows) {
output += `${row.COLUMN_NAME}\t| ${row.DATA_TYPE}\t| ${row.IS_NULLABLE}\n`;
}
return {
content: [{ type: "text" as const, text: output }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "不明なエラー";
return {
content: [{ type: "text" as const, text: `エラー: ${errorMessage}` }],
isError: true,
};
}
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("mcp-sql server started!");
}
main().catch(console.error);
4.4 ポイント解説
環境変数による設定
const dbConfig: sql.config = {
server: process.env.MSSQL_HOST || "localhost",
database: process.env.MSSQL_DATABASE || "master",
// ...
};
接続情報はコードにハードコードせず、環境変数から取得します。claude_desktop_config.json の env セクションで設定します。
安全性チェック
if (!trimmedQuery.startsWith("SELECT")) {
return { content: [...], isError: true };
}
SELECT文のみ許可することで、意図しないデータ変更を防ぎます。
パラメータ化クエリ
const result = await pool
.request()
.input("schema", sql.VarChar, schema)
.input("table", sql.VarChar, table)
.query(`... WHERE TABLE_SCHEMA = @schema ...`);
SQLインジェクション対策として、パラメータ化クエリを使用します。
4.5 Claude Desktop への登録
{
"mcpServers": {
"mcp-sql": {
"command": "node",
"args": ["C:\\path\\to\\mcp-sql\\dist\\index.js"],
"env": {
"MSSQL_HOST": "your_server_instance",
"MSSQL_USER": "your_user",
"MSSQL_PASSWORD": "your_password",
"MSSQL_DATABASE": "Your_Database"
}
}
}
}
4.6 動作確認
Claude Desktopで以下のように話しかけます:
- 「mcp-sqlでテーブル一覧を見せて」
- 「Usersテーブルの構造を教えて」
- 「SELECT TOP 10 * FROM Users を実行して」
5. まとめ
5.1 MCPサーバーの共通パターン
// 1. サーバー作成
const server = new McpServer({ name: "xxx", version: "1.0.0" });
// 2. ツール登録(複数可)
server.registerTool("ツール名", { description, inputSchema }, async (引数) => {
// ここで外部リソースにアクセス
// 結果をテキストに整形
return { content: [{ type: "text", text: "結果" }] };
});
// 3. 起動
const transport = new StdioServerTransport();
await server.connect(transport);
5.2 MCPの本質
| AIができないこと | MCPで橋渡し | 外部リソース |
|---|---|---|
| HTTPリクエスト | fetch() | 外部API |
| DB接続 | mssqlライブラリ | SQL Server |
| ファイル操作 | fs モジュール | ファイルシステム |
| などなど・・・ |
結果はすべて「テキスト」に整形してClaudeに返すことが重要です。
5.3 最後に
MCPサーバーの作り方を理解したら、社内システムとの連携や定型レポートの自動生成みたいな応用も可能になるでしょう。
当記事が何かのお役に立てれば幸いです。