MCP server
Limits and troubleshooting
The guard rails that stop an agent loop draining a balance — spend caps, confirmation, topping up, rate limits, retry and cache behaviour — and what each failure actually means.
An agent calls tools in a loop, and a tool that spends money in a loop is a different risk from an HTTP client a human is driving. The server is built around that difference: spend caps, confirmation, a rate limit, careful retries, and errors that say what they mean.
Spending#
| Guard | Default | What it does |
|---|---|---|
| Session call budget | 50 billable calls | Billing stops for the session past this. Free tools never count |
| Confirmation threshold | 20 billable calls | Past this, every further billable call asks you to confirm first |
| Response cache | 60 seconds | An identical request inside the window is served locally instead of billed twice |
| Per-attempt timeout | 28 seconds | Below the upstream 30s budget, so a slow call returns a clean error rather than a dead socket |
| Low-balance warning | 100 credits | At or below this balance, responses say so and suggest topping up. A warning only: it never opens a payment page |
The budget counts calls, not credits:
- Each billed response states what it cost (
meta.credits_charged), and the assistant and the card show that figure, never an estimate. A call served from the response cache shows 0, because it was not billed again. - The cap stays a count of calls because a call is what the assistant decides to make; what a call will cost is known only once it has answered.
- The running spend for the session is measured from your balance before and after.
- The balance is re-read before a billable call whenever the last reading is more than a minute old (the credits endpoint is free), so the figure moves as you spend. Credits bought mid-session are reported as added rather than as negative spend.
Confirmation
Past the threshold, the server asks through MCP elicitation: the protocol's own ask-the-user mechanism.
- It fails closed. In a client with no elicitation support the call is refused rather than spent. A confirmation nobody can answer must not become an implicit yes.
sprouts_research_runathigh,xhighorautoeffort always asks, whatever the session's call count. Those are the tiers where one call can cost more than a whole session of searches. The confirmation names the price, which is held when the run starts and charged only if the run succeeds: a run that fails or that you cancel costs nothing.
Running out of credits#
A billable call that fails with 402 credits.insufficient can turn into a payment you can make, instead of a dead end. What happens depends on your host:
The failed call raises a Pay-now consent dialog, drawn by the host with the domain on it. You sign in and pay in the browser, and the server notices the payment and tells the client, so the call that failed can be retried.
- The 402 never grows a payment link in its text. A URL goes only behind a host's consent dialog or through the tool you asked to use.
- Only
credits.insufficientqualifies. A402for a declined card is a different problem, and a top-up page would not fix it. - The low-balance warning works the other way round. It informs and never interrupts, because a payment prompt on every call under a threshold would be an advertisement, not a warning.
Rate limit#
60 tool calls per minute per credential. Only tools/call is metered. Protocol chatter (listing tools, reading resources, the handshake) is free, because rate-limiting discovery would make a client look broken before it had done any work. The bucket belongs to the credential's identity, so refreshing a sign-in token does not reset it.
Past the limit, the server answers the request itself, before any tool runs:
- an HTTP
429with aRetry-Afterheader, in seconds - a JSON-RPC error with
code: -32000and a message saying how long to wait data: { code: "quota.burst_exceeded", retry_after_seconds }, the same code the HTTP API uses for the same condition, so a client can branch ondata.coderather than on the message
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Rate limit reached on this server: wait 12s and retry. This protects your credit balance as much as the service — do not tighten the loop.",
"data": { "code": "quota.burst_exceeded", "retry_after_seconds": 12 }
},
"id": null
}Nothing ran and nothing was charged. Wait for Retry-After, then send the call again. The limit is surfaced rather than swallowed on purpose: a server that silently slept would look like a hang. How it reaches the model depends on your client.
Retries and pagination#
- Only
503and504are retried, once, with the sameIdempotency-Key. They are almost always answered before any charge. The exception is503 credits.unavailable, where the billing service could not confirm a charge and the call may have been billed; because the retry carries the same key, a retry that returns the same rows is not charged again. A402is commercial and will never succeed on a retry; a429carries its own wait. - A dropped connection on a billed call is never retried. The server cannot know whether the search already ran and was charged, so a retry could bill you twice. You get a clear error instead. Free calls (coverage, credits, usage, filters) are retried, because there is nothing to double-charge.
- One call, one page. No tool ever paginates internally. Continuing is a visible decision, made with a cursor, by the model or by you from a card after confirming it. Each further page is charged for the results it adds, which is exactly why it should not happen behind your back. A page bought from a card goes through the same budget, confirmation and cache as the model's own calls.
Sessions#
A session is opened by the initialize handshake and identified by Mcp-Session-Id thereafter.
- It is bound to the credential that opened it. A request presenting someone else's session id is refused, and every cache, held record and rate-limit bucket is scoped to the credential, so one hosted process serving many tenants keeps them fully separate. That holds for an API key and an OAuth token alike: the binding is to the credential, not to which kind it is.
- Sessions expire after 30 idle minutes. An expired or unknown id is answered with an instruction to re-initialize, which well-behaved clients do transparently.
Troubleshooting#
Tools do not appear
Restart the client fully: most only re-read MCP config on a cold start.
If you passed ?tools=, remember it replaces the defaults. A set that omits sprouts_search really will not serve it.
401 auth.invalid_key
The key is unknown, revoked or expired, or it was issued by a different Sprouts deployment.
Check the endpoint and the key together: a key from elsewhere fails exactly like a bad one, because to this host it is one. Retrying will not help. Create a key at API Keys, which issues keys for the endpoint these docs describe.
403 auth.insufficient_scope
The key is valid but lacks the scope that tool needs. Scopes are granted independently, so search working is no evidence Record Lookup will.
Not a sign-in failure: re-authenticating will not clear it.
403 auth.browser_origin
Our bug, not yours. It means an Origin header reached the API alongside a key, which the server must never forward. Report it with the request_id.
402 credits.insufficient
Out of credits. Commercial, and never worth a retry until you have topped up.
Ask the assistant to top up, which opens a payment page (see Running out of credits), or top up under Billing.
429 quota.burst_exceeded
Over 60 tool calls in a minute on this credential. The response carries Retry-After, and data.retry_after_seconds says the same thing. Well-behaved agents wait it out rather than tightening the loop. See Rate limit.
A category is missing from the schema
The category enum is built from live coverage, so a tool only advertises what this deployment can actually answer. If a category is absent, it is not indexed here yet. Ask sprouts_coverage, which is free and will say so.
The sign-in flow loops, or the tools never appear after consent
The commonest cause is a connector added against one deployment and consented on another. The token is bound to the server that issued it, by exact URL, and will not be accepted anywhere else.
Remove the connector, re-add it with the endpoint at the top of these pages, and consent again. A token that is well-formed but bound elsewhere is refused for the same reason a key from another deployment is.
401 after a period of working, on a signed-in connector
The access token expired and the refresh did not complete. Clients handle this silently when they can; when they cannot, disconnecting and reconnecting the connector reauthorizes it.
Nothing is lost: consent is per client, not per session.
A card does not render
See Inline cards. The server falls back to full text, so nothing is lost but the layout.