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
- Send read-only statements to database_gateway_execute_query instead.
- Pre-classify SQL client-side by leading keyword and pick the tool accordingly.
- Use the exception's suggested arguments to reroute to execute_query automatically.
- 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
- Default SELECT/WITH reads to execute_query; reserve execute_update for mutations.
- Use the mismatch exception's suggested tool/arguments for automatic rerouting.
- Include execution_mode only on the update tool.
- Test the router against your dialect's statement forms.
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
- database_gateway_execute_query only supports parser-approved
- Cross-schema SQL is not supported for database `%s`: `%s`.
- Statement is banned by the MCP contract.
- Completion argument `%s` is not declared for %s `%s`.
- %s execution_mode is required.
AI-assisted analysis of apache/shardingsphere@e952770a21 (2026-08-14).
Data as JSON: /api/errors/dde5e1cde9dae11d.
Report an issue: GitHub.