Rate limiting an MCP server per tool rather than per connection
A connection is a transport detail a client can reset by reconnecting. The unit worth counting is the tool, scoped to the account whose resources the call spends.

Count calls per tool and per account, not per connection. A single MCP session mixes tools whose cost is a database read with tools that spend a third-party quota, write to a customer's live website or charge money, and one counter across the session has to be set for one of those and will be wrong for the others. Set it low and the agent cannot read anything; set it high and nothing restrains the calls that matter. The tool is the unit where the cost is knowable, so the tool is where the limit belongs.
A connection is a transport detail, and transport details reset. A client that drops a socket and reconnects gets a fresh counter, and reconnecting is something MCP clients do automatically after a network hiccup or a restart. An agent can also hold two sessions at once without doing anything unusual. A limit keyed to the connection is therefore a limit an ordinary client evades by accident, which is worse than no limit at all because it looks like protection on a diagram.
The identity worth counting against is the one that owns the resources being consumed: the account, the organisation, or whichever record holds the connected credentials. That identity survives a reconnection, covers two parallel sessions, and matches the thing that will be billed or throttled by the vendor at the other end. A per-account counter also gives an honest answer to the question a refusal should answer, which is not "why did this fail" but "who spent it and when does it come back".
A per-tool limit protects a different resource for each class of tool, and naming the resource is what tells you the period and the number. A tool that reads the server's own database protects nothing but the server itself, so its limit is a capacity limit and can be generous. A tool that reads a vendor API protects a quota somebody else defined. A tool that writes to a public page protects the customer's reputation. A tool that spends money protects the budget, and its limit is a safety limit rather than a capacity one.
| What the call touches | Resource at risk | Sensible period |
|---|---|---|
| The server's own data, read | Server capacity | Per minute, generous |
| The server's own data, written | Data integrity and audit noise | Per minute, moderate |
| A vendor API, read | The vendor's quota, often per property or key | Per day, matched to the vendor's window |
| A public surface, written | What customers and crawlers see | Per day, deliberately small |
| Money | The budget, and trust | Per run, with an approval above it |

When an assistant works through dozens of your tools in a single turn, there is no transaction around them. Everything before the failure has already happened, and the turn can still end by reporting success.

Market4 turns one release note into a changelog page, a blog post, a mail-out and a week of social posts — and then tells you which of them brought anyone back.
Deciding a limit per tool requires knowing what each tool costs, which is a fact the tool definition should already carry. Market4's MCP server annotates every tool with one of nine cost classes — whether it reads or writes, creates or removes, spends money, and whether the target is our own data or the world outside — so the cost class is declared next to the tool rather than inferred from its name. Names are a poor guide here: a tool called get_pagespeed reaches out to a metered service, and a tool called create_blog_post touches nothing but the local database.
An annotation is a declaration rather than an enforcement, which is the point at which teams stop. The value of writing the class down is that a limit can then be attached to the class instead of to each of a hundred tools by hand, and a new tool arrives already carrying the rule that applies to it. A tool whose annotation and behaviour disagree is also a reviewable bug rather than an invisible one.
Read tools that touch only the server's own storage need a capacity limit rather than a safety one, and it can be generous. The exception is a read that reaches a third party, because that read spends someone else's quota and the cost has nothing to do with how heavy the query is locally. Treat those as metered calls even though the verb is read.
Which limit was reached, what the limit is, and when capacity returns. An agent handed a bare error retries, and retrying is the behaviour a limit exists to stop. A refusal that names the window converts a retry loop into a wait. Where the limit protects a vendor quota, saying so also tells the reader that the constraint is not the server being cautious.
No, and a product usually needs both. A rate limit bounds how often something can happen; an approval gate decides whether one particular call happens at all. Spending is the case where they meet: a sensible design refuses more than a few spending calls in a run and still asks a person before each of them. A gate with no limit behind it can be worn down by repetition.
Usually without trying. Reconnecting resets anything counted on the connection, and MCP clients reconnect on their own after network interruptions or restarts. A client can also hold more than one session. Both make a per-connection counter unreliable in ordinary use, which is why the counter belongs on the account that owns the credentials being spent.
Five outcomes an agent-facing tool can produce, and what an agent should do with each. The dangerous one is not failure; it is a response that describes a decision rather than the work.

A result too large to return has three cheaper fixes before persistence: send the delta instead of the state, a count instead of a list, and a budgeted answer that says it was budgeted.