Here is the short answer to "what should I expose in an MCP server." Expose reads broadly. Expose writes narrowly, and only as the signed-in customer with that customer's permissions, never through a shared service account. Anything with a side effect you would not want an unattended agent to run at 3am does not belong in the raw tool list. It belongs behind a bounded job that you design, guarantee, and log.
The rest is the why, and a tiering you can apply to your own product.
The fear is not tokens. It is headless control.
The first debate when a software company builds an MCP server is usually token cost. That is the wrong debate. Cost is solved: Cloudflare's Code Mode MCP puts roughly 2,500 endpoints behind two tools, and we covered the tool-count failure in a previous post.
The real fear, from the vendor's chair, is liability. Once your product is reachable over MCP, any agent holding a valid token can drive it headlessly. Not just the well-behaved one in Claude or ChatGPT. Whatever the customer wired up, including the one currently reading an email from a stranger.
In May 2025, Invariant Labs showed that an issue filed on a public repository could steer a developer's agent, connected through the GitHub MCP server, into reading private repositories and publishing the contents in a public pull request. The server was not buggy. The agent had legitimate access to both, and untrusted text told it what to do with that access.
In July 2025, General Analysis showed the same shape against Supabase's MCP server. A support ticket carried hidden instructions. The agent, running with service_role privileges that bypass row-level security, read the full integration_tokens table and inserted the results into the ticket thread, where the attacker could read them. Supabase responded with read-only mode, project scoping, and feature groups, and said plainly that these reduce the risk without eliminating it.
Simon Willison named the pattern the lethal trifecta: access to private data, exposure to untrusted content, and a way to communicate externally. A write tool is the third ingredient. A write is a channel out.
Neither incident involved the vendor's agent. The customer's own tooling hurt the customer, and the vendor got the headline. That is the liability.
Three kinds of tools, not two
Most people split tools into reads and writes. Split them three ways.
Reads change nothing. A read can still leak, so "read-only" is necessary and not sufficient, but its blast radius is bounded by what the caller could already see.
Additive writes create something new: a draft, a note, an unpurchased label. Usually reversible, rarely cascading.
Destructive writes change or remove something that exists: cancel a subscription, delete a customer, issue a refund. These cascade, are often irreversible, and end up in a dispute with your name on it.
The MCP schema encodes this. Tool annotations carry readOnlyHint (default false), destructiveHint (default true when not read-only), and idempotentHint (default false). Clients act on them: ChatGPT's developer mode treats any tool without readOnlyHint as a write and requires confirmation by default. So annotate honestly.
Then read the sentence most people skip: "Clients should never make tool use decisions based on ToolAnnotations received from untrusted servers." Annotations are advice to the client, not enforcement. If a destructive tool exists, assume it will be called.
Act as the signed-in customer, never a shared service account
The detail in the Supabase incident that matters most is service_role. The agent ran with a credential that bypassed every row-level policy. Whatever the customer could not see, the agent could.
The rule: every call an agent makes into your product runs as a specific signed-in user with that user's permissions, so the worst an injected agent can do is what that user could already do by hand. No shared API key. No integration user with admin rights. No "broad scope because in theory it might need it."
The MCP authorization spec is built for this. Since the 2025-06-18 revision, an MCP server is an OAuth 2.1 resource server. Tokens must be audience-bound with RFC 8707 resource indicators, and token passthrough to an upstream API is explicitly forbidden. The security best practices add scope minimization: start with a minimal read scope, elevate with a targeted WWW-Authenticate challenge when a privileged tool is first attempted, and treat wildcard scopes as a mistake. Cloudflare's optional OAuth scopes, shipped in August 2026, exist for the same reason: servers request everything an agent might theoretically need, and the person consenting wants to grant less.
This is the part raw MCP gets right. Per-user, audience-bound, minimally scoped tokens are the correct identity model, and they are now the spec's default.
Side effects need three things a tool schema cannot give you
Once a write exists, three properties decide whether you can defend it.
Idempotency. Agents retry. Connections drop mid-call. A tool that creates a refund must be safe to call twice. Stripe's idempotency keys are the reference design: the client sends a unique key, the server saves the first result for 24 hours, and repeats return the same result instead of creating a second object. Build this into every write tool, because the model will not.
Approvals. Some actions should pause for a human regardless of who asked. Stripe's answer is instructive: a restricted key can be tagged as an agent key, and agent-tagged keys are automatically subject to approval rules with defaults covering refunds, subscription cancellations, payouts, and outbound transfers. The API returns approval_required, a reviewer decides, and requests expire after 14 days. Anthropic's Claude in Chrome did the same on the client side, confirming before publishing, purchasing, or sharing personal data; with other mitigations, measured prompt injection success fell from 23.6% to 11.2%.
Approvals have a failure mode. Wojciech Wentland's case for read-only MCP servers puts it well: after confirming 50 harmless operations, people stop reading, and the 51st destroys an instance. Approvals work when rare and specific, which means most writes must be bounded enough not to need one.
Audit. Every side effect needs a record of which client, which user, which tool, which arguments, and what happened. The spec's token passthrough section explains why passthrough breaks this: the downstream log shows an identity other than the one that acted. If you cannot answer "who did this, on whose behalf" in one query, you cannot stand behind the write.
Why vendors will sanction bounded jobs long before raw write primitives
Put those three requirements next to a primitive like update_subscription. A primitive says: any field, any value, any time, by any agent holding a token. You cannot make that meaningfully idempotent, you cannot decide in advance which calls deserve approval, and the audit record is "someone changed something."
Now describe the same capability as a job: "Cancel a subscription at period end, with a reason from this list, for a customer the caller owns, and post the confirmation to the billing email." That is idempotent by construction, the approval rule is one line, and the run record is a complete story.
Every vendor makes this trade, named or not. Stripe kept the primitives and wrapped the dangerous ones in approvals. The step the same logic forces next is to publish the bounded version as the thing agents call, and keep the raw primitive for humans and trusted integrations.
The reason is not that models are bad at writes. It is that the vendor is the one who gets sued, churned, or paged. Vendors sanction what they can guarantee.
The test for what belongs behind a service
A service is a named outcome the vendor defines on top of the product: fixed inputs, an allowlist of tools, limits, and a run log. A request needs one instead of a tool call when it needs any of four ingredients the buyer's agent cannot get from primitives, however cheap transport gets.
- Privileged data. Cross-tenant intelligence, internal systems, anything the public API never exposes. A benchmark against similar accounts is a service. One account's numbers are a read.
- Write authority with accountability. Side effects the vendor will only permit with guardrails and audit. A refund is a service. Reading the refund policy is a read.
- Residency. Work that happens when the customer's agent is not running: nightly, on a schedule, or when an event fires in your system.
- Amortized reliability. Tested once, run thousands of times on the cheapest model that passes. This erodes as models improve and must never be the only reason.
Ingredient two is this post. A tool schema asks the model to decide whether a write is safe. A service asks the vendor to decide, once, with a run log to prove the answer.
A practical tiering
Tier 1: public reads. Anything the signed-in customer can already see in your UI. Expose freely with per-user OAuth and readOnlyHint: true.
- Shipping platform: list orders, get shipment status, list carriers and rates, get tracking events.
- Billing product: get customer, list invoices, get subscription, list payment methods.
Tier 2: scoped, additive, idempotent writes. Writes that create something reversible inside the caller's own tenant, idempotency key required, the customer's own permissions enforced. Expose as tools, annotated destructiveHint: false.
- Shipping platform: create a draft order, add a note to a shipment, create an unpurchased label.
- Billing product: create a draft invoice, add a usage record, create a payment link.
Tier 3: vendor-run jobs with guarantees. Destructive or financial side effects, cross-record operations, scheduled work, anything needing an approval, a limit, or a receipt. Do not expose the primitive. Expose the job.
- Shipping platform: "reship a damaged order" (creates the return, buys the replacement label, notifies the recipient, capped per account per day), "weekly reorder forecast across the warehouse."
- Billing product: "cancel at period end with reason and confirmation email" (approval above a revenue threshold), "dunning review for accounts 30 days past due, delivered to finance every Monday."
A tier 3 job is small to declare. Most of its value is in what it refuses to accept:
service: cancel_subscription_at_period_end
inputs: { subscription_id, reason: [too_expensive, missing_feature, switching, other] }
tools_allowed: [get_subscription, update_subscription, send_email]
identity: signed_in_customer
limits: { per_account_per_day: 3 }
approval: required_if_mrr_over: 500
audit: full_run_logThe agent cannot pass a field it was not given, exceed the cap, or skip the email. The vendor wrote those rules once. The model does not get a vote.
What raw MCP access does well
For tiers 1 and 2, tools are the correct unit: the long tail of "show me X" and "add a note" is enormous, and nobody should predefine a job for each. A developer's own agent composes primitives well. A thin MCP server over a well-permissioned API inherits most of that API's safety for free.
The line is tier 3: anything you would not want to explain to the customer afterward.
Where Aimdoc sits
Aimdoc runs one customer agent for a software product: on the website, inside the app, over email, and through a public gateway for other agents. It connects to the product's MCP server, and every call runs as the signed-in customer with their permissions. Services are the tier 3 layer: named outcomes on top of the product, each with fixed inputs, allowed tools, limits, identity, entitlements, an optional price through the vendor's own Stripe account, delivery to app, email, Slack, or webhook, and scheduled or event-triggered runs. Tiers 1 and 2 stay as tools. The free tier is 10,000 credits, no credit card.
The short version
Reads: expose them, as the signed-in user, annotated honestly.
Additive writes: expose them, idempotent, scoped to the caller's tenant, with an audit record.
Destructive and financial writes: do not expose the primitive. Design the job, bound its inputs, decide the approval rule once, log every run, and let agents call that.
Transport is going to zero. What you sell is not the tool list. It is the outcome you are willing to put your name on.
Want to see the tiering in practice? Read how the agent inside your product does the work, define your first service, or book a demo.