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