Skip to main content
Glama
zhaojw-php

MySQL ReadOnly MCP Server

by zhaojw-php

mysql_query

Execute read-only SQL SELECT queries on MySQL databases to retrieve data securely with built-in SQL injection protection and query limits.

Instructions

Execute a read-only SQL query on MySQL database

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesSQL query to execute (SELECT statements only)

Implementation Reference

  • Handler for the 'mysql_query' tool. Validates the query is read-only using isReadOnlyQuery, adds an automatic LIMIT clause with addAutoLimit, executes the query via MySQLConnection.executeQuery, and returns the JSON-formatted results.
    case 'mysql_query': {
      const { query } = args as { query: string };
    
      // Validate that it's a read-only query
      if (!isReadOnlyQuery(query)) {
        throw new Error('Only SELECT statements are allowed for security reasons');
      }
    
      // Apply automatic LIMIT to prevent large result sets
      const safeQuery = addAutoLimit(query);
    
      // Log if LIMIT was added for transparency
      if (safeQuery !== query.trim()) {
        console.error(`Added automatic LIMIT to query for safety: ${query}`);
      }
    
      const result = await mysqlConnection.executeQuery(safeQuery);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(result, null, 2),
          },
        ],
      };
    }
  • src/index.ts:32-45 (registration)
    Registration of the mysql_query tool in the ListTools response, including name, description, and input schema definition.
    {
      name: 'mysql_query',
      description: 'Execute a read-only SQL query on MySQL database',
      inputSchema: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: 'SQL query to execute (SELECT statements only)',
          },
        },
        required: ['query'],
      },
    },
  • Helper function isReadOnlyQuery used in the handler to validate that the provided SQL query is safe (SELECT only, no dangerous keywords, no injection patterns).
    export function isReadOnlyQuery(query: string): boolean {
      const trimmedQuery = query.trim();
    
      // Check if query starts with SELECT (case-insensitive)
      if (!trimmedQuery.toUpperCase().startsWith('SELECT')) {
        return false;
      }
    
      // Additional security checks
    
      // 1. Check for multiple statements using semicolons
      const semicolonCount = (trimmedQuery.match(/;/g) || []).length;
      if (semicolonCount > 1) {
        return false;
      }
    
      // 2. If there's a semicolon, ensure nothing follows it
      if (semicolonCount === 1) {
        const parts = trimmedQuery.split(';');
        // If there's content after the semicolon, reject
        if (parts.length > 2 || (parts.length === 2 && parts[1].trim().length > 0)) {
          return false;
        }
      }
    
      // 3. Check for dangerous SQL keywords (case-insensitive)
      const dangerousKeywords = [
        'DROP', 'DELETE', 'UPDATE', 'INSERT', 'CREATE', 'ALTER',
        'TRUNCATE', 'EXEC', 'EXECUTE', 'UNION', 'MERGE', 'REPLACE',
        'CALL', 'DO', 'HANDLER', 'LOAD', 'RENAME', 'GRANT', 'REVOKE'
      ];
    
      const upperQuery = trimmedQuery.toUpperCase();
      for (const keyword of dangerousKeywords) {
        // Check if dangerous keyword appears after SELECT (not in string literals)
        const regex = new RegExp(`\\b${keyword}\\b`, 'i');
        if (regex.test(upperQuery)) {
          return false;
        }
      }
    
      // 4. Check for SQL injection patterns
      const injectionPatterns = [
        /--/, // SQL comments
        /\/\*/, // Multi-line comment start
        /\*\//, // Multi-line comment end
        /xp_/, // SQL Server extended procedures
        /sp_/, // SQL Server stored procedures
      ];
    
      for (const pattern of injectionPatterns) {
        if (pattern.test(trimmedQuery)) {
          return false;
        }
      }
    
      // 5. Check for MySQL-specific dangerous functions and clauses
      const dangerousPatterns = [
        { pattern: /\bLOAD_FILE\s*\(/i, name: 'LOAD_FILE' },
        { pattern: /\bINTO\s+OUTFILE\b/i, name: 'INTO OUTFILE' },
        { pattern: /\bINTO\s+DUMPFILE\b/i, name: 'INTO DUMPFILE' },
        { pattern: /\bSYSTEM\s*\(/i, name: 'SYSTEM' },
        { pattern: /\bSLEEP\s*\(/i, name: 'SLEEP' }
      ];
    
      for (const { pattern, name } of dangerousPatterns) {
        if (pattern.test(trimmedQuery)) {
          return false;
        }
      }
    
      return true;
    }
  • Helper function addAutoLimit used in the handler to automatically append a LIMIT clause to queries without one, preventing large result sets.
    export function addAutoLimit(query: string, defaultLimit: number = 1000): string {
      const trimmedQuery = query.trim();
      const upperQuery = trimmedQuery.toUpperCase();
    
      // Don't add LIMIT if query already has one
      if (upperQuery.includes(' LIMIT ')) {
        return trimmedQuery;
      }
    
      // Don't add LIMIT to COUNT(*) queries or other aggregate functions without LIMIT
      if (/COUNT\s*\([^)]+\)/i.test(trimmedQuery)) {
        return trimmedQuery;
      }
    
      // Don't add LIMIT to EXISTS or NOT EXISTS subqueries
      if (/EXISTS\s*\(/i.test(trimmedQuery) || /NOT\s+EXISTS\s*\(/i.test(trimmedQuery)) {
        return trimmedQuery;
      }
    
      // Don't add LIMIT if query ends with a semicolon (handle it gracefully)
      if (trimmedQuery.endsWith(';')) {
        return trimmedQuery.slice(0, -1) + ` LIMIT ${defaultLimit};`;
      }
    
      // Add LIMIT clause
      return `${trimmedQuery} LIMIT ${defaultLimit}`;
    }
  • Core query execution method in MySQLConnection class, called by the mysql_query handler to run the validated SQL query using a connection pool.
    async executeQuery(query: string): Promise<QueryResult> {
      if (!this.pool) {
        throw new Error('MySQL connection pool not initialized');
      }
    
      let connection: mysql.PoolConnection | undefined;
      try {
        // Get connection from pool
        connection = await this.pool.getConnection();
        const [rows, fields] = await connection.execute(query);
    
        return {
          rows: Array.isArray(rows) ? rows : [],
          fields: fields || undefined,
        };
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown query error';
        throw new Error(`Query execution failed: ${errorMessage}`);
      } finally {
        // Always release the connection back to the pool
        if (connection) {
          connection.release();
        }
      }
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It specifies 'read-only' and mentions 'SELECT statements only,' which are useful constraints. However, it lacks details on permissions, rate limits, error handling, or result format, leaving gaps in behavioral context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads key information ('execute a read-only SQL query') without any wasted words. It's appropriately sized for the tool's complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations and no output schema, the description provides basic purpose and constraints but is incomplete. It doesn't cover behavioral aspects like authentication needs, result structure, or error scenarios, which are important for a database query tool. Adequate but with clear gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, with the input schema fully documenting the 'query' parameter as 'SQL query to execute (SELECT statements only).' The description adds no additional meaning beyond this, as it restates the same constraint. Baseline 3 is appropriate when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('execute a read-only SQL query') and resource ('MySQL database'), providing a specific verb+resource combination. However, it doesn't explicitly differentiate from sibling tools like mysql_describe_table or mysql_list_tables, which also involve database operations but for different purposes.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for read-only SQL queries, particularly SELECT statements, but doesn't provide explicit guidance on when to use this tool versus alternatives like mysql_list_tables for listing tables or mysql_describe_table for table structure. No exclusions or clear alternatives are mentioned.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/zhaojw-php/mysql-readonly-mcp'

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