はじめに
バックエンド開発や分析 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 を利用できませんので、自身で構築するするのも手段に入ってくるかと思います。
今後も拡張や、何か良さげなものがあれば作ろうと思います。