DEV Community

Cover image for Intent-Sized vs Resource-Sized, Measured
Steef-Jan Wiggers
Steef-Jan Wiggers

Posted on

Intent-Sized vs Resource-Sized, Measured

Every MCP design discussion eventually reaches the granularity debate. Should a server expose intent-sized tools, one per thing a user wants done, or resource-sized tools, one per operation on each resource, the shape an OpenAPI-to-MCP generator produces from an existing REST API? The arguments are usually aesthetic. This series settles things by measuring, so the companion repo now has measure/eval_granularity.py: the same restaurant kitchen rendered both ways, and a live model asked, for twelve user requests, which tool it would call first on each full surface.

The intent surface is the sample's three tools: search_restaurants, get_menu, place_order. The resource surface is the seven tools a generator would derive from the REST door: list_restaurants, get_restaurant, list_menu_items, get_menu_item, create_order, get_order, list_orders, with the neutral CRUD descriptions generated surfaces actually have. Same kitchen, same capability, two vocabularies.

The fixed costs

The tokenizer from What the Agent Pays for Discovery prices the two contracts:

surface tools tokens per model turn
intent 3 302
resource 7 394

A 30 percent toll on every single model turn, paid before anything is called, forever. That is the budget argument for intent-sized tools, and it compounds: agents make several turns per task, and fleets carry many servers.

The selection results

Twelve requests, three repeats each, gpt-5.4-mini choosing its first call from the full surface:

surface kind selected args ok
intent direct 66.7% 66.7%
intent chained 100.0% 100.0%
resource direct 94.4% 94.4%
resource chained 75.0% 75.0%

Read naively, that table says each surface wins half the board. Read the actual misses and it says something much more interesting.

The intent surface's misses are obedience

Every single intent-surface miss, six out of six, is the same event: given "Order two Margherita at restaurant r1", with every id it needs in hand, the model chose get_menu instead of place_order. All three repeats, both order tasks.

That is not confusion. That is the model doing what the contract told it to do. place_order's description, the one that went through the CI lint gate, says "Use it only after get_menu has confirmed the exact item name", and the model obeyed it over the shortcut the benchmark scored as correct. The harness's strict scoring calls that a 33 percent failure; a production incident review would call it the safety sentence working. The sentence exists because the confusion telemetry showed agents inventing item names; the model reading it and checking the menu first is the intended behavior, deliberately purchased, in production the difference between an order and an order_rejected sentinel.

The resource surface's misses are the dangerous kind

The resource misses are a different species. "Get the menu for restaurant r1" once went to get_restaurant instead of list_menu_items, ordinary sibling confusion in a CRUD family where four tools differ by one noun. But "Order a Stamppot from De Molen Bistro in Nijmegen" missed all three repeats, and twice the miss was create_order called directly, with a restaurant name where an identifier belongs. That is the exact name-as-id failure that Watching the Contract Fail in Production counted in live telemetry, and the resource surface invites it because generated CRUD descriptions carry no provenance sentences. Nothing in "Creates a new order resource" says where a restaurantId comes from, so the model improvises one. The intent surface went 100 percent on these same natural-language requests, steered by the sentences "as returned by search_restaurants" and "call it after search_restaurants".

Notice the symmetry. On direct order tasks the resource surface was happy to create_order immediately, which the scoring rewards, because no sentence told it to verify anything first. The intent surface hesitated, because a sentence did. Same model, same requests; the behavior difference is entirely the prose.

Where resource-sized honestly wins

The gap probes give the resource surface its real victory. "Show me the details of restaurant r2" and "Did my order f774f41d go through?" have no correct tool on the intent surface, and the model did not say so: all six repeats it called search_restaurants, a plausible-looking wrong call that would send an agent looping. The resource surface answered both perfectly, get_restaurant and get_order, three for three each. Resource-shaped surfaces inherit the coverage of the API they were generated from, and coverage gaps on an intent surface do not fail loudly, they fail as confident wrong calls. If your users ask about order status, no amount of description quality rescues a surface that has no tool for it.

The takeaway

The granularity debate turns out to be a proxy war, and the real fight is the one this series has been measuring all along: who writes the prose, and what does the surface cover. Intent-sized tools won the chained tasks not because three is a better number than seven, but because their descriptions carry provenance and sequencing sentences that a generator does not write. Resource-sized tools won the gap tasks not because CRUD is a better shape, but because they cover more of the backend. Both results are about contract quality, not tool count.

So the practical guidance is not "pick a granularity". It is: start from coverage, because a missing tool fails as a confident wrong call; pay the token toll only for operations agents actually need, because every tool costs every turn; and whatever shape you end with, write the provenance and when-to-call sentences into it, because they are what moved behavior in every measurement this series has made. A generated resource surface with linted prose would likely beat both of the surfaces measured here. That is a falsifiable claim, the harness takes arbitrary surfaces, and I would genuinely like someone to run it against mine.

The harness, both surfaces, and the full per-row results are in the companion repo: measure/eval_granularity.py and measure/eval_granularity_results.json.

Top comments (0)