@database-mcp/mysql
Advanced tools
| import type { DatabaseAdapter, QueryOptions, QueryResult, TableSummary, TableDescription, Connection } from "@database-mcp/core"; | ||
| export declare class MysqlAdapter implements DatabaseAdapter { | ||
| private connection; | ||
| engine: string; | ||
| private pool; | ||
| constructor(connection: Connection); | ||
| connect({ readOnly }: { | ||
| readOnly: boolean; | ||
| }): Promise<void>; | ||
| close(): Promise<void>; | ||
| query(sql: string, { maxRows, timeoutMs }: QueryOptions): Promise<QueryResult>; | ||
| listTables(): Promise<TableSummary[]>; | ||
| describeTable(table: string): Promise<TableDescription>; | ||
| } |
+106
| import mysql from "mysql2/promise"; | ||
| export class MysqlAdapter { | ||
| connection; | ||
| engine = "mysql"; | ||
| pool; | ||
| constructor(connection) { | ||
| this.connection = connection; | ||
| } | ||
| async connect({ readOnly }) { | ||
| const c = this.connection; | ||
| const common = { | ||
| connectionLimit: 4, | ||
| // BIGINT/DECIMAL beyond JS safe range arrive as strings (JSON-safe). | ||
| supportBigNumbers: true, | ||
| bigNumberStrings: false, | ||
| }; | ||
| this.pool = c.dsn | ||
| ? mysql.createPool({ uri: c.dsn, ...common }) | ||
| : mysql.createPool({ | ||
| host: c.host ?? "127.0.0.1", | ||
| port: c.port ?? 3306, | ||
| user: c.user, | ||
| password: c.password?.reveal(), | ||
| database: c.database, | ||
| ...common, | ||
| }); | ||
| if (readOnly) { | ||
| // Layer two: every pooled session rejects writes at the server, | ||
| // catching what the SQL guard can't see (CTE-smuggled writes etc.). | ||
| this.pool.on("connection", (conn) => { | ||
| conn.query("SET SESSION TRANSACTION READ ONLY"); | ||
| }); | ||
| } | ||
| await this.pool.query("SELECT 1"); // fail fast on bad host/credentials | ||
| } | ||
| async close() { | ||
| await this.pool.end(); | ||
| } | ||
| async query(sql, { maxRows, timeoutMs }) { | ||
| // ponytail: rows are fetched then capped — a SELECT over a huge table | ||
| // buffers before slicing; switch to mysql2's .stream() if it matters. | ||
| const [result, fields] = await this.pool.query({ sql, timeout: timeoutMs }); | ||
| if (!Array.isArray(result)) { | ||
| return { columns: [], rows: [], rowCount: result.affectedRows ?? 0, truncated: false }; | ||
| } | ||
| const columns = (fields ?? []).map((f) => f.name); | ||
| const truncated = result.length > maxRows; | ||
| const rows = result.slice(0, maxRows); | ||
| return { columns, rows, rowCount: rows.length, truncated }; | ||
| } | ||
| async listTables() { | ||
| const [rows] = await this.pool.query(`SELECT TABLE_NAME AS name, TABLE_ROWS AS estimatedRows | ||
| FROM information_schema.TABLES | ||
| WHERE TABLE_SCHEMA = DATABASE() | ||
| ORDER BY TABLE_NAME`); | ||
| return rows.map((r) => ({ | ||
| name: r.name, | ||
| estimatedRows: Number(r.estimatedRows ?? 0), | ||
| })); | ||
| } | ||
| async describeTable(table) { | ||
| const [cols] = await this.pool.query(`SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT | ||
| FROM information_schema.COLUMNS | ||
| WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? | ||
| ORDER BY ORDINAL_POSITION`, [table]); | ||
| const colRows = cols; | ||
| if (colRows.length === 0) | ||
| throw new Error(`unknown table: ${table}`); | ||
| const columns = colRows.map((c) => ({ | ||
| name: c.COLUMN_NAME, | ||
| type: c.COLUMN_TYPE, | ||
| nullable: c.IS_NULLABLE === "YES", | ||
| key: c.COLUMN_KEY || null, | ||
| default: c.COLUMN_DEFAULT, | ||
| })); | ||
| const [idx] = await this.pool.query(`SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE | ||
| FROM information_schema.STATISTICS | ||
| WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? | ||
| ORDER BY INDEX_NAME, SEQ_IN_INDEX`, [table]); | ||
| const byIndex = new Map(); | ||
| for (const r of idx) { | ||
| const entry = byIndex.get(r.INDEX_NAME) ?? { name: r.INDEX_NAME, columns: [], unique: !r.NON_UNIQUE }; | ||
| entry.columns.push(r.COLUMN_NAME); | ||
| byIndex.set(r.INDEX_NAME, entry); | ||
| } | ||
| const [fks] = await this.pool.query(`SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME | ||
| FROM information_schema.KEY_COLUMN_USAGE | ||
| WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? | ||
| AND REFERENCED_TABLE_NAME IS NOT NULL | ||
| ORDER BY CONSTRAINT_NAME, ORDINAL_POSITION`, [table]); | ||
| const byFk = new Map(); | ||
| for (const r of fks) { | ||
| const entry = byFk.get(r.CONSTRAINT_NAME) ?? { | ||
| name: r.CONSTRAINT_NAME, | ||
| columns: [], | ||
| referencesTable: r.REFERENCED_TABLE_NAME, | ||
| referencesColumns: [], | ||
| }; | ||
| entry.columns.push(r.COLUMN_NAME); | ||
| entry.referencesColumns.push(r.REFERENCED_COLUMN_NAME); | ||
| byFk.set(r.CONSTRAINT_NAME, entry); | ||
| } | ||
| return { name: table, columns, indexes: [...byIndex.values()], foreignKeys: [...byFk.values()] }; | ||
| } | ||
| } | ||
| //# sourceMappingURL=adapter.js.map |
| {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"} |
| #!/usr/bin/env node | ||
| export {}; |
| #!/usr/bin/env node | ||
| import { loadConfig, serve } from "@database-mcp/core"; | ||
| import { createRequire } from "node:module"; | ||
| import { MysqlAdapter } from "./adapter.js"; | ||
| const { version } = createRequire(import.meta.url)("../package.json"); | ||
| try { | ||
| const config = loadConfig(process.argv.slice(2), process.env, { | ||
| envPrefix: "MYSQL", | ||
| dsnEnvVar: "MYSQL_DSN", | ||
| }); | ||
| const c = config.connection; | ||
| if (!c.dsn && !c.host && !c.database) { | ||
| throw new Error("config: no database given — pass --dsn mysql://user@host/db, set MYSQL_HOST/MYSQL_USER/MYSQL_DATABASE, or use --config"); | ||
| } | ||
| await serve(new MysqlAdapter(c), config, version); | ||
| } | ||
| catch (e) { | ||
| console.error(e instanceof Error ? e.message : String(e)); | ||
| process.exit(1); | ||
| } | ||
| //# sourceMappingURL=index.js.map |
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAwB,CAAC;AAE7F,IAAI,CAAC;IACH,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE;QAC5D,SAAS,EAAE,OAAO;QAClB,SAAS,EAAE,WAAW;KACvB,CAAC,CAAC;IACH,MAAM,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC;IAC5B,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,wHAAwH,CACzH,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AACpD,CAAC;AAAC,OAAO,CAAC,EAAE,CAAC;IACX,OAAO,CAAC,KAAK,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC"} |
+34
-4
| { | ||
| "name": "@database-mcp/mysql", | ||
| "version": "0.0.0", | ||
| "description": "MCP server for MySQL with read-only guardrails, row caps, and statement timeouts. Placeholder — real release imminent.", | ||
| "version": "0.2.0", | ||
| "description": "MCP server for MySQL with read-only guardrails, row caps, and statement timeouts", | ||
| "license": "MIT", | ||
| "type": "module", | ||
| "main": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "bin": { | ||
| "database-mcp-mysql": "dist/index.js" | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "build": "tsc" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/arifulislamat/database-mcp.git" | ||
| "url": "git+https://github.com/arifulislamat/database-mcp.git", | ||
| "directory": "mysql/typescript" | ||
| }, | ||
| "homepage": "https://github.com/arifulislamat/database-mcp#readme", | ||
| "keywords": ["mcp", "model-context-protocol", "mysql", "database", "sql"], | ||
| "keywords": [ | ||
| "mcp", | ||
| "model-context-protocol", | ||
| "mysql", | ||
| "database", | ||
| "sql" | ||
| ], | ||
| "dependencies": { | ||
| "@database-mcp/core": "^0.2.0", | ||
| "mysql2": "^3.22.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^22.0.0", | ||
| "typescript": "^5.8.0" | ||
| }, | ||
| "engines": { | ||
| "node": ">=20" | ||
| }, | ||
| "publishConfig": { | ||
@@ -13,0 +43,0 @@ "access": "public" |
+56
-6
| # @database-mcp/mysql | ||
| MCP server giving AI clients safe, structured access to MySQL databases. | ||
| Two tools (`execute_sql`, `search_objects`), read-only by default, row caps, | ||
| statement timeouts. | ||
| MCP server giving AI clients safe, structured access to a MySQL database. | ||
| Two tools, guardrails on by default. | ||
| **This is a placeholder release securing the package name.** The real release | ||
| follows immediately. See the | ||
| [repository](https://github.com/arifulislamat/database-mcp) for status. | ||
| ## Quick start (Claude Desktop / Claude Code / Cursor) | ||
| The password comes from the environment or a mounted secret file — never | ||
| from the client config: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "mysql": { | ||
| "command": "npx", | ||
| "args": ["-y", "@database-mcp/mysql"], | ||
| "env": { | ||
| "MYSQL_HOST": "127.0.0.1", | ||
| "MYSQL_USER": "readonly_user", | ||
| "MYSQL_PASSWORD": "...", | ||
| "MYSQL_DATABASE": "mydb" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Alternatives: `MYSQL_PASSWORD_FILE=/run/secrets/mysql_password` | ||
| (Docker/K8s-style), a YAML file via `--config` (values support `${VAR}` | ||
| expansion), or `--dsn mysql://user@host:3306/db` (inline DSN passwords are | ||
| discouraged but redacted if used). `--print-config` shows the resolved | ||
| config with the password redacted. | ||
| ## Tools | ||
| - **`execute_sql`** `{ sql }` — run a single SQL statement. | ||
| - **`search_objects`** `{ table? }` — list tables, or describe one (columns, | ||
| indexes, foreign keys). | ||
| ## Guardrails (defaults) | ||
| | Guardrail | Default | Override | | ||
| | ------------- | -------- | ----------------------------------------- | | ||
| | Read-only | on | `--allow-write` / `ALLOW_WRITE` | | ||
| | Row cap | 1000 | `--max-rows` / `MAX_ROWS` | | ||
| | Query timeout | 30000 ms | `--query-timeout-ms` / `QUERY_TIMEOUT_MS` | | ||
| Read-only is enforced in two layers: a conservative SQL guard, plus | ||
| `SET SESSION TRANSACTION READ ONLY` on every pooled connection — so writes | ||
| smuggled through CTEs are rejected by the server itself. | ||
| ## Part of database-mcp | ||
| One package per engine, identical tool contract, shared conformance suite: | ||
| [github.com/arifulislamat/database-mcp](https://github.com/arifulislamat/database-mcp) | ||
| ## License | ||
| MIT |
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
13671
1424.08%8
300%141
Infinity%60
500%0
-100%Yes
NaN2
Infinity%2
Infinity%3
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added