目次を開く
Claude Code × MCP:自社システムにAIを直結させる完全ガイド

Claude Code × MCP:自社システムにAIを直結させる完全ガイド

Claude Code × MCP:自社システムに AI を直結させる完全ガイド

自分たちのビジネスシステムを AI に直結できたらどうなるか。

自社の営業支援システムでは、Go + Next.js + PostgreSQL のバックエンドに TypeScript 製の MCP(Model Context Protocol)サーバーを接続して、Claude Code から直接データベースクエリを実行したり API を呼び出したりできるようにした。

この記事では、その実装の全体像と、再現可能なステップバイステップガイドを共有する。

MCP とは何か — AI ツール連携の新標準

まず MCP について整理しておく。

MCP(Model Context Protocol) は、Anthropic が 2024 年 11 月にオープンソースとして公開した、AI モデルと外部ツール・データソースを接続するための標準プロトコルだ。公開直後から業界全体で採用が進み、2025 年には OpenAI、Google、Microsoft もサポートを表明した。現在は Linux Foundation の標準化プロジェクトとして運用されており、月間 9,700 万回以上の SDK ダウンロード8,200 以上の公開 MCP サーバーが存在する巨大なエコシステムに育っている。

つまり MCP は一企業の独自仕様ではなく、AI ツール連携のデファクトスタンダードだ。今 MCP に対応した仕組みを作っておけば、将来どの AI モデルを使うことになっても活用できる。

関連記事:生成AI × 業務効率化:ChatGPTを「使える」ツールにする具体的な方法

Before / After で見る変化

導入前と導入後の開発フローを比べてみる。

Before(MCP なし):

Developer → pgAdmin → スキーマ確認 → コピペ → Claude Code に貼り付け
           → Postman → API テスト → レスポンスコピー → Claude Code に貼り付け
           → 4つのアプリを行き来するコンテキストスイッチ地獄

After(MCP サーバー導入):

Claude Code ← MCP Server → API + Database
    ↓
 自然言語でSQLクエリ実行、データ分析、API操作をワンステップ

たとえば、こんなことがチャットだけで完結する。

  • “顧客テーブルから売上が100万円以上の企業を抽出して” → SQL 自動実行
  • “最新の営業案件を API 経由で取得して、ステータスごとに集計して” → DB + API 連携
  • “このユーザーのプロフィール画像を更新して” → API PATCH 自動実行 + JWT 自動更新

では、実際の実装に入っていく。

技術スタック

使うものを先に整理しておく。

コンポーネント技術
MCP SDK@modelcontextprotocol/sdk(TypeScript)
言語TypeScript 5.x+
実行環境Node.js + tsx(TypeScript ランナー)
DB クライアントpg(node-postgres)
スキーマ検証Zod
トランスポートStdio(Claude Code との通信)
認証JWT(自動リフレッシュ機能付き)

バージョンは npm install 時点の最新を使えば問題ない。MCP SDK は活発に更新されているので、package.json では ^ 指定にしておくといい。

実装ステップ — ゼロから動くサーバーを作る

Step 1: プロジェクトセットアップ

MCP サーバー用の新しい Node.js プロジェクトを作る。

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk pg zod
npm install -D typescript @types/node @types/pg tsx
npx tsc --init

package.json はこう設定する。

{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "description": "Custom MCP Server",
  "type": "module",
  "main": "src/index.ts",
  "scripts": {
    "start": "tsx src/index.ts",
    "build": "tsc",
    "typecheck": "tsc --noEmit"
  }
}

tsconfig.json の主要な設定:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "dist",
    "rootDir": "src",
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

ここまでで土台は完成。次にメインのデータベースツールを実装していく。

Step 2: データベースツールの実装 — SQL を安全に実行する

PostgreSQL への接続とクエリ実行を担当するツールを作る。MCP サーバーの核心部分だ。

2.1 基本的な MCP サーバー構造

src/index.ts のスケルトンから始める。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import pg from "pg";

// 環境変数から接続情報を読み込み
const DATABASE_URL = process.env.DATABASE_URL ||
  "postgresql://user:pass@localhost:5432/mydb";

// ロギング(stderr を使用して stdio との干渉を避ける)
function log(level: string, message: string, data?: unknown): void {
  const entry = { ts: new Date().toISOString(), level, message, ...(data && { data }) };
  process.stderr.write(JSON.stringify(entry) + "\n");
}

// データベースプール(遅延初期化)
let pool: pg.Pool | null = null;

function getPool(): pg.Pool {
  if (!pool) {
    pool = new pg.Pool({
      connectionString: DATABASE_URL,
      max: 5,
      idleTimeoutMillis: 30000,
    });
    pool.on("error", (err) => {
      log("error", "Unexpected pool error", { error: err.message });
    });
  }
  return pool;
}

// MCP サーバーの作成
const server = new McpServer({
  name: "my-system",
  version: "1.0.0",
});

// スタートアップ処理
async function main(): Promise<void> {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  log("info", "MCP Server started");
}

main().catch((err) => {
  log("error", "Fatal error", { error: err.message });
  process.exit(1);
});

ポイント: ログ出力は必ず stderr に書く。MCP は stdin/stdout を通信に使うため、console.log を使うとプロトコルが壊れる。ここはよくあるハマりどころだ。

2.2 SQL 安全性チェック — 事故を未然に防ぐ

データベースを直接触れるツールには安全装置がいる。以下のチェック機能を入れる。

import { z } from "zod";

// 危険なパターンの定義
const DANGEROUS_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
  { pattern: /\bDROP\b/i, label: "DROP" },
  { pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
  { pattern: /\bALTER\b/i, label: "ALTER" },
  { pattern: /\bCREATE\b/i, label: "CREATE" },
  { pattern: /\bDELETE\b(?!.*\bWHERE\b)/is, label: "DELETE without WHERE" },
  { pattern: /\bINSERT\s+INTO\b.*\bSELECT\b/is, label: "INSERT INTO...SELECT" },
];

// SQL の危険性をチェック
function checkSqlSafety(sql: string): string | null {
  for (const { pattern, label } of DANGEROUS_PATTERNS) {
    if (pattern.test(sql)) {
      return label;
    }
  }
  return null;
}

// 読み取り専用クエリか判定
function isReadOnly(sql: string): boolean {
  const trimmed = sql.trim().toUpperCase();
  return (
    trimmed.startsWith("SELECT") ||
    trimmed.startsWith("WITH") ||
    trimmed.startsWith("EXPLAIN") ||
    trimmed.startsWith("SHOW")
  );
}

この仕組みがあると、本番の全データをうっかり消すような事故を構造的に防げる。特に DELETE without WHERE の検出には実際に助けられた場面が何度かある。

2.3 query_db ツール — メインのクエリ実行エンジン

SQL を実行するメインツールだ。

server.tool(
  "query_db",
  "Execute a SQL query against the database. By default only SELECT queries " +
    "are allowed. Set allow_write=true to permit INSERT/UPDATE/DELETE.",
  {
    sql: z.string().describe("SQL query to execute"),
    params: z
      .array(z.unknown())
      .optional()
      .describe("Parameterized query values ($1, $2, ...)"),
    allow_write: z
      .boolean()
      .optional()
      .default(false)
      .describe("Allow write operations"),
  },
  async ({ sql, params, allow_write }) => {
    log("info", "query_db", { sql, params, allow_write });

    // 読み取り専用モードのチェック
    if (!allow_write && !isReadOnly(sql)) {
      return {
        content: [
          {
            type: "text" as const,
            text: "Blocked: Only SELECT/WITH/EXPLAIN queries in read-only mode.",
          },
        ],
        isError: true,
      };
    }

    // 危険なパターンのチェック
    const violation = checkSqlSafety(sql);
    if (violation) {
      return {
        content: [
          {
            type: "text" as const,
            text: `Blocked: Dangerous SQL operation (${violation}).`,
          },
        ],
        isError: true,
      };
    }

    try {
      const result = await getPool().query(sql, params ?? []);

      if (result.rows && result.rows.length > 0) {
        return {
          content: [
            {
              type: "text" as const,
              text: `Query returned ${result.rows.length} row(s):\n\n${JSON.stringify(result.rows, null, 2)}`,
            },
          ],
        };
      }

      return {
        content: [
          {
            type: "text" as const,
            text: `Query executed. Rows affected: ${result.rowCount ?? 0}`,
          },
        ],
      };
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      log("error", "query_db failed", { error: message });
      return {
        content: [
          { type: "text" as const, text: `SQL Error: ${message}` },
        ],
        isError: true,
      };
    }
  }
);

ここで注目してほしいのは params パラメータだ。$1, $2 形式のパラメータ化クエリが使えるので、SQL インジェクションを根本的に防げる。

関連記事:プロンプトエンジニアリング入門:AI への指示を最適化する技術

2.4 list_tables ツール — テーブル一覧の取得

データベースの全テーブルを一覧表示するツール。開発の最初に「何のテーブルがあるっけ?」と確認するときに重宝する。

server.tool(
  "list_tables",
  "List all tables with row counts and column counts.",
  {},
  async () => {
    log("info", "list_tables");

    const sql = `
      SELECT
        t.table_name,
        (SELECT count(*)::int FROM information_schema.columns c
         WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name)
         AS column_count,
        s.n_live_tup::int AS approx_row_count
      FROM information_schema.tables t
      LEFT JOIN pg_stat_user_tables s
        ON s.schemaname = t.table_schema AND s.relname = t.table_name
      WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE'
      ORDER BY t.table_name;
    `;

    try {
      const result = await getPool().query(sql);
      const lines = result.rows.map(
        (r: any) =>
          `${r.table_name.padEnd(40)} ${String(r.column_count).padStart(4)} cols   ~${String(r.approx_row_count ?? 0).padStart(8)} rows`
      );

      return {
        content: [
          {
            type: "text" as const,
            text:
              `Tables (${result.rows.length}):\n\n` +
              `${"Table".padEnd(40)} Cols   Approx Rows\n` +
              `${"─".repeat(65)}\n` +
              lines.join("\n"),
          },
        ],
      };
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return {
        content: [
          { type: "text" as const, text: `Error: ${message}` },
        ],
        isError: true,
      };
    }
  }
);

2.5 describe_table ツール — カラム・制約・外部キーの詳細表示

特定テーブルの詳細スキーマを取得する。カラム名、データ型、制約、外部キーまで一度に確認できる。

server.tool(
  "describe_table",
  "Get detailed schema including columns, types, constraints, and foreign keys.",
  {
    table: z.string().describe("Table name to describe"),
  },
  async ({ table }) => {
    log("info", "describe_table", { table });

    // テーブル名検証(SQLインジェクション対策)
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
      return {
        content: [
          {
            type: "text" as const,
            text: "Invalid table name. Only alphanumeric and underscore allowed.",
          },
        ],
        isError: true,
      };
    }

    try {
      // カラム情報取得
      const colSql = `
        SELECT
          c.column_name,
          c.data_type,
          c.character_maximum_length,
          c.is_nullable,
          c.column_default
        FROM information_schema.columns c
        WHERE c.table_schema = 'public' AND c.table_name = $1
        ORDER BY c.ordinal_position;
      `;
      const colResult = await getPool().query(colSql, [table]);

      if (colResult.rows.length === 0) {
        return {
          content: [
            {
              type: "text" as const,
              text: `Table '${table}' not found.`,
            },
          ],
          isError: true,
        };
      }

      // 制約情報取得
      const constraintSql = `
        SELECT
          tc.constraint_name,
          tc.constraint_type,
          kcu.column_name
        FROM information_schema.table_constraints tc
        JOIN information_schema.key_column_usage kcu
          ON tc.constraint_name = kcu.constraint_name
          AND tc.table_schema = kcu.table_schema
        WHERE tc.table_schema = 'public' AND tc.table_name = $1
        ORDER BY tc.constraint_type, tc.constraint_name;
      `;
      const constraintResult = await getPool().query(constraintSql, [table]);

      // 外部キー情報取得
      const fkSql = `
        SELECT
          kcu.column_name,
          ccu.table_name AS foreign_table,
          ccu.column_name AS foreign_column
        FROM information_schema.table_constraints tc
        JOIN information_schema.key_column_usage kcu
          ON tc.constraint_name = kcu.constraint_name
          AND tc.table_schema = kcu.table_schema
        JOIN information_schema.constraint_column_usage ccu
          ON ccu.constraint_name = tc.constraint_name
          AND ccu.table_schema = tc.table_schema
        WHERE tc.constraint_type = 'FOREIGN KEY'
          AND tc.table_schema = 'public'
          AND tc.table_name = $1;
      `;
      const fkResult = await getPool().query(fkSql, [table]);

      // 出力構築
      const sections: string[] = [];
      sections.push(`Table: ${table}\n`);

      // カラムセクション
      sections.push("Columns:");
      for (const col of colResult.rows) {
        let typeStr = col.data_type;
        if (col.character_maximum_length) {
          typeStr += `(${col.character_maximum_length})`;
        }
        const nullable = col.is_nullable === "YES" ? "NULL" : "NOT NULL";
        const def = col.column_default ? ` DEFAULT ${col.column_default}` : "";
        sections.push(
          `  ${col.column_name.padEnd(30)} ${typeStr.padEnd(25)} ${nullable}${def}`
        );
      }

      // 制約セクション
      if (constraintResult.rows.length > 0) {
        sections.push("\nConstraints:");
        const grouped = new Map<string, { type: string; columns: string[] }>();
        for (const row of constraintResult.rows) {
          const existing = grouped.get(row.constraint_name);
          if (existing) {
            existing.columns.push(row.column_name);
          } else {
            grouped.set(row.constraint_name, {
              type: row.constraint_type,
              columns: [row.column_name],
            });
          }
        }
        for (const [name, info] of grouped) {
          sections.push(
            `  ${info.type.padEnd(15)} ${name} (${info.columns.join(", ")})`
          );
        }
      }

      // 外部キーセクション
      if (fkResult.rows.length > 0) {
        sections.push("\nForeign Keys:");
        for (const fk of fkResult.rows) {
          sections.push(
            `  ${fk.column_name} -> ${fk.foreign_table}.${fk.foreign_column}`
          );
        }
      }

      return {
        content: [
          { type: "text" as const, text: sections.join("\n") },
        ],
      };
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return {
        content: [
          { type: "text" as const, text: `Error: ${message}` },
        ],
        isError: true,
      };
    }
  }
);

ここまででデータベース操作に必要な 3 つのツールが揃った。次は API 連携機能を追加する。

Step 3: API ツール実装 — JWT 認証を自動化する

REST API への認証付きアクセスを提供する。JWT トークンの取得と更新を MCP サーバーが自動で行うので、開発者は認証を意識しなくていい。

3.1 JWT トークン管理

let jwtToken: string | null = null;
let tokenExpiresAt = 0;

const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:8080";
const API_EMAIL = process.env.API_EMAIL || "[email protected]";
const API_PASSWORD = process.env.API_PASSWORD || "password";

// API ログイン
async function apiLogin(): Promise<string> {
  log("info", "API login", { email: API_EMAIL });

  const res = await fetch(`${API_BASE_URL}/api/v1/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email: API_EMAIL, password: API_PASSWORD }),
  });

  if (!res.ok) {
    const body = await res.text();
    throw new Error(`Login failed (${res.status}): ${body}`);
  }

  const json = (await res.json()) as {
    data?: { token?: { access_token?: string } };
    token?: string;
    access_token?: string;
  };

  const token =
    json.data?.token?.access_token ?? json.token ?? json.access_token;

  if (!token) {
    throw new Error("API response missing token field");
  }

  jwtToken = token;
  // トークン有効期限:1時間。5分前に自動更新
  tokenExpiresAt = Date.now() + 55 * 60 * 1000;
  log("info", "API login successful");
  return token;
}

// トークン取得(自動リフレッシュ付き)
async function getToken(): Promise<string> {
  if (!jwtToken || Date.now() >= tokenExpiresAt) {
    return apiLogin();
  }
  return jwtToken;
}

JWT(JSON Web Token) は Web API の認証で広く使われるトークン方式だ。サーバーがログイン時にトークンを発行し、クライアントはそれをリクエストに添付してログイン状態を維持する。有効期限があるため、期限切れ前に自動更新する仕組みが欠かせない。

3.2 api_request ツール — 認証済み API 呼び出し

server.tool(
  "api_request",
  "Make an authenticated REST API call. Automatically manages JWT authentication.",
  {
    method: z
      .enum(["GET", "POST", "PUT", "PATCH", "DELETE"])
      .describe("HTTP method"),
    path: z
      .string()
      .describe("API path (e.g., /api/v1/users)"),
    body: z
      .record(z.unknown())
      .optional()
      .describe("Request body (JSON)"),
  },
  async ({ method, path, body }) => {
    log("info", "api_request", { method, path });

    try {
      const token = await getToken();
      const url = `${API_BASE_URL}${path.startsWith("/") ? path : "/" + path}`;

      const headers: Record<string, string> = {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      };

      const fetchOptions: RequestInit = {
        method,
        headers,
      };

      if (body && ["POST", "PUT", "PATCH"].includes(method)) {
        fetchOptions.body = JSON.stringify(body);
      }

      const res = await fetch(url, fetchOptions);
      const contentType = res.headers.get("content-type") ?? "";

      let responseBody: string;
      if (contentType.includes("application/json")) {
        const json = await res.json();
        responseBody = JSON.stringify(json, null, 2);
      } else {
        responseBody = await res.text();
      }

      const statusLine = `${res.status} ${res.statusText}`;

      if (!res.ok) {
        return {
          content: [
            {
              type: "text" as const,
              text: `API Error (${statusLine}):\n\n${responseBody}`,
            },
          ],
          isError: true,
        };
      }

      return {
        content: [
          {
            type: "text" as const,
            text: `${method} ${path} -> ${statusLine}\n\n${responseBody}`,
          },
        ],
      };
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      log("error", "api_request failed", { error: message });
      return {
        content: [
          { type: "text" as const, text: `Request failed: ${message}` },
        ],
        isError: true,
      };
    }
  }
);

3.3 api_login ツール — 強制再ログイン

トークンをリセットして新しいものを取得する。認証エラーが起きたときに使う。

server.tool(
  "api_login",
  "Force re-login to get a fresh JWT token.",
  {},
  async () => {
    log("info", "api_login (forced)");

    try {
      jwtToken = null;
      tokenExpiresAt = 0;
      await apiLogin();
      return {
        content: [
          {
            type: "text" as const,
            text: "Successfully logged in. Token refreshed.",
          },
        ],
      };
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return {
        content: [
          { type: "text" as const, text: `Login failed: ${message}` },
        ],
        isError: true,
      };
    }
  }
);

これで 5 つのツール(query_dblist_tablesdescribe_tableapi_requestapi_login)が全て揃った。次は安全に運用するためのセキュリティ設計だ。

Step 4: セキュリティ — 本番運用に耐える安全設計

MCP サーバーはデータベースと API に直接アクセスできる。だからこそ、セキュリティ設計が最も重要なステップになる。ここまでの実装に組み込んだ安全機能を整理する。

4.1 多層防御の考え方

このサーバーでは 4 つのレイヤーで安全性を確保している。

レイヤー機能具体的な実装
1. 読み取り制限デフォルト読み取り専用allow_write=false で UPDATE/DELETE をブロック
2. パターン検出危険な SQL 検出DROP、TRUNCATE、DELETE without WHERE を自動ブロック
3. インジェクション対策パラメータ化クエリ$1, $2 プレースホルダーで SQL インジェクションを防止
4. 入力検証テーブル名ホワイトリスト英数字とアンダースコアのみ許可

SQL インジェクションとは、悪意のある SQL 文をアプリケーション経由で実行させる攻撃手法だ。パラメータ化クエリを使えば、ユーザー入力がそのまま SQL として実行されることを防げる。

4.2 環境変数による認証情報管理

認証情報をコードにハードコーディングするのは厳禁だ。必ず環境変数で管理する。

export DATABASE_URL="postgresql://user:[email protected]:5432/mydb"
export API_BASE_URL="https://api.example.com"
export API_EMAIL="[email protected]"
export API_PASSWORD="<secure-password>"

npm run start

4.3 監査ログ

すべてのクエリと API 呼び出しは stderr にログ出力される。本番環境ではログ集約サービスに転送すれば、不審なアクセスの検出に使える。

{"ts":"2026-02-25T10:30:00.000Z","level":"info","message":"query_db","data":{"sql":"SELECT * FROM customers","allow_write":false}}
{"ts":"2026-02-25T10:30:05.000Z","level":"info","message":"api_request","data":{"method":"PATCH","path":"/api/v1/users/42"}}

4.4 データベースアカウント権限の最小化

MCP サーバー用の専用 DB ユーザーを作り、必要最小限の権限だけを付与するのがベストプラクティスだ。

-- 読み取り専用ユーザー
CREATE USER mcp_readonly WITH PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE mydb TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;

Step 5: Claude Code への接続

最後のステップ。作った MCP サーバーを Claude Code に登録する。

5.1 Claude Code の MCP 設定

プロジェクトの .mcp.json(プロジェクト単位)または ~/.claude.json(グローバル)に以下を追加する。

{
  "mcpServers": {
    "my-system": {
      "command": "tsx",
      "args": [
        "/path/to/my-mcp-server/src/index.ts"
      ],
      "env": {
        "DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb",
        "API_BASE_URL": "http://localhost:8080",
        "API_EMAIL": "[email protected]",
        "API_PASSWORD": "password"
      }
    }
  }
}

補足: Claude Code の MCP 設定は claude mcp add コマンドでも追加できる。詳しくは Claude Code 公式ドキュメント を参照。

5.2 起動確認

cd my-mcp-server
npm run start

stderr に以下のログが出れば接続成功だ。

{"ts":"2026-02-25T10:30:00.000Z","level":"info","message":"MCP Server started"}

Claude Code を起動すると、チャット内で query_dblist_tables などのツールが使えるようになる。

実際の使い方:すぐに試せるユースケース 4 選

サーバーが動いたら、早速試してみよう。開発で頻繁に使うパターンを 4 つ紹介する。

ユースケース 1:顧客データの分析

User: "売上が500万円以上の顧客を全て取得して"

Claude Code:
> query_db(
    sql: "SELECT * FROM customers WHERE revenue >= 5000000",
    allow_write: false
  )

Result:
✓ Query returned 23 row(s):
[
  { id: 1, name: "ABC Corp", revenue: 6500000, ... },
  { id: 2, name: "XYZ Inc", revenue: 5200000, ... },
  ...
]

pgAdmin で SQL を手書きしていた作業が、自然言語で一発だ。

ユースケース 2:テーブル構造の確認

User: "顧客テーブルの構成を教えて"

Claude Code:
> describe_table(table: "customers")

Result:
✓ Table: customers

Columns:
  id                              int8              NOT NULL DEFAULT nextval(...)
  name                            text              NOT NULL
  email                           text              NOT NULL
  revenue                         numeric(12,2)    NULL
  created_at                      timestamp         NOT NULL DEFAULT now()

Constraints:
  PRIMARY KEY pk_customers (id)
  UNIQUE       uq_customers_email (email)

Foreign Keys:
  company_id -> companies.id

ユースケース 3:API 経由のデータ更新

User: "user_id=42 のプロフィール画像を更新して"

Claude Code:
> api_request(
    method: "PATCH",
    path: "/api/v1/users/42",
    body: { profile_image: "https://example.com/avatar.png" }
  )

Result:
✓ PATCH /api/v1/users/42 -> 200 OK
{
  "id": 42,
  "name": "John Doe",
  "profile_image": "https://example.com/avatar.png",
  "updated_at": "2026-02-25T10:35:00Z"
}

Postman を起動して URL を入力して認証ヘッダーを設定して……という手順が全部不要になる。

ユースケース 4:複雑な集計分析

User: "過去30日間の営業案件をステータスごとに集計して、
件数と平均金額を出して"

Claude Code:
> query_db(
    sql: `
      SELECT
        status,
        COUNT(*) as count,
        AVG(amount) as avg_amount
      FROM deals
      WHERE created_at >= NOW() - INTERVAL '30 days'
      GROUP BY status
      ORDER BY count DESC
    `,
    allow_write: false
  )

Result:
✓ Query returned 5 row(s):
[
  { status: "closed_won", count: 47, avg_amount: 1250000 },
  { status: "negotiation", count: 23, avg_amount: 800000 },
  { status: "prospect", count: 15, avg_amount: 500000 },
  ...
]

複雑な SQL もチャットで依頼するだけで実行できる。Claude が SQL を自動生成するので、SQL に詳しくないメンバーでもデータ分析に参加できる。

関連記事:AI 議事録ツールの選び方:会議の生産性を2倍にする実践ガイド

既存の MCP サーバーとの違い

「PostgreSQL 用の MCP サーバーなら既に公開されているのでは?」という疑問はもっともだ。実際、公開レジストリには PostgreSQL 用のサーバーがいくつかある。

ただ、自社システム専用に作る価値は明確にある。

観点汎用 MCP サーバー自作 MCP サーバー
API 連携なし(DB のみ)自社 API に完全対応
認証汎用的自社の JWT フローに最適化
セキュリティ汎用ルール自社のポリシーに合わせた制御
ビジネスロジックなし業務特有のツールを追加可能
メンテナンス外部依存チーム内で完全制御

汎用サーバーはデータベースだけ触れればいい場合には十分だが、API 連携、カスタム認証、業務固有のロジックが必要になると自作が圧倒的に有利になる。

よくある質問(FAQ)

Q: MCP サーバーは本番環境でも使えますか?

A: 使えるが、慎重な設計が要る。本番では読み取り専用の DB アカウント、SSL/TLS 通信、アクセスログの監視を必ず設定すること。この記事の「セキュリティ」セクションのベストプラクティスに従えば、安全に運用できる。

Q: 他の AI ツール(ChatGPT、Gemini など)でも使えますか?

A: 使える。MCP はオープン標準であり、対応する AI ツールなら何でも接続できる。2025 年以降、OpenAI や Google も MCP サポートを発表しており、一度作った MCP サーバーは複数の AI ツールで再利用可能だ。

Q: 複雑な JOIN クエリでも大丈夫ですか?

A: 基本的には問題ない。ただし、何十行にもわたる超複雑なクエリやパフォーマンスチューニングが必要なケースでは、DBeaver のような専用クエリエディタのほうが適している場面もある。MCP は日常の開発作業を効率化するツールであり、すべてを置き換えるものではない。

Q: セキュリティリスクはありますか?

A: MCP サーバー自体はローカルで動作し、ネットワークに公開されない。リスクは主に (1) 認証情報の管理ミス、(2) 過剰な DB 権限、(3) 安全チェックの不備 の 3 点。この記事で解説した多層防御(読み取り制限、パターン検出、パラメータ化クエリ、入力検証)をすべて実装すれば、リスクは最小限に抑えられる。

自社システム × MCP で開発体験を変える

MCP サーバーを自社システムに統合すると、以下が手に入る。

  • 開発効率の向上 — データベースクエリや API 呼び出しが自然言語で実行できる
  • 堅牢なセキュリティ — 多層防御で、読み取り専用、SQL パターン検出、パラメータ化クエリ、入力検証をカバー
  • 将来性 — MCP はオープン標準なので、一度作ればどの AI ツールでも再利用できる
  • 柔軟な拡張性 — 5 つのツールをベースに、業務に合わせたカスタムツールを足していける

実装は 600 行程度の TypeScript で完成し、セットアップも半日あれば十分。自社システムを持っているなら、専用の MCP サーバーを作る価値は十分にある。

この記事のコードをベースに、あなたのシステム専用の MCP サーバーを作ってみてほしい。

記事をシェアする