{"record":{"id":"42fbe7905876a646","repo":"apache/shardingsphere","slug":"mcp-session-exceeded-the-maximum-tool-call-quota-o","errorCode":null,"errorMessage":"MCP session exceeded the maximum tool call quota of %d.","messagePattern":"MCP session exceeded the maximum tool call quota of (.+?)\\.","errorType":"validation","errorClass":"MCPToolCallLimitExceededException","httpStatus":null,"severity":"error","filePath":"mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/MCPToolCallLimiter.java","lineNumber":48,"sourceCode":" */\npublic final class MCPToolCallLimiter {\n    \n    private final int maxToolCallsPerSession = MCPRuntimeProtectionPolicy.getMaxToolCallsPerSession();\n    \n    private final Map<String, AtomicInteger> sessionToolCallCounts = new ConcurrentHashMap<>();\n    \n    /**\n     * Acquire one tool call budget slot.\n     *\n     * @param sessionId session identifier\n     * @param toolName tool name\n     * @throws MCPToolCallLimitExceededException when session tool call quota is exhausted\n     */\n    public void acquire(final String sessionId, final String toolName) {\n        String actualSessionId = normalizeSessionId(sessionId);\n        int callCount = sessionToolCallCounts.computeIfAbsent(actualSessionId, ignored -> new AtomicInteger()).incrementAndGet();\n        if (callCount > maxToolCallsPerSession) {\n            throw new MCPToolCallLimitExceededException(actualSessionId, Objects.toString(toolName, \"\"), maxToolCallsPerSession);\n        }\n    }\n    \n    /**\n     * Release all tracked budget state for one session.\n     *\n     * @param sessionId session identifier\n     */\n    public void releaseSession(final String sessionId) {\n        sessionToolCallCounts.remove(normalizeSessionId(sessionId));\n    }\n    \n    private String normalizeSessionId(final String sessionId) {\n        String result = Objects.toString(sessionId, \"\").trim();\n        return result.isEmpty() ? \"anonymous\" : result;\n    }\n}\n","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/apache/shardingsphere/blob/e952770a215630a3659c75d64369168cd3e26b82/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/MCPToolCallLimiter.java#L30-L66","documentation":"MCPToolCallLimiter counts tool invocations per session with an AtomicInteger; when a session's count exceeds maxToolCallsPerSession it throws MCPToolCallLimitExceededException with the session id, tool name, and quota. The quota is a runtime-protection cap that bounds how much work one MCP session can trigger. releaseSession(sessionId) clears the counter, so a closed session's budget is freed.","triggerScenarios":"More than maxToolCallsPerSession tools/call requests through a single session id, e.g. an agent loop calling database_gateway_execute_query hundreds of times without re-initializing, or a runaway client retry storm.","commonSituations":"Autonomous agent loops with no call budget; low operator-configured quota for the deployment; session left open across many batch iterations; retries after transient errors consuming budget.","solutions":["Start a new session (initialize) to obtain a fresh tool call budget, or reduce the number of tool calls by batching SQL.","Track calls client-side and rotate the session before hitting maxToolCallsPerSession.","Ask the operator to raise maxToolCallsPerSession if the workload legitimately needs more calls.","Fix retry logic that hammers the same tool on failure."],"exampleFix":"// before\nfor (const sql of oneThousandStatements) {\n  await mcp.callTool(sessionId, 'database_gateway_execute_update', { sql, execution_mode: 'execute' });\n} // blows the per-session quota\n\n// after: batch statements and rotate sessions\nfor (let i = 0; i < oneThousandStatements.length; i += BATCH) {\n  if (callsThisSession + BATCH > QUOTA) sessionId = await mcp.initialize();\n  await mcp.callTool(sessionId, 'database_gateway_execute_update', batchArgs(oneThousandStatements.slice(i, i + BATCH)));\n}","handlingStrategy":"validation","validationCode":"// Track calls per session and rotate before the quota\nlet callsThisSession = 0;\nasync function budgetedCall(tool, args) {\n  if (callsThisSession >= MAX_TOOL_CALLS_PER_SESSION) {\n    sessionId = (await mcp.initialize()).sessionId;\n    callsThisSession = 0;\n  }\n  callsThisSession++;\n  return mcp.callTool(sessionId, tool, args);\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await mcp.callTool(sessionId, tool, args);\n} catch (e) {\n  if (/maximum tool call quota/.test(e.message)) {\n    sessionId = (await mcp.initialize()).sessionId; // fresh budget\n    return mcp.callTool(sessionId, tool, args);\n  }\n  throw e;\n}","preventionTips":["Budget tool calls client-side; rotate sessions before reaching the quota.","Batch multiple statements into fewer execute_update calls.","Cap retry attempts so failures don't burn quota.","Ask operators to size maxToolCallsPerSession to the real workload."],"tags":["mcp","quota","tool-calls","rate-limit","session"],"backgroundTag":null,"analyzedSha":"e952770a215630a3659c75d64369168cd3e26b82","analyzedAt":"2026-08-14T13:54:53.392Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}