apache/shardingsphere · error · MCPBannedSQLStatementException

Statement is banned by the MCP contract.

Error message

Statement is banned by the MCP contract.

What it means

SQLStatementSafetyValidator.checkLeadingStatement bans entire statement families before parsing: executable comments, and SQL starting with USE, SET, COPY, LOAD, CALL, or ALTER SYSTEM throw MCPBannedSQLStatementException ('Statement is banned by the MCP contract.'). These statements alter session/connection state, bulk-load data, invoke procedures, or reconfigure the server, so they are excluded outright (note metadata-introspection statements get their own distinct exception). The walker also inspects parsed statements for banned types and banned side-effecting functions.

Source

Thrown at mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementSafetyValidator.java:54

import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dml.SelectStatement;

import java.util.List;
import java.util.Locale;

final class SQLStatementSafetyValidator {
    
    private static final List<String> SIDE_EFFECTING_FUNCTION_NAMES = List.of("NEXTVAL", "NEXT VALUE FOR", "SETVAL", "GET_LOCK", "RELEASE_LOCK", "RELEASE_ALL_LOCKS",
            "PG_ADVISORY_LOCK", "PG_ADVISORY_XACT_LOCK", "PG_TRY_ADVISORY_LOCK", "PG_TRY_ADVISORY_XACT_LOCK", "PG_ADVISORY_UNLOCK", "PG_ADVISORY_UNLOCK_ALL",
            "SET_CONFIG", "PG_REPLICATION_SLOT_ADVANCE", "PG_LOGICAL_SLOT_GET_CHANGES", "PG_LOGICAL_SLOT_GET_BINARY_CHANGES", "PG_LOGICAL_EMIT_MESSAGE", "PG_SWITCH_WAL",
            "PG_RELOAD_CONF", "PG_CANCEL_BACKEND", "PG_TERMINATE_BACKEND");
    
    private static final List<String> METADATA_LOOKUP_FUNCTION_NAMES = List.of("TO_REGCLASS", "TO_REGTYPE", "TO_REGPROC", "TO_REGPROCEDURE", "TO_REGOPER",
            "TO_REGOPERATOR", "TO_REGNAMESPACE", "TO_REGROLE", "OBJECT_ID");
    
    void checkLeadingStatement(final String upperSql, final boolean executableComment) {
        if (executableComment || upperSql.startsWith("USE ") || upperSql.startsWith("SET ") || upperSql.startsWith("COPY ") || upperSql.startsWith("LOAD ")
                || upperSql.startsWith("CALL ") || isAlterSystemStatement(upperSql)) {
            throw new MCPBannedSQLStatementException();
        }
        if (isMetadataIntrospectionStatement(upperSql)) {
            throw new MetadataIntrospectionSQLStatementException(extractStatementType(upperSql));
        }
    }
    
    void checkParsedStatement(final SQLStatement sqlStatement) {
        new SQLStatementTreeWalker(this::checkStatement, this::checkExpression).walk(sqlStatement);
    }
    
    private boolean isBannedStatementType(final SQLStatement sqlStatement) {
        return sqlStatement instanceof SetStatement || sqlStatement instanceof CallStatement
                || sqlStatement instanceof CreateUserStatement || sqlStatement instanceof AlterUserStatement || sqlStatement instanceof DropUserStatement
                || sqlStatement instanceof CreateRoleStatement || sqlStatement instanceof AlterRoleStatement || sqlStatement instanceof DropRoleStatement;
    }
    
    private boolean containsExecutableComment(final SQLStatement sqlStatement) {
        for (CommentSegment each : sqlStatement.getComments()) {

View on GitHub (pinned to e952770a21)

Solutions

  1. Remove SET/USE/COPY/LOAD/CALL/ALTER SYSTEM statements from MCP-bound SQL; configure session settings at the datasource.
  2. Select databases/schemas via the tool's database and schema arguments instead of USE.
  3. Strip executable comment directives (/*! ... */) from dump-derived SQL before sending.
  4. Invoke stored procedures through your own database client, not the MCP tools.

Example fix

// before
await tools.call('database_gateway_execute_update', { sql: 'SET FOREIGN_KEY_CHECKS=0; TRUNCATE t;', execution_mode: 'execute' }); // banned leading SET

// after
await tools.call('database_gateway_execute_update', { sql: 'TRUNCATE t', execution_mode: 'execute' }); // FK checks managed on the datasource
Defensive patterns

Strategy: validation

Validate before calling

const BANNED_PREFIX_RE = /^\s*(USE|SET|COPY|LOAD|CALL)\b|^\s*ALTER\s+SYSTEM\b/i;
const EXECUTABLE_COMMENT_RE = /\/\*!|\/\*\s*\+/;
function screenBannedSql(sql) {
  const stripped = sql.replace(/\/\*[!+]?.*?\*\//gs, '').trim();
  if (EXECUTABLE_COMMENT_RE.test(sql)) throw new Error('Executable comments are banned by the MCP contract');
  if (BANNED_PREFIX_RE.test(stripped)) throw new Error(`Statement family banned by the MCP contract: ${stripped.slice(0, 20)}`);
}
screenBannedSql(sql);

Type guard

function isBannedLeadingStatement(sql) {
  const s = sql.trimStart().toUpperCase();
  return s.startsWith('USE ') || s.startsWith('SET ') || s.startsWith('COPY ') || s.startsWith('LOAD ')
      || s.startsWith('CALL ') || s.startsWith('ALTER SYSTEM ') || /^\/\*[!+]/.test(sql.trimStart());
}

Try / catch

try {
  return await tools.call('database_gateway_execute_update', { sql, execution_mode });
} catch (e) {
  if (/banned by the MCP contract/.test(e.message)) {
    // strip banned leading statements / executable comments, keep the rest, retry once
    const cleaned = sql.replace(/\/\*[!+]?.*?\*\//gs, '').replace(/^\s*(USE|SET|COPY|LOAD|CALL)\b[^;]*;?\s*/i, '');
    return cleaned ? tools.call('database_gateway_execute_update', { sql: cleaned, execution_mode }) : undefined;
  }
  throw e;
}

Prevention

When it happens

Trigger: Sending SET foreign_key_checks=0, USE mydb, COPY t FROM stdin, LOAD DATA INFILE ..., CALL proc(), ALTER SYSTEM ..., or SQL prefixed with an executable comment (/*! ... */) to any MCP SQL execution tool.

Common situations: Dump/restore scripts replayed through MCP; ORM session-setup statements (SET names/timeout); stored-procedure invocations; MySQL dumps full of /*!40001 ... */ executable comments; attempting schema switching via USE instead of the schema argument.

Related errors


AI-assisted analysis of apache/shardingsphere@e952770a21 (2026-08-14). Data as JSON: /api/errors/7f883de31d520ad7. Report an issue: GitHub.