# MCPサーバー開発入門 – 誤解しやすい概念から実装まで完全ガイド

## はじめに

有料で販売されているMCP(Model Context Protocol)サーバー開発に関する情報を見かけました。内容を確認したところ、基本的な概念説明と簡単なサンプルコードだけで数千円の価格設定でした。この程度なら無料で提供できると考え、Anthropic公式ドキュメントと[Model Context Protocol公式サイト](https://modelcontextprotocol.io/introduction)および技術評論社の公開情報を徹底的に調査しました。本記事では、MCPサーバーの開発方法を実装例とともに解説します。

## MCPとは何か – 誤解しやすい「サーバー」の意味

### プロトコルとしてのMCP

**MCP(Model Context Protocol)**は、LLMと外部ツールを接続するための**標準化されたプロトコル**です。ここで重要なのは、「サーバー」という言葉が一般的なWebサーバーとは異なる意味で使われている点です。

MCP文脈における「サーバー」とは、**LLMからの要求に応答する機能モジュール**を指します。HTTPサーバーのように常に起動している必要はなく、LLMクライアントが必要なときに呼び出されるプログラムです。

### 従来の統合方法との違い

従来、LLMに外部機能を追加するには、以下のような方法がありました:

– Function Calling: OpenAIやAnthropicのAPIが提供する機能呼び出し
– Plugin: 特定のLLMプラットフォーム専用の拡張機能
– Custom Integration: 個別にAPI統合を実装

これらはプラットフォームごとに実装が異なり、再利用性が低い問題がありました。MCPは、標準化されたインターフェースを提供することで、一度実装すれば複数のLLMクライアントから利用できる仕組みです。

### MCPが解決する課題

MCPは以下の課題を解決します:

1. 統合コストの削減: LLMごとに異なるAPI仕様に対応する必要がなくなる
2. 再利用性の向上: 一度実装したMCPサーバーは、対応するすべてのクライアントで利用可能
3. セキュリティの標準化: 認証・認可の仕組みがプロトコルレベルで定義される

[gRPCの仕組みを徹底解説](https://techinnovateit.com/grpc-architecture-rest-api-comparison/)で解説されているように、標準化されたプロトコルは開発効率を大幅に向上させます。

## MCPのアーキテクチャ – 3つの役割

MCPのアーキテクチャは、**3つの主要コンポーネント**で構成されます。

### 1. MCP Host(ホスト)

ホストは、ユーザーと直接対話するアプリケーションです。例えば:

– Claude Desktop
– IDE(VS Code、Cursor等)
– カスタムAIアシスタントアプリ

ホストは、ユーザーからの入力を受け取り、LLMに送信し、必要に応じてMCPサーバーを呼び出します。

### 2. MCP Client(クライアント)

クライアントは、ホストアプリケーション内でMCPサーバーと通信するモジュールです。クライアントは以下の役割を果たします:

– MCPサーバーの発見と接続
– サーバーが提供する機能(ツール、リソース、プロンプト)の取得
– ツール呼び出しの実行
– エラーハンドリング

ホストとクライアントは通常、同じアプリケーション内に存在します。

### 3. MCP Server(サーバー)

サーバーは、外部機能を提供するプログラムです。サーバーは以下を定義します:

– **Tools(ツール)**: LLMが実行できる関数(例: データベース検索、API呼び出し)
– **Resources(リソース)**: LLMが参照できるデータソース(例: ファイル、ドキュメント)
– **Prompts(プロンプト)**: 事前定義されたプロンプトテンプレート

サーバーは、標準入出力(stdin/stdout)またはHTTPトランスポートを介してクライアントと通信します。

## 開発環境セットアップ

MCPの仕様詳細はAnthropicの[公式ドキュメント](https://docs.anthropic.com/en/docs/mcp)でも随時更新されるため、実装前に最新情報を確認することをおすすめします。

### TypeScript環境

TypeScriptでMCPサーバーを開発する場合の環境構築手順です。

“`bash
# プロジェクト初期化
mkdir my-mcp-server
cd my-mcp-server
npm init -y

# MCPライブラリのインストール
npm install @modelcontextprotocol/sdk

# TypeScript関連パッケージ
npm install –save-dev typescript @types/node tsx

# tsconfig.json作成
npx tsc –init
“`

### Python環境

Python実装の場合は以下のようにセットアップします。

“`bash
# 仮想環境作成
python -m venv venv
source venv/bin/activate # Windowsの場合: venv\Scripts\activate

# MCPライブラリのインストール
pip install mcp
“`

## 基本的なMCPサーバーの実装例

### TypeScript実装

最もシンプルなMCPサーバーの実装例です。

“`typescript
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: “example-server”,
version: “0.1.0”,
},
{
capabilities: {
tools: {},
},
}
);

// ツール一覧の提供
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: “get_current_time”,
description: “現在時刻を取得します”,
inputSchema: {
type: “object”,
properties: {
timezone: {
type: “string”,
description: “タイムゾーン(例: Asia/Tokyo)”,
},
},
},
},
],
};
});

// ツール実行
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === “get_current_time”) {
const timezone = request.params.arguments?.timezone || “UTC”;
const now = new Date().toLocaleString(“ja-JP”, { timeZone: timezone });
return {
content: [
{
type: “text”,
text: `現在時刻: ${now}`,
},
],
};
}

throw new Error(`Unknown tool: ${request.params.name}`);
});

// サーバー起動
const transport = new StdioServerTransport();
await server.connect(transport);
“`

### Python実装

Python版の同等実装です。

“`python
from mcp.server.models import InitializationOptions
from mcp.server import NotificationOptions, Server
from mcp.server.stdio import stdio_server
from mcp import types
from datetime import datetime
import pytz

server = Server(“example-server”)

@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
return [
types.Tool(
name=”get_current_time”,
description=”現在時刻を取得します”,
inputSchema={
“type”: “object”,
“properties”: {
“timezone”: {
“type”: “string”,
“description”: “タイムゾーン(例: Asia/Tokyo)”,
}
},
},
)
]

@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
if name == “get_current_time”:
tz_name = arguments.get(“timezone”, “UTC”) if arguments else “UTC”
tz = pytz.timezone(tz_name)
now = datetime.now(tz).strftime(“%Y-%m-%d %H:%M:%S %Z”)
return [types.TextContent(type=”text”, text=f”現在時刻: {now}”)]

raise ValueError(f”Unknown tool: {name}”)

if __name__ == “__main__”:
import asyncio
asyncio.run(stdio_server(server))
“`

## ツール・リソース・プロンプトの定義方法

### Tools(ツール)の定義

**ツールは、LLMが実行できる関数**です。ツール定義には以下が必要です:

– name: ツールの一意な名前
– description: ツールの機能説明(LLMがツールを選択する際に参照)
– inputSchema: JSON Schemaで定義された入力パラメータ

例として、データベース検索ツールを実装します:

“`typescript
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: “search_database”,
description: “SQLiteデータベースを検索します”,
inputSchema: {
type: “object”,
properties: {
query: {
type: “string”,
description: “SQL SELECT文”,
},
limit: {
type: “number”,
description: “結果の最大件数”,
default: 10,
},
},
required: [“query”],
},
},
],
};
});
“`

### Resources(リソース)の定義

リソースは、LLMが参照できるデータソースです。ファイル、ドキュメント、API応答などを提供します。

“`typescript
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from “@modelcontextprotocol/sdk/types.js”;

server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: “file:///docs/manual.md”,
name: “製品マニュアル”,
description: “製品の操作マニュアル”,
mimeType: “text/markdown”,
},
],
};
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;

if (uri === “file:///docs/manual.md”) {
const content = await readFile(“./docs/manual.md”, “utf-8”);
return {
contents: [
{
uri: uri,
mimeType: “text/markdown”,
text: content,
},
],
};
}

throw new Error(`Unknown resource: ${uri}`);
});
“`

### Prompts(プロンプト)の定義

プロンプトは、事前定義されたプロンプトテンプレートです。よく使う質問パターンを定義できます。

“`typescript
import { ListPromptsRequestSchema, GetPromptRequestSchema } from “@modelcontextprotocol/sdk/types.js”;

server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: “code_review”,
description: “コードレビュー用プロンプト”,
arguments: [
{
name: “language”,
description: “プログラミング言語”,
required: true,
},
],
},
],
};
});

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
if (request.params.name === “code_review”) {
const language = request.params.arguments?.language || “Python”;
return {
messages: [
{
role: “user”,
content: {
type: “text”,
text: `以下の${language}コードをレビューしてください。バグ、パフォーマンスの問題、セキュリティリスクを指摘してください。`,
},
},
],
};
}

throw new Error(`Unknown prompt: ${request.params.name}`);
});
“`

## 実用的なMCPサーバーの例

### データベース接続サーバー

PostgreSQLデータベースに接続するMCPサーバーの例です。

“`typescript
import { Pool } from “pg”;

const pool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === “query_database”) {
const query = request.params.arguments?.query;
const limit = request.params.arguments?.limit || 100;

try {
const result = await pool.query(`${query} LIMIT ${limit}`);
return {
content: [
{
type: “text”,
text: JSON.stringify(result.rows, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: “text”,
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
}
});
“`

### API連携サーバー

外部APIと連携するMCPサーバーの例です。

“`typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === “search_github”) {
const query = request.params.arguments?.query;
const response = await fetch(
`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}`,
{
headers: {
“Authorization”: `token ${process.env.GITHUB_TOKEN}`,
“Accept”: “application/vnd.github.v3+json”,
},
}
);

const data = await response.json();
const repos = data.items.slice(0, 5).map((repo: any) => ({
name: repo.full_name,
description: repo.description,
stars: repo.stargazers_count,
url: repo.html_url,
}));

return {
content: [
{
type: “text”,
text: JSON.stringify(repos, null, 2),
},
],
};
}
});
“`

[Claude Opus 4.6 vs GPT-5.3-Codex](https://techinnovateit.com/claude-opus-46-vs-gpt-53-codex-comparison/)のような高度なLLMと組み合わせることで、より強力な統合が実現できます。

## デバッグとテスト方法

### ログ出力の実装

MCPサーバーのデバッグには、適切なログ出力が不可欠です。ただし、標準出力はMCPプロトコルの通信に使用されるため、**標準エラー出力(stderr)** にログを出力する必要があります。

“`typescript
function log(message: string) {
console.error(`[${new Date().toISOString()}] ${message}`);
}

server.setRequestHandler(CallToolRequestSchema, async (request) => {
log(`Tool called: ${request.params.name}`);
log(`Arguments: ${JSON.stringify(request.params.arguments)}`);

// ツール実行
const result = await executeTool(request.params.name, request.params.arguments);

log(`Result: ${JSON.stringify(result)}`);
return result;
});
“`

### MCP Inspector の活用

Anthropicが提供するMCP Inspectorを使うと、MCPサーバーの動作を視覚的にテストできます。

“`bash
# MCP Inspectorのインストール
npm install -g @modelcontextprotocol/inspector

# サーバーの起動とテスト
mcp-inspector node path/to/your/server.js
“`

Inspector上で、ツール一覧の確認、ツールの実行、レスポンスの検証が可能です。

### ユニットテストの実装

MCPサーバーのロジックをテストするには、通常のユニットテストフレームワークを使用します。

“`typescript
import { describe, it, expect } from “vitest”;
import { executeToolLogic } from “./server.js”;

describe(“get_current_time tool”, () => {
it(“should return current time in specified timezone”, async () => {
const result = await executeToolLogic(“get_current_time”, {
timezone: “Asia/Tokyo”,
});

expect(result.content[0].type).toBe(“text”);
expect(result.content[0].text).toMatch(/現在時刻:/);
});

it(“should default to UTC if no timezone specified”, async () => {
const result = await executeToolLogic(“get_current_time”, {});
expect(result.content[0].text).toMatch(/UTC/);
});
});
“`

## セキュリティとエラーハンドリング

### 入力検証

MCPサーバーは、すべての入力を検証する必要があります。LLMが不正なパラメータを送信する可能性があるためです。

“`typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === “search_database”) {
const query = request.params.arguments?.query;

// SQLインジェクション対策: SELECT文のみ許可
if (!query || !query.trim().toLowerCase().startsWith(“select”)) {
return {
content: [{
type: “text”,
text: “Error: Only SELECT queries are allowed”,
}],
isError: true,
};
}

// 危険なキーワードのチェック
const dangerousKeywords = [“drop”, “delete”, “update”, “insert”];
if (dangerousKeywords.some(kw => query.toLowerCase().includes(kw))) {
return {
content: [{
type: “text”,
text: “Error: Query contains dangerous keywords”,
}],
isError: true,
};
}

// クエリ実行
return await executeQuery(query);
}
});
“`

### エラーハンドリング

予期しないエラーが発生した場合の処理も重要です。

“`typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
// ツール実行ロジック
return await executeTool(request.params.name, request.params.arguments);
} catch (error) {
log(`Error in tool execution: ${error.message}`);
log(`Stack trace: ${error.stack}`);

return {
content: [{
type: “text”,
text: `An error occurred: ${error.message}`,
}],
isError: true,
};
}
});
“`

### 環境変数による設定管理

APIキーやデータベース接続情報は、環境変数で管理します。コードに直接記述してはいけません。

“`typescript
import dotenv from “dotenv”;
dotenv.config();

const dbConfig = {
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
};

if (!dbConfig.host || !dbConfig.database) {
throw new Error(“Database configuration is incomplete”);
}
“`

## まとめ

MCP(Model Context Protocol)サーバーの開発は、標準化されたプロトコルを理解すれば、それほど複雑ではありません。**重要なポイント**は以下の通りです:

1. **MCPの「サーバー」**は一般的なWebサーバーとは異なり、LLMから呼び出される機能モジュールである
2. **ツール、リソース、プロンプトの3つの要素**を適切に定義する
3. **セキュリティとエラーハンドリング**を徹底する
4. **MCP Inspector**を活用してデバッグする

MCPサーバーを実装することで、LLMに独自の機能を追加し、特定の業務に特化したAIアシスタントを構築できます。この記事が、MCPサーバー開発の第一歩となれば幸いです。

**メタディスクリプション**: MCPサーバー開発を基礎から解説。TypeScript/Python実装例、ツール定義、デバッグ方法まで網羅