apache/shardingsphere · error · SQLToolMismatchException

database_gateway_execute_update does not accept read-only SQ

Error message

database_gateway_execute_update does not accept read-only SQL. Use database_gateway_execute_query for read-only SQL.

What it means

The mirror of the execute_query guard: database_gateway_execute_update classifies the SQL first, and if it is a query statement it throws SQLToolMismatchException telling the caller to use database_gateway_execute_query. This prevents using the side-effecting tool (and its preview/execute modes) for read-only SQL, keeping read and write paths separate in the MCP contract. The exception includes suggested arguments pointing at execute_query.

Source

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

    
    @Override
    public MCPSuccessPayload handle(final MCPFeatureRequestContext requestContext, final Map<String, Object> arguments) {
        MCPToolArguments toolArguments = new MCPToolArguments(arguments);
        String executionMode = resolveExecutionMode(toolArguments);
        SQLExecutionToolHandlerSupport.checkExecutionArguments(toolArguments, CoreToolNames.EXECUTE_UPDATE);
        String sql = toolArguments.getStringArgument("sql");
        ClassificationResult classificationResult = checkUpdateStatement(requestContext, toolArguments, sql);
        if (EXECUTION_MODE_PREVIEW.equals(executionMode)) {
            return createPreviewResponse(toolArguments, classificationResult);
        }
        return SQLExecutionPayload.executed(requestContext.getExecutionFacade().execute(
                SQLExecutionToolHandlerSupport.createExecutionRequest(requestContext.getSessionIdentity().getSessionId(), toolArguments, sql, CoreToolNames.EXECUTE_UPDATE)));
    }
    
    private ClassificationResult checkUpdateStatement(final MCPFeatureRequestContext requestContext, final MCPToolArguments toolArguments, final String sql) {
        ClassificationResult classificationResult = SQLExecutionToolHandlerSupport.analyze(requestContext, toolArguments, sql);
        if (SQLExecutionToolHandlerSupport.isQueryStatement(classificationResult)) {
            throw new SQLToolMismatchException("database_gateway_execute_update does not accept read-only SQL. Use database_gateway_execute_query for read-only SQL.",
                    CoreToolNames.EXECUTE_UPDATE, CoreToolNames.EXECUTE_QUERY, classificationResult,
                    createQuerySuggestedArguments(toolArguments, classificationResult));
        }
        return classificationResult;
    }
    
    private String resolveExecutionMode(final MCPToolArguments toolArguments) {
        String result = toolArguments.getStringArgument(MCPPayloadFieldNames.EXECUTION_MODE);
        if (result.isEmpty()) {
            throw new MCPExecutionModeRequiredException(CoreToolNames.EXECUTE_UPDATE, EXECUTION_MODES, createPreviewSuggestedArguments(toolArguments));
        }
        if (EXECUTION_MODE_EXECUTE.equals(result) || EXECUTION_MODE_PREVIEW.equals(result)) {
            return result;
        }
        throw new MCPInvalidExecutionModeException(CoreToolNames.EXECUTE_UPDATE, EXECUTION_MODES, createPreviewSuggestedArguments(toolArguments));
    }
    
    private MCPSuccessPayload createPreviewResponse(final MCPToolArguments toolArguments, final ClassificationResult classificationResult) {

View on GitHub (pinned to e952770a21)

Solutions

  1. Send read-only statements to database_gateway_execute_query instead.
  2. Pre-classify SQL client-side by leading keyword and pick the tool accordingly.
  3. Use the exception's suggested arguments to reroute to execute_query automatically.
  4. For EXPLAIN, use database_gateway_execute_explain_query.

Example fix

// before
await tools.call('database_gateway_execute_update', { sql: 'SELECT * FROM t', execution_mode: 'execute' }); // SQLToolMismatchException

// after
await tools.call('database_gateway_execute_query', { sql: 'SELECT * FROM t' });
Defensive patterns

Strategy: validation

Validate before calling

const isReadOnly = /^\s*(SELECT|WITH)\b/i.test(sql) && !/\b(INSERT|UPDATE|DELETE|MERGE)\b/i.test(sql);
if (isReadOnly) {
  return tools.call('database_gateway_execute_query', { sql });
}
return tools.call('database_gateway_execute_update', { sql, execution_mode: mode });

Type guard

function isQueryStatementSql(sql) {
  const upper = sql.trimStart().toUpperCase();
  if (upper.startsWith('SELECT')) return true;
  return upper.startsWith('WITH') && !/\b(INSERT|UPDATE|DELETE|MERGE)\b/.test(upper);
}

Try / catch

try {
  return await tools.call('database_gateway_execute_update', { sql, execution_mode: mode });
} catch (e) {
  if (e.name === 'SQLToolMismatchException' && e.suggestedTool === 'database_gateway_execute_query') {
    return tools.call('database_gateway_execute_query', { sql });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling database_gateway_execute_update (any execution_mode) with SELECT, WITH-CTE reads, or other parser-approved query statements.

Common situations: Agent defaulting to the 'update' tool for all SQL; scripts that always pass execution_mode:'execute' regardless of statement type; EXPLAIN-free reports mistakenly sent through the update path.

Related errors


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