コンピュータアクセスシステム

初めてのMCPサーバー構築


Model Context Protocol(MCP)は、AIエコシステムにおいてユニバーサルアダプターに最も近いものです。AIモデルがツールをどのように発見して呼び出すか、そしてそれらのツールが構造化された結果をどのように返すかを定義します。MCPサーバーが稼働したら、互換性のある任意のクライアント(Claude、Claude Code、またはMCPを話す任意のエージェントフレームワーク)が追加のグルーコードなしでそれを使用できます。

このガイドでは、実際のMCPサーバーをゼロから構築する手順を説明します。おもちゃの例ではなく、本番使用に拡張できるパターンです。

MCPとは実際に何か

MCPはstdio上のJSON-RPC 2.0プロトコルです(リモートサーバーの場合はHTTP+SSE)。クライアントとサーバーは小さなメッセージタイプのセットを交換します:

  • tools/list — クライアントが利用可能なツールを尋ねる
  • tools/call — クライアントが引数を指定してツールを呼び出す
  • resources/list / resources/read — クライアントがファイル、データベース、またはサーバーが公開する任意のコンテキストを読み取る

サーバーは各ツールの入力用JSON Schemaとともに、ツールを事前に宣言します。クライアントはこのスキーマを使用して有効な呼び出しを構築します。インタラクション全体はクライアントの観点からステートレスです。サーバーは必要な状態を内部で管理します。

この単純さが要点です。MCPはフレームワークではありません。Postgresデータベース、Kubernetesクラスター、GitHubリポジトリ、独自の内部APIなど何でも包み込み、任意のAIモデルが使用できる型付きで呼び出し可能な関数のセットとして公開できるコントラクトです。

セットアップ

Node.js 18+とTypeScriptプロジェクトが必要です。公式MCP SDKによりボイラープレートが最小限になります:

Terminal window
mkdir mcp-server && cd mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

tsconfig.jsonを追加します:

{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"outDir": "./dist"
},
"include": ["src"]
}

package.jsonにスタートスクリプトを追加します:

{
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}

サーバーの構築

src/index.tsを作成します。構造は常に同じです。サーバーを初期化し、ツールを登録し、stdioトランスポートに接続します。

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: "my-mcp-server",
version: "1.0.0",
});

次にツールを登録します。各ツールには、名前、モデルがいつ呼び出すかを決定するために使用する説明、および入力スキーマがあります:

server.tool(
"get_weather",
"Get current weather conditions for a city",
{
city: z.string().describe("The city name to get weather for"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
},
async ({ city, units }) => {
// Your actual implementation here
const data = await fetchWeather(city, units);
return {
content: [
{
type: "text",
text: `Weather in ${city}: ${data.temp}°${units === "celsius" ? "C" : "F"}, ${data.condition}`,
},
],
};
}
);

サーバーをstdioトランスポートに接続します:

const transport = new StdioServerTransport();
await server.connect(transport);

これが完全なサーバースケルトンです。残りはツールの追加です。

ツール設計パターン

MCPサーバーの品質は、ツールをどれだけうまく設計するかにかかっています。重要なパターンをいくつか紹介します:

幅広いモノリスではなく、狭く、合成可能なツール

一つのことをするツールは、多くのことをするものよりも有用です。モデルは狭いツールを予期しない方法で組み合わせることができます。幅広いツールはモデルを制限します。

これを避けてください:

server.tool("manage_database", "Do anything with the database", {
operation: z.enum(["read", "write", "delete", "schema"]),
query: z.string(),
// ... many optional params
})

こちらを好んでください:

server.tool("query_records", "Run a SELECT query and return rows as JSON", {
table: z.string(),
where: z.string().optional().describe("SQL WHERE clause, e.g. 'status = active'"),
limit: z.number().default(50),
})
server.tool("get_schema", "Return the column definitions for a table", {
table: z.string(),
})

入力を正確に説明する

モデルはdescribe()文字列を読んで何を渡すかを知ります。コードベースを見たことがない開発者向けのドキュメントとして扱ってください:

{
date_range: z.string().describe(
"ISO 8601 date range in the format YYYY-MM-DD/YYYY-MM-DD. " +
"Example: 2025-01-01/2025-03-31"
),
}

曖昧な説明は誤った入力を生みます。正確な説明は正確な入力を生みます。

生のJSONではなく、構造化されたテキストを返す

モデルはツール結果の生のJSONブロブよりもテキストをより確実に解析します。出力をフォーマットしてください:

return {
content: [
{
type: "text",
text: [
`Found ${rows.length} records:`,
...rows.map(r => `- ${r.id}: ${r.name} (${r.status})`),
].join("\n"),
},
],
};

呼び出し元が下流処理のために構造化データを本当に必要とする場合は、明確にラベル付けされたテキストブロック内にJSONを埋め込みます。

エラー処理

スローされた例外ではなく、ツールの結果としてエラーを返します。MCP SDKはスローされた例外をプロトコルレベルのエラーに変換しますが、クライアントはツールレベルのエラーとは異なる方法でそれらを処理します:

async ({ table, where }) => {
try {
const rows = await db.query(table, where);
return { content: [{ type: "text", text: formatRows(rows) }] };
} catch (err) {
return {
content: [
{
type: "text",
text: `Query failed: ${err instanceof Error ? err.message : "unknown error"}. ` +
`Check that the table name is correct and the WHERE clause uses valid column names.`,
},
],
isError: true,
};
}
}

isError: trueフラグはクライアントに結果が失敗を表すことを知らせ、エージェントが回復方法を決定できるようにします。

実際の例: GitHubツールサーバー

エージェントがリポジトリの内容を読み取れるようにするGitHub APIをラップした具体的なツールです:

import { Octokit } from "@octokit/rest";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
server.tool(
"get_file_contents",
"Read the contents of a file from a GitHub repository",
{
owner: z.string().describe("Repository owner (username or org)"),
repo: z.string().describe("Repository name"),
path: z.string().describe("File path within the repository"),
ref: z.string().optional().describe("Branch, tag, or commit SHA. Defaults to the default branch."),
},
async ({ owner, repo, path, ref }) => {
try {
const { data } = await octokit.rest.repos.getContent({
owner,
repo,
path,
ref,
});
if (Array.isArray(data)) {
// It's a directory — return listing
const listing = data.map(f => `${f.type === "dir" ? "📁" : "📄"} ${f.name}`).join("\n");
return {
content: [{ type: "text", text: `Directory listing for ${path}:\n${listing}` }],
};
}
if (data.type !== "file" || !data.content) {
return {
content: [{ type: "text", text: `${path} is not a readable file` }],
isError: true,
};
}
const content = Buffer.from(data.content, "base64").toString("utf-8");
return {
content: [
{
type: "text",
text: `Contents of ${owner}/${repo}/${path}:\n\`\`\`\n${content}\n\`\`\``,
},
],
};
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown error";
return {
content: [{ type: "text", text: `Failed to read ${path}: ${message}` }],
isError: true,
};
}
}
);

これは完全で本番対応のツールです。ディレクトリ、エラー、エンコーディングを処理し、モデルが簡単に読んで引用できるテキストで結果をラップします。

Claude Desktopへの接続

サーバーが稼働したら、macOSでは~/Library/Application Support/Claude/claude_desktop_config.json、Windowsでは%APPDATA%\Claude\claude_desktop_config.jsonの設定を編集してClaude Desktopに接続します:

{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"GITHUB_TOKEN": "your_token_here"
}
}
}
}

Claude Desktopを再起動します。ツールはツールセレクターに自動的に表示されます。

開発にはtsxを直接使用します:

{
"mcpServers": {
"my-server-dev": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/src/index.ts"]
}
}
}

Claude Codeへの接続

MCPサーバーはCLI経由でClaude Codeに接続します:

Terminal window
claude mcp add my-server node /absolute/path/to/dist/index.js

または環境変数を使って:

Terminal window
claude mcp add my-server --env GITHUB_TOKEN=your_token node /path/to/dist/index.js

Claude Codeは、それらから利益を得るタスクに取り組むとき、ツールを自動的に使用します。追加のプロンプトは不要です。

次に構築すること

MCPサーバーは本質的に能力の境界です。エージェントは何を見て、何ができるか?設計上の問いは常に、どの能力を公開するか、そしてそれらを安全にスコープするかです。

探求する価値のあるいくつかの方向性:

  • データベースサーバー — スキーマイントロスペクションツールを備えてPostgres、SQLite、またはDynamoDBへの読み取り専用クエリアクセスを公開し、エージェントがクエリを実行する前に構造を発見できるようにする
  • コード分析サーバー — tree-sitterまたは言語サーバープロトコルをラップし、単純なファイル読み取りを超えたコードベースの意味的理解をエージェントに与える
  • 監視サーバー — メトリクスシステム(Prometheus、Datadog)をブリッジして、エージェントが実際のオブザーバビリティデータでインシデントを調査できるようにする
  • ドキュメントサーバー — 内部ドキュメントやランブックをインデックス化して提供し、エージェントがすべてがどこにあるかを知らなくても運用上の質問に答えられるようにする

MCPエコシステムはまだ初期段階です。ほとんどの有用なサーバーはまだ構築されていません。パターンは、現在ダッシュボードやAPIを通じてアクセスしているものであれば何でもMCPサーバーの合理的な候補となるほど単純です。

あなたの環境で実際の問題を解決するものを構築してください。プロトコルが残りを処理します。


関連記事