Claude Codeは賢い──だが、社内のデータに触れないClaude Codeは『目隠しされた天才』に過ぎない。
この記事では、MCP(Model Context Protocol)サーバーを自作して、Claude Codeから社内PostgreSQL・Slack・Datadogに接続する3つの実践レシピを紹介します。読み終わる頃には「Claude Codeに社内の文脈を渡す設計パターン」が手元に揃っているはずです。
環境・前提条件
| 項目 | バージョン / 条件 |
|---|---|
| Claude Code | 最新版(CLI) |
| Node.js | v20 以上 |
| MCP SDK |
@modelcontextprotocol/sdk v1.x |
| PostgreSQL | 14 以上 |
| Slack API | Bot Token(xoxb-) |
| Datadog API | API Key + Application Key |
| OS | macOS / Linux(WSL2可) |
MCPサーバーはすべて TypeScript + stdio トランスポート で実装します。
MCPサーバーの仕組み30秒解説
MCPは、LLMが外部ツールを安全に・統一的に呼び出すためのオープンプロトコルです。Claude Codeがツールを呼ぶまでの流れを整理します。
ポイントは3つです。
- Claude Code自身がツール選択を行う ── プロンプトに応じて最適なMCPサーバーを自動で選びます
- JSON-RPCベースの標準プロトコル ── サーバーさえ書けばどんな外部サービスにも繋がります
- stdio / SSE の2種類のトランスポート ── ローカル開発にはstdioが最も手軽です
全体構成図
今回構築する3つのMCPサーバーの全体像です。
各MCPサーバーは独立したプロセスとして動作し、Claude Codeが必要に応じて呼び分けます。
レシピ1:PostgreSQL MCPサーバー ── 自然言語で社内DBをクエリさせる
ゴール
「先月の売上トップ10の顧客を教えて」とClaude Codeに頼むと、適切なSQLを生成→実行→結果を返してくれる状態を作ります。
実装
// src/postgres-mcp.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import pg from "pg";
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
});
const server = new McpServer({
name: "postgres-mcp",
version: "1.0.0",
});
// ツール1: スキーマ情報を取得
server.tool(
"get_schema",
"データベースのテーブル一覧とカラム情報を返します",
{
table_name: z.string().optional().describe("特定テーブル名(省略で全テーブル)"),
},
async ({ table_name }) => {
const query = table_name
? `SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = $1
ORDER BY ordinal_position`
: `SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' ORDER BY table_name`;
const params = table_name ? [table_name] : [];
const result = await pool.query(query, params);
return {
content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
};
}
);
// ツール2: SELECT文のみ実行可能なクエリツール
server.tool(
"execute_query",
"SELECT文のみ実行できる読み取り専用クエリツール",
{
sql: z.string().describe("実行するSELECT文"),
},
async ({ sql }) => {
// セキュリティ: SELECT以外は拒否
const normalized = sql.trim().toUpperCase();
if (!normalized.startsWith("SELECT")) {
return {
content: [{ type: "text", text: "エラー: SELECT文のみ実行可能です" }],
isError: true,
};
}
// 行数制限
const limitedSql = sql.includes("LIMIT") ? sql : `${sql} LIMIT 100`;
const result = await pool.query(limitedSql);
return {
content: [
{
type: "text",
text: `${result.rowCount}件取得:\n${JSON.stringify(result.rows, null, 2)}`,
},
],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Claude Code への登録
claude mcp add postgres-mcp -- npx tsx /path/to/src/postgres-mcp.ts
環境変数は .claude/ の設定か、起動スクリプトで渡します。
使い方の例
> 先月の売上トップ10の顧客名と売上額を教えて
Claude Code の動作:
1. get_schema を呼んでテーブル構造を確認
2. execute_query で適切なSELECTを生成・実行
3. 結果を日本語で整形して回答
設計のコツ: get_schema と execute_query を分離することで、Claude Codeが「まずスキーマを見る → SQLを組み立てる」という人間と同じ思考ステップを踏みます。これにより正確なSQL生成率が大幅に向上します。
レシピ2:Slack MCP ── チャンネルの議論をコンテキストとして渡す
ゴール
「#backend-design チャンネルで議論されている認証方式の結論をまとめて」といった指示に対応させます。
実装
// src/slack-mcp.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { WebClient } from "@anthropic-ai/slack";
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
const server = new McpServer({
name: "slack-mcp",
version: "1.0.0",
});
// ツール1: チャンネルのメッセージを取得
server.tool(
"get_channel_messages",
"指定チャンネルの最新メッセージを取得します",
{
channel_name: z.string().describe("チャンネル名(#なし)"),
count: z.number().default(20).describe("取得件数(最大50)"),
},
async ({ channel_name, count }) => {
const safeCount = Math.min(count, 50);
// チャンネル名からIDを解決
const channels = await slack.conversations.list({ limit: 200 });
const channel = channels.channels?.find((c) => c.name === channel_name);
if (!channel?.id) {
return {
content: [{ type: "text", text: `チャンネル #${channel_name} が見つかりません` }],
isError: true,
};
}
const history = await slack.conversations.history({
channel: channel.id,
limit: safeCount,
});
const messages = (history.messages ?? []).map((m) => ({
user: m.user,
text: m.text,
ts: m.ts,
thread_ts: m.thread_ts,
}));
return {
content: [{ type: "text", text: JSON.stringify(messages, null, 2) }],
};
}
);
// ツール2: スレッドの返信を取得
server.tool(
"get_thread_replies",
"スレッドの返信一覧を取得します",
{
channel_name: z.string(),
thread_ts: z.string().describe("スレッドのタイムスタンプ"),
},
async ({ channel_name, thread_ts }) => {
const channels = await slack.conversations.list({ limit: 200 });
const channel = channels.channels?.find((c) => c.name === channel_name);
if (!channel?.id) {
return { content: [{ type: "text", text: "チャンネルが見つかりません" }], isError: true };
}
const replies = await slack.conversations.replies({
channel: channel.id,
ts: thread_ts,
});
return {
content: [{ type: "text", text: JSON.stringify(replies.messages, null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
登録
claude mcp add slack-mcp \
-e SLACK_BOT_TOKEN=xoxb-xxxx \
-- npx tsx /path/to/src/slack-mcp.ts
実運用Tips: Bot Tokenのスコープは channels:history, channels:read の最小権限に絞ります。書き込み権限(chat:write)はセキュリティリスクが高いため、最初のうちは付与しないことを推奨します。
レシピ3:Datadog MCP ── アラート情報を読み取って障害対応コードを生成させる
ゴール
「現在発生中のアラートを確認して、対応に必要な修正コードを提案して」という障害対応フローをClaude Codeで実現します。
実装
// src/datadog-mcp.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { client, v1 } from "@datadog/datadog-api-client";
const config = client.createConfiguration({
authMethods: {
apiKeyAuth: process.env.DD_API_KEY!,
appKeyAuth: process.env.DD_APP_KEY!,
},
});
const monitorsApi = new v1.MonitorsApi(config);
const eventsApi = new v1.EventsApi(config);
const server = new McpServer({
name: "datadog-mcp",
version: "1.0.0",
});
// ツール1: アクティブなアラートを取得
server.tool(
"get_active_alerts",
"現在トリガー中のDatadogモニターアラートを取得します",
{
service: z.string().optional().describe("サービス名でフィルタ"),
},
async ({ service }) => {
const monitors = await monitorsApi.listMonitors({
monitorTags: service ? `service:${service}` : undefined,
});
const triggered = monitors.filter(
(m) => m.overallState === "Alert" || m.overallState === "Warn"
);
const summary = triggered.map((m) => ({
id: m.id,
name: m.name,
state: m.overallState,
message: m.message,
tags: m.tags,
lastTriggered: m.modified,
}));
return {
content: [
{
type: "text",
text: `${summary.length}件のアクティブアラート:\n${JSON.stringify(summary, null, 2)}`,
},
],
};
}
);
// ツール2: 最近のイベントログを取得
server.tool(
"get_recent_events",
"直近のDatadogイベントを取得します",
{
hours: z.number().default(1).describe("遡る時間数"),
priority: z.enum(["normal", "low"]).default("normal"),
},
async ({ hours, priority }) => {
const now = Math.floor(Date.now() / 1000);
const start = now - hours * 3600;
const events = await eventsApi.listEvents({
start,
end: now,
priority,
});
return {
content: [{ type: "text", text: JSON.stringify(events.events?.slice(0, 20), null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
実際の障害対応フロー
ユーザー: 「payment-serviceでアラートが出てる。原因を調べて修正案を出して」
Claude Code の動作:
1. get_active_alerts(service: "payment-service") でアラート内容を取得
2. get_recent_events(hours: 2) で直近イベントを取得
3. プロジェクト内のソースコードを読み取り
4. アラート内容 × ソースコードを照合し、修正コードを生成
この「外部コンテキスト × コードベース」の掛け合わせこそ、MCPでClaude Codeを拡張する最大の価値です。
セキュリティ設計:MCPサーバーに渡してよい情報の境界線
MCPサーバーは便利ですが、社内データをLLMに渡す行為であることを忘れてはいけません。
アクセス制御パターン
| パターン | 実装方法 | 推奨度 |
|---|---|---|
| リードオンリー制約 | SQLのSELECTのみ許可、Slackはreadスコープのみ |
★★★ |
| 行数制限 | クエリ結果を最大100行に制限 | ★★★ |
| リードレプリカ接続 | 本番DBではなくレプリカに接続 | ★★★ |
| カラムマスキング | PII列(email, phone等)をハッシュ化して返す | ★★☆ |
| 監査ログ | MCPサーバーで全リクエストをログ出力 | ★★★ |
| 許可リスト | 接続可能なテーブル・チャンネルを明示的に列挙 | ★★☆ |
// 監査ログの実装例(各ツールハンドラの先頭に追加)
function auditLog(tool: string, params: Record<string, unknown>) {
const entry = {
timestamp: new Date().toISOString(),
tool,
params,
pid: process.pid,
};
// 標準エラー出力に出す(stdoutはJSON-RPCで使用中のため)
console.error(JSON.stringify(entry));
}
3つのMCPを同時接続した際のパフォーマンス計測と最適化Tips
3つのMCPサーバーを同時に claude mcp add で登録し、実際の応答時間を計測しました。
計測環境
- MacBook Pro M3, メモリ36GB
- PostgreSQL: ローカルDocker
- Slack / Datadog: 本番API
計測結果
| 指標 | MCPなし | 1つ接続 | 3つ同時接続 |
|---|---|---|---|
| Claude Code起動時間 | 約1.2秒 | 約1.8秒 | 約2.5秒 |
| ツール呼び出し(初回) | - | 約300ms | 約350ms |
| ツール呼び出し(2回目以降) | - | 約150ms | 約180ms |
| メモリ使用量(合計) | 約120MB | 約180MB | 約290MB |
最適化Tips
1. 遅延起動(Lazy Initialization)
DB接続プールやAPIクライアントは、ツールが初回呼び出しされたタイミングで初期化します。
let pool: pg.Pool | null = null;
function getPool(): pg.Pool {
if (!pool) {
pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
}
return pool;
}
2. 結果のサイズを制御する
LLMに渡すコンテキストが大きすぎると処理速度もトークンコストも悪化します。MCPサーバー側で積極的に要約・フィルタしましょう。
// 悪い例: 全カラムを返す
const result = await pool.query("SELECT * FROM orders LIMIT 100");
// 良い例: 必要なカラムだけ返す
const result = await pool.query(
"SELECT id, customer_name, total_amount FROM orders LIMIT 100"
);
3. MCPサーバーのグルーピング
関連性の低いMCPサーバーが多すぎると、Claude Codeのツール選択精度が下がることがあります。プロジェクトごとに必要なMCPだけを有効化するのがベストです。
# プロジェクトスコープで登録(.claude/settings.local.json に保存)
claude mcp add postgres-mcp --scope project -- npx tsx ./mcp/postgres-mcp.ts
4. タイムアウト設定
外部APIの応答が遅い場合に備え、MCPサーバー内でタイムアウトを設定します。
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000); // 10秒
try {
const result = await fetch(url, { signal: controller.signal });
// ...
} finally {
clearTimeout(timeout);
}
まとめ
- **MCPサーバーは「Claude Codeの目と手」**── 社内DB・Slack・監視ツールに接続することで、コードベースだけでは得られない文脈をAIに渡せます
- セキュリティは「リードオンリー + 行数制限 + 監査ログ」の3点セットから始めるのが現実的です。最初から完璧を目指さず、段階的に制御を強化しましょう
- 3つ同時接続してもオーバーヘッドは軽微── 起動時間+1.3秒、メモリ+170MB程度。遅延初期化と結果サイズの制御を入れれば実用上問題ありません