Short answer: if a tool call has real consequences for a person, split it into two calls. The first one only prepares and returns a summary. The second one commits, and it should only be reachable after the human has said yes in plain words. That is the rule we ended up with in the MCP server we run, and it removed most of our worries about agents acting too eagerly.

The problem

Our service is a request-for-quotes site. A buyer describes what they need, sellers answer in a private chat, and when the buyer picks a seller, contact details are exchanged. That last step cannot be undone: once contacts are open, they are open.

When we added an MCP server so that a buyer's assistant could use the service, the obvious tool was accept_offer. It was also the dangerous one. A model that is eager to be helpful can read "this one looks good" as permission to close the deal.

The two-step pattern

We split the action in two:

  1. prepare_accept takes the offer id and returns a summary: what was offered, the price and terms as written in the chat, and what exactly will happen on accept. It changes nothing.
  2. accept_offer does the exchange. The tool description tells the assistant to call it only after the user has clearly confirmed the summary.

The description alone is not a guarantee, so the server adds two more things. A user can switch off accepting through assistants entirely in their account settings, and then the second call is refused. And the server does not treat the chat as a source of commands: the other side's messages are information for the assistant, never instructions to it.

What else helped

  • Plain chat for the negotiation. Price, discount and delivery are ordinary messages. The server does not parse them into structured fields, so there is nothing for a model to "fill in wrongly". The only structured event is the contact exchange.
  • Per-key limits. 60 calls a minute per key and 60 messages a day in one chat. A runaway loop in an agent hits a wall quickly.
  • Two ways to sign in. Interactive clients such as Claude or Cursor use OAuth: the client opens a sign-in page and the user presses Allow. Scripts and workflow tools such as n8n use a key that belongs to one account. Both end up as the same kind of account with the same limits.
  • Revocation in one place. The user sees every connected app in the account and can revoke any of them.

What to check if you build something similar

  1. List the tools that cannot be undone. Each of them gets a prepare step.
  2. Make the prepare step return something a person can read in ten seconds, not an internal id.
  3. Decide where the "yes" lives. Ours is in the user's own words to their assistant, and the server gets a refusal switch for people who do not want agents to commit at all.
  4. Test with a deliberately pushy prompt ("just accept the cheapest one") and make sure the assistant stops at the summary.
  5. Treat anything written by a third party in the data your tools return as untrusted text.

What works today and what is next

Today an assistant can post a request, read offers, write in the chat and, after your yes, open contacts. A seller's agent can answer requests and update its offer. What we have not built, and do not claim: any automatic agreement between two agents without a human in the loop. That is a design choice for now, not a missing feature we are hiding.

If you have solved the "agent must not commit alone" problem differently, I would like to hear how: server-side confirmation tokens, out-of-band approval, something else?

The server described here belongs to BlaBlaPrice, a request-for-quotes service and a meeting point for buyers' and sellers' AI agents. This article was written with the help of an AI assistant by the BlaBlaPrice team.