Report MCP tool rate limits with retry detail
What does this MR do and why?
The GitLab MCP server's tools call API endpoints that may be rate limited. When throttled, the endpoint sets a Retry-After header, but this was discarded before anything could read it. The tool result contained only "This endpoint has been requested too many times. Try again later." A client could not tell a throttle apart from a 404 or a 500, so it retried immediately and stayed throttled.
This exposes the retry detail in the tool result. Text content, which is what the model reads and self-corrects from:
Rate limited by search_rate_limit. Retry after 60 seconds.structuredContent, keeping the existing error envelope so nothing else changes shape:
{
"error": {
"type": "rate_limited",
"retry_after_seconds": 60,
"limit": "search_rate_limit",
"message": "This endpoint has been requested too many times. Try again later."
}
}Changes:
app/services/mcp/tools/base/api_tool.rbre-dispatches into the Grape route in process.status, _, body = route.app.call(request.env)threw the headers away. It now captures them and passes them toprocess_response.app/services/mcp/tools/base/api_service.rbdid the same for real HTTP responses inhandle_response, readingresponse.codebut ignoringresponse.headers.- New shared module
app/services/mcp/tools/base/rate_limited_response.rb, included by both, so every tool reports a throttle identically regardless of which path it took. Mcp::Tools::Base::Response.rate_limited_errorbuilds the result.lib/api/helpers/rate_limiter.rbsets aRateLimit-Nameheader naming the throttle key that tripped. The key was already known at the point of failure but was never emitted.RateLimit-Nameis an existing convention, already set by Rack::Attack inlib/gitlab/rack_attack/request_throttle_data.rb.- A troubleshooting section was added to
doc/user/model_context_protocol/mcp_server_troubleshooting.md.
Design points worth stating:
- This stays a tool execution error with
isError: trueand HTTP 200, not a transport 429. The MCP specification lists API failures as tool execution errors and says clients SHOULD pass them to the model so it can self-correct. A transport 429 would move it into the category clients are less likely to surface usefully. - The response's
RateLimit-*headers are deliberately not set from the inner endpoint. They stay reserved for the MCP Server's own limit, otherwise one response would carry two different limits under the same header names and a client could not tell which it was reading. retry_after_secondsis the full rate limit period, not the time remaining in the window. Someone throttled five seconds into a sixty second window is told to wait sixty. It is a safe over-estimate, not a precise one.- No
resets_atfield, becauseApplicationRateLimiteremits no reset time, so it would benow + intervalpresented as though it were authoritative. - Aggregated tools are covered without extra work. An aggregated tool routes to exactly one sub-tool and returns its response, and
enhance_response_with_operationreturns early on an error response, so a throttled result is never rewritten. There is a regression test pinning this.
References
Closes #629635 (closed)
Related to #629885 (closed)
Screenshots or screen recordings
There is no UI change. The client-visible change is the JSON result shape shown above, which now carries type, retry_after_seconds and limit under the existing error envelope.
How to set up and validate locally
- Lower a rate limit, such as the search rate limit, in the Admin settings.
- Call the MCP
searchtool past that limit. - Confirm the result carries
retry_after_secondsandlimitinstructuredContentrather than only prose. - Repeat through the aggregated
searchtool to confirm it is not flattened.
Specs, all run locally and passing:
spec/services/mcp/tools/base/response_spec.rb, 37 examplesspec/services/mcp/tools/base/api_tool_spec.rbandapi_service_spec.rb, 70 examples togetherspec/services/mcp/tools/base/aggregated_service_spec.rb, 14 examplesspec/lib/api/helpers/rate_limiter_spec.rb, 22 examples- Full sweep of
spec/services/mcp/andspec/requests/api/mcp/, 2024 examples, 0 failures
Coverage includes the headers being absent, header casing variations, and a non JSON body.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.