はじめに

バックエンド開発や分析 SQL を構築する際に、毎度データベース定義書などをエージェントに読み込ます必要がありますが、それもなかなかにめんどくさく、また MySQL MCP も個人開発されたものしかなかったため、業務利用のため Cursor と一緒に構築してみました。

プロジェクトの構造

今回は TypeScript を用いて、下記の SDK を利用します。
https://github.com/modelcontextprotocol/typescript-sdk

フォルダ構成は次のとおりです。

mysql-mcp/
├── package.json
├── tsconfig.json
├── src/
│   └── index.ts
└── README.md

MCP サーバーの実装

1. 基本的な MCP サーバーの骨組み

// src/index.ts
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import mysql from "mysql2/promise";
import dotenv from "dotenv";

dotenv.config();

// サーバーインスタンスを作成
const server = new Server(
  {
    name: "mysql-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

2. データベース接続の設定

// MySQL接続設定
const dbConfig = {
  host: process.env.DB_HOST || "localhost",
  port: parseInt(process.env.DB_PORT || "3306"),
  user: process.env.DB_USER || "",
  password: process.env.DB_PASSWORD || "",
  database: process.env.DB_NAME || "",
};

// データベース接続
connection = await mysql.createConnection(dbConfig);

3. セキュリティ機能の実装

こちらはそもそもの接続を読み取り専用ユーザーで利用するべきではありますが、DB へ影響を及ぼす操作には許可設定を導入してます。

// セキュリティ設定
const securityConfig = {
  allowDrop: process.env.ALLOW_DROP === "true",
  allowCreate: process.env.ALLOW_CREATE === "true",
  allowInsert: process.env.ALLOW_INSERT === "true",
  allowUpdate: process.env.ALLOW_UPDATE === "true",
  allowDelete: process.env.ALLOW_DELETE === "true",
  maxRows: parseInt(process.env.MAX_ROWS || "1000"),
};

// 操作権限と実行操作のチェック
function isDangerousQuery(query: string): {
  isDangerous: boolean;
  reason: string;
} {
  const normalizedQuery = query.trim().toUpperCase();

  // DROP操作のチェック
  if (normalizedQuery.startsWith("DROP ") && !securityConfig.allowDrop) {
    return { isDangerous: true, reason: "DROP操作は許可されていません" };
  }

  // UPDATE文にWHERE句があるかチェック
  if (
    normalizedQuery.startsWith("UPDATE ") &&
    !normalizedQuery.includes("WHERE ")
  ) {
    return { isDangerous: true, reason: "WHERE句のないUPDATE文は禁止です" };
  }

  // DELETE文にWHERE句があるかチェック
  if (
    normalizedQuery.startsWith("DELETE ") &&
    !normalizedQuery.includes("WHERE ")
  ) {
    return { isDangerous: true, reason: "WHERE句のないDELETE文は禁止です" };
  }

  return { isDangerous: false, reason: "" };
}

4. 利用可能なツールの定義

// ツールリストを定義
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "mysql_query",
        description: "SQLクエリを実行します(SELECT、INSERT、UPDATE、DELETE)",
        inputSchema: {
          type: "object",
          properties: {
            query: {
              type: "string",
              description: "実行するSQLクエリ",
            },
            params: {
              type: "array",
              description:
                "プリペアドステートメント用のパラメータ(オプション)",
              items: { type: "string" },
            },
          },
          required: ["query"],
        },
      },
      {
        name: "mysql_list_tables",
        description: "データベース内の全テーブル一覧を取得します",
        inputSchema: {
          type: "object",
          properties: {},
        },
      },
      {
        name: "mysql_describe_table",
        description: "テーブルの構造とスキーマ情報を取得します",
        inputSchema: {
          type: "object",
          properties: {
            table: {
              type: "string",
              description: "調べるテーブル名",
            },
          },
          required: ["table"],
        },
      },
    ],
  };
});

5. ツール実行ハンドラーの実装

// ツール実行ハンドラー
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (!connection) {
    return {
      content: [
        {
          type: "text",
          text: "エラー: データベース接続が確立されていません",
        },
      ],
    };
  }

  try {
    switch (name) {
      case "mysql_query": {
        const { query, params = [] } = args as {
          query: string;
          params?: any[];
        };

        // セキュリティチェック
        const dangerCheck = isDangerousQuery(query);
        if (dangerCheck.isDangerous) {
          return {
            content: [
              {
                type: "text",
                text: `セキュリティエラー: ${dangerCheck.reason}`,
              },
            ],
          };
        }

        const [results] = await connection.execute(query, params);

        return {
          content: [
            {
              type: "text",
              text: `クエリが正常に実行されました:\n${JSON.stringify(
                results,
                null,
                2
              )}`,
            },
          ],
        };
      }

      case "mysql_list_tables": {
        const [results] = await connection.execute("SHOW TABLES");

        return {
          content: [
            {
              type: "text",
              text: `データベース内のテーブル:\n${JSON.stringify(
                results,
                null,
                2
              )}`,
            },
          ],
        };
      }

      case "mysql_describe_table": {
        const { table } = args as { table: string };
        const [results] = await connection.execute("DESCRIBE ??", [table]);

        return {
          content: [
            {
              type: "text",
              text: `${table} テーブルの構造:\n${JSON.stringify(
                results,
                null,
                2
              )}`,
            },
          ],
        };
      }

      default:
        return {
          content: [
            {
              type: "text",
              text: `不明なツール: ${name}`,
            },
          ],
        };
    }
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `エラー: ${
            error instanceof Error ? error.message : String(error)
          }`,
        },
      ],
    };
  }
});

設定ファイルの作成

1. 環境変数ファイル (.env)

# データベース設定
DB_HOST=localhost
DB_PORT=3306
DB_USER=your_username
DB_PASSWORD=your_password
DB_NAME=your_database

# セキュリティ設定
ALLOW_DROP=false
ALLOW_CREATE=false
ALLOW_INSERT=false
ALLOW_UPDATE=false
ALLOW_DELETE=false
MAX_ROWS=1000

2. TypeScript 設定 (tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

MCP 設定ファイル (mcp.json)

{
  "mcpServers": {
    "mysql": {
      "command": "node",
      "args": ["/path/to/your/mysql-mcp/dist/index.js"],
      "cwd": "/path/to/your/mysql-mcp",
      "env": {
        "DB_HOST": "localhost",
        "DB_PORT": "3306",
        "DB_USER": "your_username",
        "DB_PASSWORD": "your_password",
        "DB_NAME": "your_database",
        "MAX_ROWS": "1000"
      }
    }
  }
}

実際の使用例

今回はテスト用としてローカルにMySQLコンテナを立ち上げ、ECサイトサービス想定のテーブルを作成します。

categories – カテゴリテーブル
order_items – 注文項目テーブル
orders – 注文テーブル
products – 商品テーブル
users – ユーザーテーブル

テーブル一覧の取得

下記のようにテーブルの一覧が取得できました。

API開発を行う際など、そのまま構造を読み込んでくれるので、かなり便利です。

データ分析

ユーザーの一人あたりの商品の注文数を洗い出してもらいます。

テーブル構造の確認を行い、適切な対象のテーブルやカラムを選定してくれているようです。

結果が出ました。
気を遣って表にしてくれましたね、サマリーとしてその他の情報も載せてくれています。
これで自然言語でのクエリが実現できました。

さいごに

かなり簡単に構築することができました。
業務などではセキュリティの懸念から出所不明の MCP を利用できませんので、自身で構築するするのも手段に入ってくるかと思います。
今後も拡張や、何か良さげなものがあれば作ろうと思います。

参考資料