Rightshold™ Sign up

Answers · MCP security

How do I make MCP rate-limit and quota responses consistent across denial and empty-result paths?

Last updated

TL;DR: Agents probe policy by comparing responses. If a denied tools/call returns HTTP 429 with RateLimit-Remaining: 0 while an empty-result success returns 200 with Remaining: 47, or if a policy deny returns isError: true with "quota exceeded" while a not-found returns a bare empty content array, the model can map which tools, tenants, or rows your gate treats specially. Pick one quota counter and one response shape for "slow down / over budget" and use it on every path that counts against the same limit — authz deny that still consumes a slot, empty business results, and hard throttle alike. Put the same Retry-After (or retryAfterSeconds) and remaining/limit fields in headers and in the JSON the model sees. Do not leak whether the reason was policy, emptiness, or throttle in fields the agent is allowed to read; keep that distinction in server-only logs.

What should I check, step by step?

1. Inventory every path that can answer a tools/call: success with data, success with empty content, tool execution error (isError: true), JSON-RPC protocol error, and transport HTTP 429/401/403.

  1. Decide which of those paths consume quota. Charge the same bucket for "ran and empty" and "denied by policy after auth" if both reveal that the credential was accepted and the tool name was valid — otherwise an agent learns the difference by watching remaining drop on one path only.
  2. Define a single over-limit envelope: same HTTP status strategy per MCP method family, same RateLimit-* / Retry-After headers, and the same machine-readable retryAfter (seconds) in JSON-RPC error data or in the isError text the model reads.
  3. Follow MCP's two error classes deliberately: protocol/JSON-RPC errors for malformed or unknown tools; tool results with isError: true for execution and budget failures the model should see. Do not mix "quota exceeded" into an empty successful result.
  4. Normalize denial vs empty: when you must hide whether a row exists or a policy blocked it, return the same body, headers, and timing band for both (constant-shape empty or constant-shape error), and log the real reason only server-side.
  5. Never put different remaining, limit, or reset values on denial vs empty for the same credential and window. Agents compare those counters across probes.
  6. Test with a scripted client: call the same tool under (a) throttle, (b) policy deny, (c) empty dataset, and assert status, header names, remaining, and retryAfter match your matrix. Diff response bodies with a schema, not by eye.
  7. Record allow/deny and which envelope you emitted with record_ai_run (hashes of tool name, decision class, remaining, retryAfter) outside the agent session so you can audit side-channel fixes without giving the model the reason codes.

Why denial and empty must not disagree on quota

MCP Security Considerations for tools tell servers to rate-limit invocations and sanitize outputs. Rate limits only help if the throttle signal is not itself a free oracle about policy or existence.

A practical consistency matrix

Transport throttle (burst / concurrency): prefer HTTP 429 with Retry-After plus RateLimit-Limit/Remaining/Reset (or your gateway's x-ratelimit-* equivalents) before tool code runs. Say the same retry seconds in any JSON body you also return.

What can go wrong?

Policy deny returns 403 with Remaining: 0 while empty search returns 200 with Remaining: 12 — the agent maps which queries your ACL cares about.

How do I ask Rightshold about this (free, no key)?

Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Record a signed receipt for this MCP rate-limit decision: hashes of the tool name, decision class (throttle|policy_deny|empty|ok), remaining, limit and retryAfter I returned — without putting the internal reason code in the agent session.”

One-click setup

{
  "tool": "record_ai_run",
  "arguments": {
    "model": "<mcp-server-or-gateway-id>",
    "provider": "mcp-quota",
    "prompt_sha256": "<sha256 of tool|credential_id|window>",
    "output_sha256": "<sha256 of decision_class|remaining|limit|retry_after_sec>",
    "params": {
      "tool": "search_tickets",
      "decision_class": "throttle",
      "remaining": 0,
      "limit": 60,
      "retry_after_sec": 20,
      "http_status": 200,
      "is_error": true
    },
    "ran_at": "2026-11-06T20:00:00Z"
  }
}

Rightshold records signed run receipts and spend decisions for agents that call it. It does not sit in your MCP gateway, emit RateLimit headers, or stop a server that returns different remaining values on deny vs empty. Enforce the consistency matrix in your gateway and tool handlers. Not legal advice.

What are the sources?

Related

Not legal advice. JSON · Markdown · All answers

Get a free API key in one call

No card and no email needed. 100 free credits; the license and policy checks stay free without a key.

curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_...   # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
  -H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'