1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPサーバーを自作してClaude Codeの能力を拡張する実践レシピ3選 ── 社内DB・Slack・監視ツール編

1
Posted at

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つです。

  1. Claude Code自身がツール選択を行う ── プロンプトに応じて最適なMCPサーバーを自動で選びます
  2. JSON-RPCベースの標準プロトコル ── サーバーさえ書けばどんな外部サービスにも繋がります
  3. 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_schemaexecute_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程度。遅延初期化と結果サイズの制御を入れれば実用上問題ありません

参考リンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?