Skip to main content
Glama
Heelc

vmysql-mcp

by Heelc

MySQL Query

mysql_query
Read-onlyIdempotent

Execute read-only SQL queries on MySQL databases through environment-based routing with security constraints. Configure environments like dev or prod_ro, run single statements, and manage execution limits.

Instructions

Run one read-only SQL statement on a configured MySQL environment.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
envYesEnvironment alias such as dev, stg, or prod_ro.
databaseNoOptional database override allowed by server policy.
sqlYesOne read-only SQL statement.
limitNoRequested row limit. The server may clamp it.
timeoutMsNoRequested execution timeout in milliseconds.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
okYes
envYes
rowsYes
columnsYes
databaseNo
rowCountYes
elapsedMsYes
truncatedYes
limitAppliedYes
statementTypeYes

Implementation Reference

  • The main handler function that executes the mysql_query tool logic. It resolves the query context, executes the SQL query with limits, and returns the results with metadata including row count, columns, and execution time.
    async (args) => {
      try {
        const context = resolveQueryContext(registry, args);
        const startedAt = Date.now();
        const result = await pools.query(
          context.env,
          buildLimitedReadSql(
            context.statementType,
            context.normalizedSql,
            context.limitApplied,
          ),
          context.database,
          context.timeoutMs,
        );
        const limitedRows = result.rows.slice(0, context.limitApplied);
        const structuredContent: QueryStructuredContent = {
          ok: true,
          env: context.env,
          database: context.database,
          statementType: context.statementType,
          rowCount: limitedRows.length,
          rows: limitedRows,
          columns: result.fields.map((field) => ({
            name: field.name,
            mysqlType: String(field.columnType),
          })),
          truncated: result.rows.length > context.limitApplied,
          limitApplied: context.limitApplied,
          elapsedMs: Date.now() - startedAt,
        };
    
        return buildSuccessResult(
          `Returned ${structuredContent.rowCount} row(s) from ${context.env}${context.database ? `.${context.database}` : ""} in ${structuredContent.elapsedMs}ms.`,
          structuredContent,
        );
      } catch (error) {
        return buildErrorResult(error, args.env);
      }
    },
  • Input schema definition for mysql_query tool using Zod. Validates environment alias, optional database, SQL statement, row limit, and timeout parameters.
    export const mysqlQueryInputSchema = z.object({
      env: z.string().min(1).describe("Environment alias such as dev, stg, or prod_ro."),
      database: z
        .string()
        .min(1)
        .optional()
        .describe("Optional database override allowed by server policy."),
      sql: z.string().min(1).describe("One read-only SQL statement."),
      limit: z
        .number()
        .int()
        .min(1)
        .optional()
        .describe("Requested row limit. The server may clamp it."),
      timeoutMs: z
        .number()
        .int()
        .min(100)
        .optional()
        .describe("Requested execution timeout in milliseconds."),
    });
  • Output schema builder for mysql_query tool using Zod. Defines the structure for successful query results including rows, columns, metadata, and execution statistics.
    const buildMysqlQueryOutputSchema = (hardMaxRows: number) =>
      z.object({
        ok: z.literal(true),
        env: z.string(),
        database: z.string().optional(),
        statementType: z.enum(["select", "show", "describe", "explain"]),
        rowCount: z.number().int().nonnegative(),
        rows: z.array(z.record(z.string(), z.unknown())).max(hardMaxRows),
        columns: z.array(
          z.object({
            name: z.string(),
            mysqlType: z.string().optional(),
          }),
        ),
        truncated: z.boolean(),
        limitApplied: z.number().int().positive().max(hardMaxRows),
        elapsedMs: z.number().int().nonnegative(),
      });
  • Registration function that registers the mysql_query tool with the MCP server. Configures tool metadata, annotations (readOnly, idempotent), input/output schemas, and attaches the handler function.
    export const registerMysqlQueryTool = (
      server: McpServer,
      registry: EnvironmentRegistry,
      pools: MysqlPoolRegistry,
    ) => {
      const serverConfig = registry.getServerConfig();
    
      server.registerTool(
        "mysql_query",
        {
          title: "MySQL Query",
          description: "Run one read-only SQL statement on a configured MySQL environment.",
          annotations: {
            title: "MySQL Query",
            readOnlyHint: true,
            destructiveHint: false,
            idempotentHint: true,
            openWorldHint: false,
          },
          inputSchema: mysqlQueryInputSchema,
          outputSchema: buildMysqlQueryOutputSchema(serverConfig.hardMaxRows),
        },
        async (args) => {
          try {
            const context = resolveQueryContext(registry, args);
            const startedAt = Date.now();
            const result = await pools.query(
              context.env,
              buildLimitedReadSql(
                context.statementType,
                context.normalizedSql,
                context.limitApplied,
              ),
              context.database,
              context.timeoutMs,
            );
            const limitedRows = result.rows.slice(0, context.limitApplied);
            const structuredContent: QueryStructuredContent = {
              ok: true,
              env: context.env,
              database: context.database,
              statementType: context.statementType,
              rowCount: limitedRows.length,
              rows: limitedRows,
              columns: result.fields.map((field) => ({
                name: field.name,
                mysqlType: String(field.columnType),
              })),
              truncated: result.rows.length > context.limitApplied,
              limitApplied: context.limitApplied,
              elapsedMs: Date.now() - startedAt,
            };
    
            return buildSuccessResult(
              `Returned ${structuredContent.rowCount} row(s) from ${context.env}${context.database ? `.${context.database}` : ""} in ${structuredContent.elapsedMs}ms.`,
              structuredContent,
            );
          } catch (error) {
            return buildErrorResult(error, args.env);
          }
        },
      );
    };
  • Query context resolver that validates SQL statements for mysql_query. Ensures only read-only statement types (SELECT, SHOW, DESCRIBE, EXPLAIN) are allowed, validates database permissions, and applies row limits and timeouts.
    export const resolveQueryContext = (
      registry: EnvironmentRegistry,
      args: QueryArgs,
    ): ResolvedExecutionContext & {
      normalizedSql: string;
      statementType: "select" | "show" | "describe" | "explain";
      limitApplied: number;
    } => {
      const environment = registry.getEnvironment(args.env);
      const inspection = inspectSql(args.sql);
    
      if (!inspection.normalizedSql) {
        throw createError("SQL_EMPTY", "SQL cannot be empty.", false);
      }
      if (inspection.hasMultipleStatements) {
        throw createError(
          "SQL_MULTI_STATEMENT",
          "Only one SQL statement is allowed per call.",
          false,
        );
      }
      if (inspection.hasLeadingWithClause && inspection.statementType !== "select") {
        throw createError(
          "SQL_NOT_READ_ONLY",
          "Only top-level WITH ... SELECT statements are allowed in mysql_query.",
          false,
        );
      }
      if (!isReadOnlyType(inspection.statementType)) {
        throw createError(
          "SQL_NOT_READ_ONLY",
          `Statement type '${inspection.mainKeyword || inspection.firstKeyword || "unknown"}' is not allowed in mysql_query.`,
          false,
        );
      }
    
      const database = args.database ?? environment.defaultDatabase;
      assertAllowedDatabase(environment, database);
      assertReferencedDatabasesAllowed(environment, inspection.normalizedSql);
    
      const serverConfig = registry.getServerConfig();
      const envMaxRows = Math.min(environment.maxRows, serverConfig.hardMaxRows);
      const requestedLimit = args.limit ?? serverConfig.defaultQueryLimit;
      const limitApplied = Math.max(1, Math.min(requestedLimit, envMaxRows));
    
      return {
        env: args.env,
        database,
        environment,
        timeoutMs: resolveTimeout(
          args.timeoutMs,
          environment.defaultTimeoutMs,
          serverConfig.hardTimeoutMs,
          serverConfig.defaultTimeoutMs,
        ),
        normalizedSql: inspection.normalizedSql,
        statementType: inspection.statementType,
        limitApplied,
      };
    };
Behavior4/5

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

The description adds valuable context beyond annotations by specifying 'one read-only SQL statement', which clarifies behavioral constraints not covered by annotations (e.g., single statement limitation). Annotations already cover safety (readOnlyHint=true, destructiveHint=false), so the description appropriately supplements with execution details without contradiction.

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 the core purpose ('Run one read-only SQL statement') and includes essential context ('on a configured MySQL environment'). There is no wasted wording, making it highly concise and well-structured for quick comprehension.

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

Completeness5/5

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

Given the tool's complexity (SQL querying), the description is complete enough when combined with annotations (which cover safety and idempotency) and the presence of an output schema (handling return values). It effectively sets expectations for read-only, single-statement execution without needing to detail outputs or advanced behaviors.

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?

With 100% schema description coverage, the input schema fully documents all parameters. The description does not add any parameter-specific details beyond what's in the schema, such as examples or usage tips. This meets the baseline for high schema coverage but doesn't enhance parameter understanding.

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

Purpose5/5

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

The description clearly states the action ('Run'), the resource ('one read-only SQL statement'), and the context ('on a configured MySQL environment'). It specifically distinguishes from the sibling tool 'mysql_exec' by emphasizing 'read-only', which helps differentiate between query and execution operations.

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

Usage Guidelines5/5

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

The description explicitly states when to use this tool: for 'read-only SQL statement[s]'. This implies that for write operations or multiple statements, the sibling tool 'mysql_exec' should be used instead, providing clear alternatives and context for tool selection.

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/Heelc/vmysql-mcp'

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