@andezdev/tokenlite-mysql-mcp 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,9 +10,11 @@ Designed specifically to solve the shortcomings of current generic MCP servers t
10
10
  ## 🌟 Core Pillars
11
11
 
12
12
  1. **Safe-Query Optimizer (AST & EXPLAIN)**: Protects production databases by pre-analyzing queries. Blocks unindexed Full Table Scans that exceed configurable thresholds and injects strict `LIMIT` clauses automatically at the AST level.
13
- 2. **Business Intelligence Injection**: Bridges the gap between raw data and company logic. Automatically attaches semantic dictionaries (`metadata.json`) to database schema exploration, and exposes a Semantic Template Search tool (`templates.json`) so the LLM uses pre-approved analytical queries instead of hallucinating them.
14
- 3. **Graph-Based Semantic Schema**: Avoids sending giant schemas to the LLM that saturate the context window. When a table is searched, the engine uses heuristics to deduce implicit relationships and packages the exact "Auto-Join Context".
15
- 4. **CSV Token Compression**: Database results are efficiently transformed into tabular CSV markdown, saving up to 60% of Output Tokens compared to verbose JSON.
13
+ 2. **Granular AST-Based Write Permissions**: By default, TokenLite is 100% Read-Only. You can surgically enable specific write operations (INSERT, UPDATE, DELETE, DDL) via environment variables. The firewall uses strict AST parsing to prevent SQL injection and comment-bypass attacks, and strictly prohibits privilege escalation commands (like `GRANT` or `CALL`).
14
+ 3. **Session-Level Defense in Depth**: If the server is configured in strict Read-Only mode (all write variables disabled), TokenLite injects `SET SESSION TRANSACTION READ ONLY` directly into the connection pool sockets. This guarantees that even if a theoretical bypass exists in the AST parser, the MySQL engine itself will physically reject any data modification.
15
+ 4. **Business Intelligence Injection**: Bridges the gap between raw data and company logic. Automatically attaches semantic dictionaries (`metadata.json`) to database schema exploration, and exposes Semantic Templates via the official **MCP Prompts API** (`templates.json`) so the LLM uses pre-approved analytical queries instead of hallucinating them.
16
+ 5. **Graph-Based Semantic Schema**: Avoids sending giant schemas to the LLM that saturate the context window. When a table is searched, the engine uses heuristics to deduce implicit relationships and packages the exact "Auto-Join Context".
17
+ 6. **CSV Token Compression**: Database results are efficiently transformed into tabular CSV markdown, saving up to 60% of Output Tokens compared to verbose JSON.
16
18
 
17
19
  ---
18
20
 
@@ -98,6 +100,14 @@ To use within Cursor IDE:
98
100
  | `MCP_SAFE_QUERY_ENABLE_BLOCKING`| Enable or disable the EXPLAIN guardrail. | `true` | No |
99
101
  | `MCP_METADATA_PATH` | Absolute path to your custom `metadata.json` dictionary. | (Disabled) | No |
100
102
  | `MCP_TEMPLATES_PATH` | Absolute path to your custom `templates.json` queries. | (Disabled) | No |
103
+ | `TOOL_PREFIX` | Prefix for tool names (useful when running multiple instances). | Random (e.g., `db_a1b2_`) | No |
104
+ | `MYSQL_QUERY_TIMEOUT` | Max execution time for a query (in ms). Aborts heavy queries to protect against DoS. | `15000` | No |
105
+ | `MYSQL_CONNECTION_LIMIT` | Max concurrent pool connections. | `10` | No |
106
+ | `MYSQL_CONNECT_TIMEOUT` | Max time to wait for a socket to establish (in ms). | `10000` | No |
107
+ | `ALLOW_INSERT_OPERATION` | Enable `INSERT` and `REPLACE` queries. | `false` | No |
108
+ | `ALLOW_UPDATE_OPERATION` | Enable `UPDATE` queries. | `false` | No |
109
+ | `ALLOW_DELETE_OPERATION` | Enable `DELETE` and `TRUNCATE` queries. | `false` | No |
110
+ | `ALLOW_DDL_OPERATION` | Enable Data Definition Language (`CREATE`, `ALTER`, `DROP`, `RENAME`). | `false` | No |
101
111
 
102
112
  ---
103
113
 
@@ -158,6 +168,26 @@ TokenLite converts raw database rows to a dense, structured CSV layout. This avo
158
168
 
159
169
  ---
160
170
 
171
+ ## 🌐 Advanced Networking & Remote Connections
172
+
173
+ By design, `tokenlite-mysql-mcp` adheres to the Unix philosophy: it does one thing (AI-driven MySQL interactions) and does it securely via the standard `stdio` transport. It deliberately avoids bloating the codebase with HTTP servers or built-in SSH clients.
174
+
175
+ If you need to connect to remote databases or expose this server over the network, here are the recommended, enterprise-grade alternatives:
176
+
177
+ ### 1. Connecting to Remote Databases (SSH Tunnels)
178
+ Instead of embedding SSH libraries, we recommend using native OS tunnels. This is much more secure, respects your `~/.ssh/config`, and supports advanced authentication (2FA, hardware keys).
179
+
180
+ Simply open a terminal and run:
181
+
182
+ ```bash
183
+ ssh -N -L 3306:127.0.0.1:3306 user@your-remote-server.com
184
+ ```
185
+ Then, point `tokenlite-mysql-mcp` to `localhost` and port `3306`.
186
+
187
+ ### 2. Exposing the MCP Server over HTTP/Network
188
+ If you need to host this MCP Server in the cloud (AWS, GCP) and have multiple Claude desktop clients connect to it remotely via HTTP/SSE, do not modify this codebase to add Express/HTTP logic.
189
+ Instead, wrap the process using standard open-source MCP proxies like [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy). This cleanly separates the transport layer security from the AI logic.
190
+
161
191
  ## 🐛 Troubleshooting
162
192
 
163
193
  **Error: `OptimizerError: Full table scan detected...`**
@@ -169,3 +199,5 @@ This means the MCP JSON-RPC protocol crashed. Ensure you are passing the correct
169
199
 
170
200
  ---
171
201
  *Built for the AI Engineering era.*
202
+
203
+ ---
package/dist/db/index.js CHANGED
@@ -10,24 +10,65 @@ export const pool = mysql.createPool({
10
10
  password: process.env.DB_PASSWORD || '',
11
11
  database: process.env.DB_NAME || 'test',
12
12
  waitForConnections: true,
13
- connectionLimit: 10,
13
+ connectionLimit: parseInt(process.env.MYSQL_CONNECTION_LIMIT || '10', 10),
14
14
  queueLimit: 0,
15
- connectTimeout: 10000 // 10 seconds
15
+ connectTimeout: parseInt(process.env.MYSQL_CONNECT_TIMEOUT || '10000', 10)
16
16
  });
17
+ // Session-Level Defense in Depth
18
+ const allowInsert = process.env.ALLOW_INSERT_OPERATION === 'true';
19
+ const allowUpdate = process.env.ALLOW_UPDATE_OPERATION === 'true';
20
+ const allowDelete = process.env.ALLOW_DELETE_OPERATION === 'true';
21
+ const allowDdl = process.env.ALLOW_DDL_OPERATION === 'true';
22
+ const isStrictReadOnlyMode = !(allowInsert || allowUpdate || allowDelete || allowDdl);
23
+ if (isStrictReadOnlyMode) {
24
+ pool.on('connection', (connection) => {
25
+ // At runtime this is a callback connection, despite what TS types say
26
+ connection.query('SET SESSION TRANSACTION READ ONLY', (err) => {
27
+ if (err) {
28
+ console.error('[tokenlite-mysql-mcp] Warning: Failed to set connection to READ ONLY:', err.message);
29
+ }
30
+ });
31
+ });
32
+ }
17
33
  export function getDbName() {
18
34
  return process.env.DB_NAME || 'test';
19
35
  }
20
36
  /**
21
- * Executes a safe query with a Timeout.
37
+ * Executes a safe query with a Timeout and Granular Permissions.
22
38
  */
23
39
  export async function executeSafeQuery(sql) {
24
40
  // AST Validation and Limit Injection
25
- const astOptimizedSql = injectLimitAst(sql);
26
- // Pre-flight Analysis
27
- await analyzeQueryPlan(astOptimizedSql, pool);
41
+ const { sql: astOptimizedSql, astType } = injectLimitAst(sql);
42
+ // Permission Enforcement
43
+ if (astType !== 'select' && astType !== 'show') {
44
+ const blockedTypes = ['call', 'grant', 'revoke', 'set', 'use'];
45
+ if (blockedTypes.includes(astType)) {
46
+ throw new Error(`Security Error: Dangerous operation '${astType}' is strictly prohibited.`);
47
+ }
48
+ const allowInsert = process.env.ALLOW_INSERT_OPERATION === 'true';
49
+ const allowUpdate = process.env.ALLOW_UPDATE_OPERATION === 'true';
50
+ const allowDelete = process.env.ALLOW_DELETE_OPERATION === 'true';
51
+ const allowDdl = process.env.ALLOW_DDL_OPERATION === 'true';
52
+ let isAllowed = false;
53
+ if ((astType === 'insert' || astType === 'replace') && allowInsert)
54
+ isAllowed = true;
55
+ else if (astType === 'update' && allowUpdate)
56
+ isAllowed = true;
57
+ else if ((astType === 'delete' || astType === 'truncate') && allowDelete)
58
+ isAllowed = true;
59
+ else if (['create', 'alter', 'drop', 'rename'].includes(astType) && allowDdl)
60
+ isAllowed = true;
61
+ if (!isAllowed) {
62
+ throw new Error(`Security Error: Operation '${astType}' is disabled in the server configuration.`);
63
+ }
64
+ }
65
+ // Pre-flight Analysis (Only blocks full table scans on SELECT)
66
+ if (astType === 'select') {
67
+ await analyzeQueryPlan(astOptimizedSql, pool);
68
+ }
28
69
  const [rows] = await pool.query({
29
70
  sql: astOptimizedSql,
30
- timeout: 15000
71
+ timeout: parseInt(process.env.MYSQL_QUERY_TIMEOUT || '15000', 10)
31
72
  });
32
73
  return rows;
33
74
  }
@@ -40,3 +81,15 @@ export async function pingDb() {
40
81
  return false;
41
82
  }
42
83
  }
84
+ /**
85
+ * Gracefully shuts down the MySQL connection pool.
86
+ */
87
+ export async function closePool() {
88
+ try {
89
+ await pool.end();
90
+ console.error('[tokenlite-mysql-mcp] Database connection pool closed gracefully.');
91
+ }
92
+ catch (error) {
93
+ console.error('[tokenlite-mysql-mcp] Error closing database connection pool:', error.message);
94
+ }
95
+ }
@@ -16,11 +16,11 @@ function isBlockingEnabled() {
16
16
  return process.env.MCP_SAFE_QUERY_ENABLE_BLOCKING !== 'false';
17
17
  }
18
18
  /**
19
- * Parses the SQL query to AST, injects a LIMIT if missing, and returns the modified SQL.
19
+ * Parses the SQL query to AST, injects a LIMIT if missing for SELECTs, and returns the modified SQL and AST type.
20
20
  */
21
21
  export function injectLimitAst(sql, maxLimit = 500) {
22
22
  if (sql.trim().toUpperCase().startsWith('SHOW')) {
23
- return sql;
23
+ return { sql, astType: 'show' };
24
24
  }
25
25
  try {
26
26
  const astOpt = { database: 'MySQL' };
@@ -32,27 +32,29 @@ export function injectLimitAst(sql, maxLimit = 500) {
32
32
  }
33
33
  ast = ast[0];
34
34
  }
35
- if (ast.type !== 'select') {
36
- throw new OptimizerError("Security Error: Only SELECT or SHOW statements are allowed.");
37
- }
38
- if (!ast.limit) {
39
- ast.limit = {
40
- seperator: "",
41
- value: [
42
- { type: 'number', value: maxLimit }
43
- ]
44
- };
45
- }
46
- else {
47
- // Check if existing limit exceeds maxLimit
48
- // @ts-ignore
49
- const limitValue = ast.limit.value[0]?.value;
50
- if (typeof limitValue === 'number' && limitValue > maxLimit) {
51
- // @ts-ignore
52
- ast.limit.value[0].value = maxLimit;
35
+ // @ts-ignore
36
+ const type = ast.type?.toLowerCase();
37
+ // Only inject LIMIT and sqlify if it's a SELECT query.
38
+ // For DML/DDL, we just return the original SQL to avoid parser mangling.
39
+ if (type === 'select') {
40
+ const selectAst = ast;
41
+ if (!selectAst.limit) {
42
+ selectAst.limit = {
43
+ seperator: "",
44
+ value: [
45
+ { type: 'number', value: maxLimit }
46
+ ]
47
+ };
48
+ }
49
+ else {
50
+ const limitValue = selectAst.limit.value[0]?.value;
51
+ if (typeof limitValue === 'number' && limitValue > maxLimit) {
52
+ selectAst.limit.value[0].value = maxLimit;
53
+ }
53
54
  }
55
+ return { sql: parser.sqlify(selectAst, astOpt), astType: type };
54
56
  }
55
- return parser.sqlify(ast, astOpt);
57
+ return { sql, astType: type };
56
58
  }
57
59
  catch (e) {
58
60
  if (e instanceof OptimizerError) {
package/dist/db/schema.js CHANGED
@@ -1,5 +1,20 @@
1
1
  import { pool, getDbName } from './index.js';
2
2
  export let schemaGraph = new Map();
3
+ /**
4
+ * Retrieves the raw DDL (CREATE TABLE) statement for a given table.
5
+ */
6
+ export async function getTableDDL(tableName) {
7
+ try {
8
+ const [rows] = await pool.query(`SHOW CREATE TABLE \`${tableName}\``);
9
+ if (rows && rows.length > 0) {
10
+ return rows[0]['Create Table'] || rows[0]['Create View'];
11
+ }
12
+ return null;
13
+ }
14
+ catch (e) {
15
+ return null;
16
+ }
17
+ }
3
18
  /**
4
19
  * Connects to the database and builds the relational graph in-memory.
5
20
  * Optimized for low RAM usage by only extracting node names and edges (no DDL/Columns cached).
package/dist/index.js CHANGED
@@ -4,9 +4,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { registerExecuteQueryTool } from "./tools/executeQuery.js";
5
5
  import { registerSearchSchemaTool } from "./tools/searchSchema.js";
6
6
  import { registerRefreshSchemaTool } from "./tools/refreshSchema.js";
7
- import { registerGetTemplatesTool } from "./tools/getTemplates.js";
7
+ import { registerTemplatesPrompt } from "./prompts/templates.js";
8
+ import { registerTableResources } from "./resources/tables.js";
8
9
  import { buildSchemaGraph } from "./db/schema.js";
9
10
  import { initMetadata } from "./db/metadata.js";
11
+ import { closePool } from "./db/index.js";
10
12
  import dotenv from "dotenv";
11
13
  dotenv.config({ quiet: true });
12
14
  async function main() {
@@ -18,12 +20,27 @@ async function main() {
18
20
  await buildSchemaGraph();
19
21
  // Load Metadata and Templates
20
22
  initMetadata();
21
- // Register MCP Tools
22
- registerSearchSchemaTool(server);
23
- registerExecuteQueryTool(server);
24
- registerRefreshSchemaTool(server);
25
- registerGetTemplatesTool(server);
23
+ // Generate or use provided tool prefix (dev_, prod_ ...)
24
+ let prefix = process.env.TOOL_PREFIX;
25
+ if (!prefix) {
26
+ const randomStr = Math.random().toString(36).substring(2, 6);
27
+ prefix = `db_${randomStr}_`;
28
+ }
29
+ // Register MCP tools & prompts & resources
30
+ registerSearchSchemaTool(server, prefix);
31
+ registerExecuteQueryTool(server, prefix);
32
+ registerRefreshSchemaTool(server, prefix);
33
+ registerTemplatesPrompt(server, prefix);
34
+ registerTableResources(server);
26
35
  const transport = new StdioServerTransport();
27
36
  await server.connect(transport);
37
+ const cleanup = async () => {
38
+ console.error('\n[tokenlite-mysql-mcp] Shutting down server...');
39
+ await server.close();
40
+ await closePool();
41
+ process.exit(0);
42
+ };
43
+ process.on('SIGINT', cleanup);
44
+ process.on('SIGTERM', cleanup);
28
45
  }
29
46
  main().catch(console.error);
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ import { searchTemplates } from "../db/metadata.js";
3
+ export function registerTemplatesPrompt(server, prefix = "") {
4
+ server.prompt(`${prefix}query_templates`, "Official company SQL templates for business metrics. Use this as a starting point to avoid writing SQL from scratch.", {
5
+ query: z.string().optional().describe("Keyword to search for in templates (e.g., 'revenue', 'ltv').")
6
+ }, ({ query }) => {
7
+ const results = searchTemplates(query || "");
8
+ if (results.length === 0) {
9
+ return {
10
+ messages: [
11
+ {
12
+ role: "user",
13
+ content: {
14
+ type: "text",
15
+ text: "No SQL templates found matching your query."
16
+ }
17
+ }
18
+ ]
19
+ };
20
+ }
21
+ let output = "--- PRE-APPROVED SQL TEMPLATES ---\n\n";
22
+ for (const t of results) {
23
+ output += `### ${t.name}\n`;
24
+ output += `Description: ${t.description}\n`;
25
+ output += `SQL:\n\`\`\`sql\n${t.sql}\n\`\`\`\n\n`;
26
+ }
27
+ return {
28
+ messages: [
29
+ {
30
+ role: "user",
31
+ content: {
32
+ type: "text",
33
+ text: output
34
+ }
35
+ }
36
+ ]
37
+ };
38
+ });
39
+ }
@@ -0,0 +1,50 @@
1
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { schemaGraph, getTableDDL } from "../db/schema.js";
3
+ import { getTableSemantics } from "../db/metadata.js";
4
+ export function registerTableResources(server) {
5
+ // List all available tables
6
+ server.resource("schema", "mysql://schema", { description: "Lists all available tables in the database schema" }, async (uri) => {
7
+ const tables = Array.from(schemaGraph.keys());
8
+ if (tables.length === 0) {
9
+ return {
10
+ contents: [{
11
+ uri: uri.href,
12
+ text: "Schema Graph is empty. Make sure the database is connected."
13
+ }]
14
+ };
15
+ }
16
+ return {
17
+ contents: [{
18
+ uri: uri.href,
19
+ text: `Available tables in the database:\n\n${tables.map(t => `- ${t}`).join('\n')}`
20
+ }]
21
+ };
22
+ });
23
+ // DDL for a specific table
24
+ server.resource("table", new ResourceTemplate("mysql://tables/{name}", { list: undefined }), { description: "Exposes the SQL DDL and semantic dictionary of a specific table" }, async (uri, { name }) => {
25
+ const tableName = typeof name === 'string' ? name : String(name);
26
+ const ddl = await getTableDDL(tableName);
27
+ if (!ddl) {
28
+ return {
29
+ contents: [{
30
+ uri: uri.href,
31
+ text: `Table '${tableName}' not found or could not fetch DDL.`
32
+ }]
33
+ };
34
+ }
35
+ let output = `-- === TABLE: ${tableName} ===\n${ddl};\n`;
36
+ // Append Semantics if available
37
+ const semantics = getTableSemantics(tableName);
38
+ if (Object.keys(semantics).length > 0) {
39
+ output += `\n/* SEMANTIC DICTIONARY:\n`;
40
+ output += JSON.stringify(semantics, null, 2);
41
+ output += `\n*/\n`;
42
+ }
43
+ return {
44
+ contents: [{
45
+ uri: uri.href,
46
+ text: output
47
+ }]
48
+ };
49
+ });
50
+ }
@@ -2,32 +2,48 @@ import { z } from "zod";
2
2
  import { executeSafeQuery } from "../db/index.js";
3
3
  import { jsonToCsv } from "../utils/csvFormatter.js";
4
4
  export async function handleExecuteQuery({ sql }) {
5
- if (!sql.trim().toUpperCase().startsWith("SELECT") && !sql.trim().toUpperCase().startsWith("SHOW")) {
6
- return {
7
- content: [{ type: "text", text: "Security Error: Only SELECT or SHOW statements are allowed." }],
8
- isError: true
9
- };
10
- }
11
5
  try {
12
- const rows = await executeSafeQuery(sql);
13
- const csvData = jsonToCsv(rows);
14
- return {
15
- content: [{ type: "text", text: csvData }]
16
- };
6
+ const result = await executeSafeQuery(sql);
7
+ // If it's a SELECT, result are rows
8
+ if (Array.isArray(result)) {
9
+ const csvData = jsonToCsv(result);
10
+ return {
11
+ content: [{ type: "text", text: csvData }]
12
+ };
13
+ }
14
+ // If it's a DML/DDL operation, result is a ResultSetHeader object
15
+ else {
16
+ const header = result;
17
+ const message = `Operation executed successfully.\nAffected rows: ${header.affectedRows || 0}` +
18
+ (header.insertId ? `\nInsert ID: ${header.insertId}` : '') +
19
+ (header.changedRows ? `\nChanged rows: ${header.changedRows}` : '');
20
+ return {
21
+ content: [{ type: "text", text: message }]
22
+ };
23
+ }
17
24
  }
18
25
  catch (error) {
19
26
  let errorMessage = error.name === 'OptimizerError' ? error.message : `Database Error: ${error.message}`;
20
27
  if (error.code === 'ER_BAD_FIELD_ERROR' || error.message?.includes('Unknown column')) {
21
28
  errorMessage += `\n\nHint: If you believe this column exists, the DBA might have just added it. Please call the 'refresh_schema' tool and try again.`;
22
29
  }
30
+ else if (error.code === 'PROTOCOL_SEQUENCE_TIMEOUT' || error.message?.includes('timeout')) {
31
+ errorMessage += `\n\nHint: The query took too long and was aborted to protect the database (DoS protection). Please optimize your query by using better filters, utilizing indexes, or ask the user to increase the MYSQL_QUERY_TIMEOUT.`;
32
+ }
23
33
  return {
24
34
  content: [{ type: "text", text: errorMessage }],
25
35
  isError: true
26
36
  };
27
37
  }
28
38
  }
29
- export function registerExecuteQueryTool(server) {
30
- server.tool("execute_safe_query", "Executes a safe SELECT query on the database. Large results are automatically truncated. CRITICAL: NEVER use this tool (e.g., SHOW TABLES or querying information_schema) to understand the database structure. You MUST ALWAYS use the 'search_schema' tool first to understand the relationships and tables before writing any JOIN queries.", {
39
+ export function registerExecuteQueryTool(server, prefix = "") {
40
+ const allowInsert = process.env.ALLOW_INSERT_OPERATION === 'true';
41
+ const allowUpdate = process.env.ALLOW_UPDATE_OPERATION === 'true';
42
+ const allowDelete = process.env.ALLOW_DELETE_OPERATION === 'true';
43
+ const allowDdl = process.env.ALLOW_DDL_OPERATION === 'true';
44
+ // If any write operation is enabled, this tool is no longer read-only
45
+ const isReadOnly = !(allowInsert || allowUpdate || allowDelete || allowDdl);
46
+ server.tool(`${prefix}execute_safe_query`, "Executes a safe SELECT query on the database. Large results are automatically truncated. CRITICAL: NEVER use this tool (e.g., SHOW TABLES or querying information_schema) to understand the database structure. You MUST ALWAYS use the 'search_schema' tool first to understand the relationships and tables before writing any JOIN queries.", {
31
47
  sql: z.string().describe("SQL SELECT statement to execute."),
32
- }, handleExecuteQuery);
48
+ }, { readOnlyHint: isReadOnly }, handleExecuteQuery);
33
49
  }
@@ -1,6 +1,6 @@
1
1
  import { buildSchemaGraph } from "../db/schema.js";
2
- export function registerRefreshSchemaTool(server) {
3
- server.tool("refresh_schema", "Forces the MCP server to rebuild the internal Schema Graph. Use this if you suspect a DBA recently added a table, column, or foreign key and the search_schema or execute queries are failing.", {}, async () => {
2
+ export function registerRefreshSchemaTool(server, prefix = "") {
3
+ server.tool(`${prefix}refresh_schema`, "Forces the MCP server to rebuild the internal Schema Graph. Use this if you suspect a DBA recently added a table, column, or foreign key and the search_schema or execute queries are failing.", {}, { idempotentHint: true }, async () => {
4
4
  try {
5
5
  await buildSchemaGraph();
6
6
  return {
@@ -1,20 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import Fuse from "fuse.js";
3
- import { pool } from "../db/index.js";
4
- import { schemaGraph } from "../db/schema.js";
3
+ import { schemaGraph, getTableDDL } from "../db/schema.js";
5
4
  import { getTableSemantics } from "../db/metadata.js";
6
- async function getTableDDL(tableName) {
7
- try {
8
- const [rows] = await pool.query(`SHOW CREATE TABLE \`${tableName}\``);
9
- if (rows && rows.length > 0) {
10
- return rows[0]['Create Table'] || rows[0]['Create View'];
11
- }
12
- return null;
13
- }
14
- catch (e) {
15
- return null;
16
- }
17
- }
18
5
  export async function handleSearchSchema({ query }) {
19
6
  if (schemaGraph.size === 0) {
20
7
  return {
@@ -81,13 +68,13 @@ export async function handleSearchSchema({ query }) {
81
68
  if (inferredHints.length > 0) {
82
69
  output += "\n-- === HEURISTIC GRAPH HINTS ===\n" + inferredHints.join("\n");
83
70
  }
84
- output += "\n\n/* ⚠️ CRITICAL REMINDER: If you are asked to calculate business metrics (LTV, revenue, etc.), DO NOT write the SQL manually. You MUST use the `get_query_templates` tool first to fetch the official template. */";
71
+ output += "\n\n/* ⚠️ CRITICAL REMINDER: If you are asked to calculate business metrics (LTV, revenue, etc.), DO NOT write the SQL manually. You MUST use the `query_templates` prompt first to fetch the official template. */";
85
72
  return {
86
73
  content: [{ type: "text", text: output }]
87
74
  };
88
75
  }
89
- export function registerSearchSchemaTool(server) {
90
- server.tool("search_schema", "CRITICAL TOOL FOR SCHEMA EXPLORATION: Use this tool FIRST to understand the database structure. Searches for a table and returns its exact SQL DDL, along with the DDL of its direct parent and child tables (Auto-Join Context). Do NOT use execute_safe_query for schema exploration.", {
76
+ export function registerSearchSchemaTool(server, prefix = "") {
77
+ server.tool(`${prefix}search_schema`, "CRITICAL TOOL FOR SCHEMA EXPLORATION: Use this tool FIRST to understand the database structure. Searches for a table and returns its exact SQL DDL, along with the DDL of its direct parent and child tables (Auto-Join Context). Do NOT use execute_safe_query for schema exploration.", {
91
78
  query: z.string().describe("The name of the table or entity to search for (e.g. 'users', 'invoices')."),
92
- }, handleSearchSchema);
79
+ }, { readOnlyHint: true }, handleSearchSchema);
93
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andezdev/tokenlite-mysql-mcp",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "A secure, efficient, and intelligent MySQL server for the Model Context Protocol",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",