Tools are not endpoints
Every tool definition an MCP server exposes is paid for in the caller's context window, on every connection, before any work happens. That makes the tool surface a budget, and most servers are over it.
When an agent connects to an MCP server, the first thing it does is
ask for tools/list. The whole response lands in the
model's context: every name, every description, every input and output
schema. A server with sixty tools can burn tens of thousands of tokens
before the agent has read the user's question. The agent pays this tax
again on every session.
The most common way to end up here is also the most tempting one: generate the MCP server from an OpenAPI document, one tool per endpoint. A REST API with eighty routes becomes eighty tools. Each carries its full request and response schema. The agent now has to pick the right CRUD verb out of a wall of near-identical definitions, and the model's accuracy at choosing tools degrades as the list grows. Even the vendors who sell these generators have started saying the quiet part: a tool is not an endpoint.
Opt-in is the design stance
In AgentBack, a route never becomes a tool by accident. REST
verbs and @tool are separate decorators that happen to
accept the same Zod schemas. You expose the three task-shaped
operations an agent actually needs and keep the other seventy-seven
routes as plain HTTP.
@api({basePath: '/orders'})
@mcpServer()
class Orders {
// HTTP-only: agents don't need raw CRUD.
@get('/orders/{id}', {path: OrderPath, response: Order})
async get(input: {path: z.infer<typeof OrderPath>}) { … }
// The one operation worth a tool: outcome-level, not CRUD.
@tool('refund_order', {
description: 'Refund an order and notify the customer.',
input: RefundIn,
output: RefundOut,
})
async refund(input: z.infer<typeof RefundIn>) { … }
}
The same class, the same schemas, the same DI container. The choice of
which methods agents see is an explicit one you make per method, and
the @authorize policy layer narrows it further per
caller.
Measure the bill
Budgets only work if you can see the spend.
MCPServer.toolCostReport() prices each tool's
tools/list entry, name, description, and emitted JSON
Schemas, and totals what one listing costs a caller:
const mcp = await app.get(MCPBindings.SERVER);
console.log(formatToolCostReport(mcp.toolCostReport()));
// tool tokens bytes
// search_catalog 412 1648
// refund_order 187 748
// get_status 64 256
// total 663 2652
Tools over 500 tokens get flagged. That threshold is a heuristic, not a law; the point is that a fat tool is now a number in a report instead of an invisible cost spread across every agent session. A definition that large usually means the description restates the schema, or the schema models the database row instead of the task.
What a good tool looks like
The pattern that holds up in practice: fewer tools, each one an outcome. "Refund this order" beats four CRUD calls the agent must sequence correctly. The schema names the task's inputs, not your table columns. The description says when to use the tool, in one or two sentences, because the schema already documents the rest.
Endpoints are for callers who read documentation once and write code against it. Tools are for callers who re-read the entire catalog every session and pay per word. Design accordingly.