The API Habits We Built for Frontends May Be Confusing Our Agents
Translate backend APIs one-to-one into MCP tools and an agent gets a wall of buttons with no interface. From UI constraints and stateful business Actions to the 2026-07 MCP spec: how agent-facing tools should be designed.
We may have wired up MCP the wrong way from the start.
As the industry begins to put agents into core business systems, a counterintuitive pattern keeps showing up: what holds agents back is often not model intelligence, but a decade of muscle memory from building APIs for frontends.
Engineering teams are very good at hard-coding every step of a flow, and much less practiced at giving an AI a place to say what it is actually trying to accomplish. Translate your backend APIs one-to-one into MCP tools, and the agent usually ends up facing a wall of buttons with no interface.
When MCP first appeared, the obvious move was this: we already have hundreds of backend endpoints built for web and mobile, so write a script, generate hundreds of MCP tools, and mount them all. Frontend APIs were designed to be fine-grained and single-purpose. Two hundred endpoints became two hundred switches, neatly lined up.
But in domains like orders, refunds, and approvals, a model needs something different: a handful of business entry points that fire based on the situation.
Once a local agent starts running real workflows, this long-trusted habit shows its cracks. Before the backend was connected, the model reasoned and conversed just fine. After mounting two hundred fine-grained tools, even basic tasks like looking up an order or changing its status start failing and retrying.
The model did not suddenly get worse. We forgot one thing: an LLM is not a frontend.
Why would an API that works smoothly inside an app become a liability when handed to an AI as-is? Because a layer of invisible constraint is missing: the UI.
The UI has been filtering state for us all along
Take after-sales support. Following the page logic, the backend naturally splits into:
- Refund only (not yet shipped)
- Return and refund (after delivery)
- Request an exchange
- Refund shipping difference
- Change delivery address
- Track shipment
Real users rarely tap the wrong one, because once an order is in the "shipped" state, the app has already hidden or greyed out "refund only (not yet shipped)".
The frontend has quietly been doing state filtering and input validation on behalf of the business.
Translate those seven or eight endpoints directly into MCP tools and the UI that hid the buttons is gone. Similarly named, adjacent tools now sit side by side in the context, and the model has to decide which to call first and which next, with no state-based guidance at all.
An LLM is fundamentally a probabilistic model. The frontend used to filter out illegal states; now those implicit business rules are left to the model's inference. With a clean context it will usually pick correctly, which creates the impression that the system works. Once the conversation grows long and noise builds up, it may well take an order that is still in transit and call "refund only (not yet shipped)".
In a demo, that is an occasional hallucination. In production, it is a financial loss.
This is not just anecdote. An empirical study of 116 official MCP servers (From REST to MCP) found that 88.6% are fully or partially REST-backed, and 92% implement tools as bare API wrappers. A separate benchmark, LongFuncEval, shows that function-calling performance of long-context models drops as the number of available tools grows, by as much as 85% on some models (the observed range is 7% to 85%).
The problem is not REST itself. Fine-grained endpoints remain valuable in a microservice architecture. The mismatch is at the abstraction layer: fine-grained APIs suit frontends, where deterministic page code composes a full flow; for multi-step core business processes, an agent-facing tool is better off expressing the currently available capability, its state constraints, and the next possible action.
This is increasingly a shared view. Anthropic's engineering blog recommends merging tools that are commonly chained together, so that a single tool can handle multiple API calls under the hood. Block, drawing on more than 60 MCP servers, likewise argues for designing top-down from the workflow rather than exposing granular endpoints.
But "consolidate your tools" only answers half the question. For stateful, side-effecting flows like checkout, refunds, and approvals, what should the consolidated tool actually look like? What happens when information is missing halfway through? Who stops the charge? How do you recover after a timeout? That is the part this post wants to explore.
Layer one: don't show the whole API catalog up front
The industry has already started subtracting from the "flatten every API for the agent" approach.
OpenAI's Tool Search gives the model a lightweight catalog first, then loads the full definitions of relevant tools into context on demand, and recommends grouping deferred functions by namespace or MCP server.
Grok Bot and Muse put more emphasis on the execution environment. Grok Bot runs on a persistent cloud computer and reaches external services through MCP-based connectors, handing control back to the user for things like local commands, team accounts, payments, or identity checks. Muse runs on a dedicated, isolated cloud computer, keeps credentials in secure storage where the model cannot see them, pays with one-time card numbers, and asks before purchases.
The details differ, but they point to a shared conclusion: an agent should not see the entire backend API surface at the start.
Still, Tool Search mainly addresses discovery and loading, while connectors and controlled runtimes mainly address the execution boundary. For stateful, side-effecting core flows such as checkout, refunds, approvals, and payments, there is one more step worth taking: redefining the smallest business unit the agent sees.
Layer two: from chained API calls to a stateful Action
Designing APIs for a frontend and designing tools for an AI follow different contracts. The former splits operations into small pieces and lets the page orchestrate the flow. The latter needs to pull the state machine scattered across pages back in, and wrap an entire business intent into an Action that can advance, pause, ask for confirmation, and recover.
Take placing an order. With REST-wrapped tools, the agent has to chain the whole sequence itself, inside its context:
Query products → Select product → Query addresses → Select address → Check inventory → Calculate shipping → Query coupons → Select payment method → Create order → Create payment → Submit payment → Query order
This chain is brittle: a wrong parameter or inconsistent state at any step can break the whole thing.
If we collapse it into a stateful CheckoutAction, then after starting the action the model receives a self-contained business context, including a list of everything the flow needs to get from the user:
{
"action_id": "checkout-123",
"entity": "Order",
"node": "ITEMS",
"items": [
{ "id": "item-1", "name": "Product A", "quantity": 1 }
],
"addresses": [
{ "id": "address-1", "label": "Office" },
{ "id": "address-2", "label": "Home" }
],
"payment_methods": [
{ "type": "balance", "label": "Account balance" },
{ "type": "card", "label": "Bank card" }
],
"inputs": [
{ "field": "items[].quantity", "node": "ITEMS", "required": true },
{ "field": "address_id", "node": "ADDRESS", "required": true },
{ "field": "coupon_id", "node": "COUPON", "required": false, "depends_on": ["items", "address_id"] },
{ "field": "payment_type", "node": "PAYMENT", "required": true }
],
"allowed_patch": ["items[].quantity", "address_id", "coupon_id", "payment_type"],
"requires_confirmation": false,
"next_actions": [
{ "tool": "advance_action", "description": "Fill in or change the order fields listed in allowed_patch" }
],
"version": 3
}
One convention runs through the rest of this post: every response must carry next_actions, whether it comes from search, start, advance, confirm, verify, or recover. It tells the model explicitly which tools it may call next and what each one does. An empty next_actions means the flow is finished, and any call not on the list is rejected by the server. The model does not have to scan the tool list and guess its next move; it chooses among the options the server gives it.
inputs describes the workflow end to end: what each node needs, which fields are required, and which depend on upstream results. The agent does not need to know how many microservices sit behind this, and it does not need to ask one field per page like a web form. When the user says "two of these, ship to the office, pay from my balance," the model can extract every field at once and submit them together:
{
"action_id": "checkout-123",
"version": 3,
"patch": {
"items": [{ "id": "item-1", "quantity": 2 }],
"address_id": "address-1",
"payment_type": "balance"
}
}
On receiving the patch, the server processes it in node order: update the quantity and check inventory, validate the address and calculate shipping, match available coupons against the new total, then validate the payment method. Each step runs in deterministic code, and the action advances as far as it can:
{
"action_id": "checkout-123",
"node": "CONFIRM_ORDER",
"applied": ["items", "address_id", "payment_type"],
"missing": [],
"available_coupons": [
{ "id": "coupon-1", "label": "30 off orders over 300" }
],
"requires_confirmation": true,
"summary": {
"amount": 398,
"address": "Office",
"payment_type": "balance",
"coupon": null
},
"next_actions": [
{ "tool": "advance_action", "description": "Apply a coupon or change order details" },
{ "tool": "confirm_action", "description": "Submit the order and charge payment", "requires_user_approval": true }
],
"version": 4
}
One call takes the order from the start all the way to the boundary where money actually moves. available_coupons is downstream information that only exists once the total is known; the agent can ask the user about it before confirming. If the user wants one, the agent submits coupon_id, the server recalculates, and the action stays at the confirmation node.
If a field fails validation, say inventory runs short, the server stops at the failing node: applied lists the fields that took effect, validation_errors explains why, and downstream fields are held as pending until the upstream issue is fixed and they can be re-evaluated. At that point next_actions contains only advance_action; confirm_action does not appear.
Two boundaries must hold:
- Fields that depend on upstream results get their options from the server. Shipping methods and available coupons depend on items, quantity, and address. The model may submit a preference early, but whether it is valid is decided by the server's calculation.
- No matter how many fields are submitted, the confirmation node is never skipped.
requires_confirmationis a hard boundary; having all the information does not bypass it.
At this point the model still has not charged anything. It has only moved the order to a node that requires human confirmation. Only after the user confirms in the UI does the agent call confirm_action, performing the final submission with an idempotency key:
{
"action_id": "checkout-123",
"approval": "approved",
"idempotency_key": "req-20261026-001"
}
After submission, the response again tells the model what to do next:
{
"action_id": "checkout-123",
"node": "PAYMENT_SUBMITTED",
"external_id": "payment-123",
"next_actions": [
{ "tool": "verify_action", "description": "Check that the payment actually went through" }
]
}
The key point: the server knows the workflow's upstream and downstream, so it can tell the model up front everything it will need from the user; the model is good at pulling multiple fields out of natural language in one pass, so there is no reason to copy the step-by-step rhythm of a web form. Advancing node by node is the server's internal validation order, not the rhythm of the conversation between model and user.
The agent does not need to memorize the order of the flow. It asks for everything listed in inputs at once, submits once, and then picks its next step from the next_actions in each response. The whole interaction loop can be summarized as:
Search → Start Action → Read Context → Submit Patch → Advance → Confirm → Verify → Recover
One boundary worth noting: next_actions tells the model which platform primitives it may call, but the underlying microservice handler should not be exposed to the model. A structure like this belongs on the server as an internal flow definition:
{
"handler": "CONFIRM_ORDER",
"params": ["address", "payment_type", "coupon"]
}
The model-facing interface is reduced to pure business state:
{
"node": "CONFIRM_ORDER",
"missing": ["payment_type"],
"allowed_patch": ["payment_type"],
"next_actions": [
{ "tool": "advance_action", "description": "Fill in the payment method" }
]
}
handler is an internal implementation detail. node, missing, allowed_patch, and next_actions are the signposts a model actually needs. Passing handler through would only rename the endpoint, and the model would still be guessing how to call the backend.
How this relates to the MCP protocol itself
Readers who follow MCP closely may have noticed by now that the protocol is moving in a similar direction.
The 2026-07-28 MCP specification is the largest revision since launch, and several of its changes bear directly on the design above:
- Stateless core: protocol-level sessions are gone, and every request carries its own version, identity, and capabilities. State that has to persist across requests is carried by an explicit handle the tool mints itself, which is exactly the role of
action_idandversion. - Multi Round-Trip Requests: a tool call can return
input_required, telling the client which inputs are still needed; the client retries with the answers attached ininputResponses. This is effectively a protocol-level, general-purpose version ofmissingandpatch. - Tasks extension: long-running tool calls can run as tasks, with the client polling via
tasks/getand advancing them viatasks/update. Payment gateway timeouts and waits on external approvals fit here naturally. - MCP Apps: servers can provide UIs that they render themselves. Earlier I argued that the UI used to do state filtering for us; at nodes that need human confirmation, the business can now supply that UI directly.
So a stateful business Action is not about routing around the protocol. A more sensible division of labor:
- The protocol handles transport, pausing and resuming, asking the user for input, and carrying long-running work.
- The business Action owns the business semantics: which fields the current node allows given this order's state, which step has financial side effects, and why a validation failed.
The protocol can tell the model "input is needed here," but it cannot answer "for this already-shipped order, what operations are legal right now?" That answer still has to come from the business system, in deterministic code.
Four principles for redesigning MCP tools
1. Find where the flow starts, from intent
To place an order, the agent should not be guessing which of hundreds of endpoints is called create_order. The platform needs to give each business flow a trigger entry point, so the agent can start from what the user actually said and find where that flow begins.
The search entry takes a natural-language intent, not a capability name the agent already knows:
{
"query": "Buy two of the item in my cart and ship it to the office"
}
The platform's capability catalog keeps a trigger description for each flow: when to use it, which business objects it applies to, and when it does not apply. Matching can use embedding retrieval, or combine keywords with model reranking. A hit returns more than a name; it returns an overview of the flow's upstream and downstream:
{
"capability": "checkout",
"trigger": "The user wants to buy items and pay for them",
"prerequisites": ["signed in", "cart or specified item is not empty"],
"inputs": ["items[].quantity", "address_id", "coupon_id", "payment_type"],
"side_effects": ["charges payment", "reserves inventory"],
"requires_approval": true,
"next_actions": [
{ "tool": "start_action", "args": { "capability": "checkout" }, "description": "Start the checkout flow" }
]
}
From this the agent knows where the flow starts, what it needs to ask the user, and which step has side effects. The node-by-node progression is then handled by the state machine that start_action creates.
This looks a lot like progressive loading in Agent Skills: only a short description sits in the model's view by default, and the full content is expanded once it matches. In other words, it builds the discovery logic of Skills into tool design.
The difference is who executes once it is expanded. A Skill expands into instructions written for the model, which the model then follows. A business capability expands into an overview of the flow, while node progression, validation, and side effects are carried out by the server-side state machine. The model needs to see the whole picture, not walk every step by hand.
2. Return the full business context in one read
When reading an order, a few rows from the main table are usually not enough. Items, payment, fulfillment progress, after-sales eligibility, currently allowed operations, and the reasons for disallowed ones are better presented together in a single read.
This does not mean dumping whole database tables into the context. It means assembling the relationships and constraints needed for one business judgment into a cohesive view:
{
"entity": "Order",
"status": "SHIPPED",
"refund": { "eligible": false, "reason": "order_has_shipped" },
"requires_approval": false,
"next_actions": [
{ "tool": "start_action", "args": { "capability": "return_refund" }, "description": "Request a return and refund" },
{ "tool": "start_action", "args": { "capability": "exchange" }, "description": "Request an exchange" }
]
}
Note that next_actions here points directly at the entry points of two other flows: reading an order already tells the model which business flows it can enter from here. If the business system already has explicit rules, it should hand the model the deterministic answer, rather than making the model join order, payment, and fulfillment tables and then probabilistically infer refund eligibility.
3. Converge entry points around intent
Whether it is a refund, a return, or a profile change, around one business object you can keep a single entry point that expresses intent. The agent only has to say: "Start an after-sales request for this order."
Whether the order qualifies, whether to take refund-only or return-and-refund, whether evidence is needed, whether human approval is triggered, and which microservices to orchestrate are all dispatched by the underlying RefundAction state machine through deterministic code.
4. Put confirmation, verification, and recovery in the same action lifecycle
The highest-risk operations in production are charging, refunding, deleting data, changing permissions, submitting orders, and sending files outside. These should not stop at "request sent". They need to return a verifiable, closed-loop state:
{
"status": "submitted",
"external_id": "payment-123",
"verification": { "status": "paid", "amount": 199 },
"next_actions": []
}
For unresolved states such as a gateway timeout, the model should not blindly retry. It should receive explicit recovery guidance:
{
"status": "unknown",
"recoverable": true,
"action_id": "checkout-123",
"next_actions": [
{ "tool": "verify_action", "description": "Check with the payment gateway first; do not resubmit" },
{ "tool": "recover_action", "description": "If verification shows no payment, retry with the original idempotency key" }
]
}
A production-grade business Action usually maintains at least these fields: action_id, current_node, version, inputs, missing, allowed_patch, applied, next_actions, requires_confirmation, verification, recoverable_errors. It is essentially a lightweight state machine: the business system keeps control of state transitions, and the agent contributes information gathering and decisions at the current node.
Giving the agent a different skeleton
After this kind of refactor, the toolset a platform exposes to agents can collapse into a few stable primitives:
search_capabilities: locate a business capability entry pointstart_action: create a business action instanceget_context: read a business entity view, or re-fetch the current node's state and constraints when resuming mid-flowadvance_action: submit a patch and move the node forwardconfirm_action: gate actions that have side effects behind approvalverify_action: confirm the external system actually completedrecover_action: handle timeouts, dangling states, and idempotent retries
The backend can keep its hundreds of REST, RPC, and microservice endpoints. They just no longer sit in the model's decision space. The model sees business actions; deterministic code runs beneath the waterline.
There is an objection worth facing head-on: advance_action(action_id, patch) is itself a generic interface. The model only learns what it can fill in by reading allowed_patch at runtime. How is that different from stuffing two hundred tools behind a single catch-all execute()?
The difference is where the constraints come from. execute() leaves parameter assembly and flow orchestration to the model, and the interface says nothing about what is possible right now. With advance_action, every step returns the current node, the missing fields, what may be changed, and which actions may be called next, and an invalid patch is rejected by deterministic code along with a reason.
The cost is real, though: the tool definitions lose static, typed schemas, so you cannot tell from the tool list which fields each Action needs. One compromise is to have get_context also return a JSON Schema for the current node's patch, moving type constraints from the tool definition into the runtime context.
What really needs compressing is the decision space the model has to understand at each step.
Looking back at recent work, several threads are converging:
- Tool Search loads tool definitions on demand;
- Connectors and controlled runtimes execute external operations inside a safety boundary;
- The MCP protocol itself now offers multi-round input, long-running tasks, and server-provided UIs;
- Stateful business Actions turn transactions and approvals into state machines that can advance and pause.
Stacked together, they may come closer to a production-grade architecture for connecting agents to a business.
If you look only at how an agent gets from a single sentence to a completed order, these layers form a progression, each answering one question:
- Finding the tool: Tool Search lets the model discover and load tools on demand from a large set;
- Finding the flow and seeing its upstream and downstream: a trigger entry point lets the model start from user intent, locate where the flow begins, and know in advance what information is needed and which step has side effects;
- Finishing the flow: the state machine handles node progression, validation, confirmation, verification, and recovery.
The first layer is retrieval at the tool level; the third is execution at the business level. The middle layer is the easiest to overlook, and it decides whether a model holding the right tools is guessing at the flow or working from a map of it.
Following this line, there is a question I have not fully worked out but think is worth discussing: which side should the flow knowledge live on?
One option is the model side, as a Skill: describe in natural language that "when placing an order, confirm the address first, then choose a payment method," and let the model call tools accordingly. The other is the server side, as a business capability: the flow definition and state machine live inside the platform, and the model only reads the overview and submits information.
Each has its place. Flows that span several systems, have no single owning team, and are mostly about reading and organizing information are more flexible as Skills, and quicker to change. Flows inside a single business system, with financial or data side effects and rules that must be enforced, are safer on the server, because rules in a Skill are suggestions while rules in a state machine are constraints.
In practice the two will likely coexist: Skills handle cross-system orchestration and conversational strategy, while business capabilities handle the part inside each system that cannot go wrong. Where to draw that line may be one of the trade-offs engineering teams keep revisiting as MCP moves into core business systems.
As model capability keeps improving, the number of exposed endpoints stops being a moat. The real engineering moat is whether you can abstract complex business logic into a few stable action primitives, while the layers underneath hold state, permissions, approvals, verification, and failure recovery.
A useful exercise: look at your own codebase again. If you pulled back inside the platform every fine-grained endpoint that only works because a frontend hides the wrong options, how many real business switches would the agent still need to see?
Perhaps what remains in the model's view is no longer two hundred endpoints, but seven clear stages:
find the capability, start the action, fill in information, advance the node, confirm, verify, recover.
Looking back, this direction circles back toward the thinking behind GraphQL: instead of organizing capabilities around individual endpoints, you model business entities and the relationships between them, and let the caller, whether a frontend or an LLM, compose freely according to its own intent within boundaries the server draws. The difference is that GraphQL mostly answers "which data do I need," while an agent also needs the server to tell it "what can I do now, and what comes next." On top of the entity view sits another layer: constraints on state and action.
Show the agent a few less fragments and let it see the business itself. That may be a direction worth continuing to explore as MCP moves into deeper water.
References
- OpenAI, Tool search: https://developers.openai.com/api/docs/guides/tools-tool-search
- Anthropic, Writing effective tools for agents — with agents: https://www.anthropic.com/engineering/writing-tools-for-agents
- Block, Block's Playbook for Designing MCP Servers: https://engineering.block.xyz/blog/blocks-playbook-for-designing-mcp-servers
- Model Context Protocol, The 2026-07-28 Specification: https://blog.modelcontextprotocol.io/posts/2026-07-28/
- xAI, Grok Bot: Computer and Apps: https://docs.x.ai/grok-bot/computer-and-apps
- Meta, Introducing Muse: https://about.fb.com/news/2026/09/introducing-muse-personal-ai-agent/
- Mastouri et al., From REST to MCP: An Empirical Study of API Wrapping and Automated Server Generation for LLM Agents: https://arxiv.org/abs/2507.16044
- Kate et al., LongFuncEval: Measuring the Effectiveness of Long Context Models for Function Calling: https://arxiv.org/abs/2505.10570
— end —