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.