Skip to main content
Glama
JoeWithGlide

MySQL MCP Server

by JoeWithGlide
index.ts5.23 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import mysql from "mysql2/promise"; import { readFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; // Load .env from script directory (not cwd) so it works when spawned by MCP clients const __dirname = dirname(fileURLToPath(import.meta.url)); const envPath = join(__dirname, "..", ".env"); try { const envContent = readFileSync(envPath, "utf-8"); for (const line of envContent.split("\n")) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith("#")) { const [key, ...valueParts] = trimmed.split("="); if (key && valueParts.length > 0) { process.env[key.trim()] = valueParts.join("=").trim(); } } } } catch { // .env file doesn't exist, will check required vars below } // Validate required environment variables const requiredEnvVars = ["DB_HOST", "DB_USER", "DB_PASSWORD", "DB_NAME"] as const; for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { console.error(`Missing required environment variable: ${envVar}`); console.error("Create a .env file with DB_HOST, DB_USER, DB_PASSWORD, and DB_NAME"); process.exit(1); } } // Create MySQL connection pool using environment variables from .env file const pool = mysql.createPool({ host: process.env.DB_HOST!, user: process.env.DB_USER!, password: process.env.DB_PASSWORD!, database: process.env.DB_NAME!, ssl: { rejectUnauthorized: true }, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }); /** * Validates that a query is safe to execute (SELECT-only). * Uses a blacklist approach to block dangerous SQL operations. */ function isSafeQuery(query: string): boolean { const q = query.trim().toLowerCase(); // Must start with SELECT if (!q.startsWith("select")) return false; // Block multiple statements (but allow semicolons inside quoted strings) // Simple check: if there's a semicolon not inside quotes, reject // For safety, we'll just check if there's a semicolon followed by non-whitespace const withoutStrings = q.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, '""'); if (withoutStrings.includes(";")) return false; // Block SQL comments (potential injection vectors) if (withoutStrings.includes("--") || withoutStrings.includes("/*")) return false; // Block dangerous keywords - must be whole words (using word boundaries) // These keywords should not appear as standalone SQL commands const forbidden = [ /\binsert\b/, /\bupdate\b(?!\s*\()/, // Allow UPDATE() function but not UPDATE statement /\bdelete\b(?!\s*\()/, // Allow DELETE() function but not DELETE statement /\bdrop\b/, /\bcreate\b(?!\s*\()/, // Allow CREATE() function but not CREATE statement /\balter\b/, /\btruncate\b/, /\breplace\b(?!\s*\()/, // Allow REPLACE() function but not REPLACE statement /\bgrant\b/, /\brevoke\b/, /\bset\b(?!\s*\()/, // Allow SET() function but not SET statement /\buse\b(?=\s+\w)/, // Block USE database but allow "use" in other contexts /\bcommit\b/, /\brollback\b/, /\block\s+tables?\b/, /\bunlock\s+tables?\b/, /\bcall\b/, /\bexecute\b/, /\bprepare\b/, /\bdeallocate\b/, /\bload\b/, /\binto\s+outfile\b/, /\binto\s+dumpfile\b/, ]; return !forbidden.some((pattern) => pattern.test(withoutStrings)); } // Create MCP server const server = new McpServer({ name: "mysql-readonly", version: "1.0.0", }); // Register the execute_sql tool server.registerTool( "execute_sql", { description: "Executes a SELECT-only SQL query on the MySQL database. Only read operations are allowed - no INSERT, UPDATE, DELETE, or other modifying statements.", inputSchema: { query: z.string().describe("The SELECT SQL query to execute"), }, }, async ({ query }) => { // Validate query safety if (!isSafeQuery(query)) { return { content: [ { type: "text" as const, text: JSON.stringify({ error: "Only SELECT queries are allowed. The query must not contain modifying statements, semicolons, or SQL comments.", }), }, ], isError: true, }; } try { const [rows] = await pool.query(query); return { content: [ { type: "text" as const, text: JSON.stringify({ rows }, null, 2), }, ], }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown database error"; return { content: [ { type: "text" as const, text: JSON.stringify({ error: message }), }, ], isError: true, }; } } ); // Start the server with stdio transport async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MySQL MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JoeWithGlide/mysql-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server