← All posts

MCP Tool Design for AI Agents, Not API Endpoints

AI agents do not need your whole API

If you are building MCP servers, LLM agent tools, function calling layers, or internal AI agent infrastructure, the tool list is part of your product. That makes this an infrastructure problem more than a gripe about productivity tools.

Most Model Context Protocol servers are designed by looking at the API.

The API has users.create, so the MCP server gets a create_user tool. The API has users.update, so it gets update_user. Then delete_user, list_users, create_invoice, update_invoice, query_database, append_block, upload_file.

It feels honest, and it is expensive for LLM agents.

Every MCP tool is prompt surface: name, description, arguments, schema, examples, edge cases, and all the warnings we add because agents otherwise call things wrong.

Connect one server with twenty tools and maybe you survive. Connect ten integrations like that and you have two hundred tools. Before the model helps you with anything, it is already carrying a catalog of things it might never call.

Then people say the model got worse. Maybe you filled its working memory with your integration design.

Tool lists are LLM context windows in disguise

The context window holds the available interface as well as the chat history, so tool design is also a context engineering problem.

Every enabled tool teaches the model what exists, how to call it, what shape the arguments take and what the response might mean. That is useful when the tool is relevant and waste when it is not.

The naive MCP pattern gives every endpoint first-class space in the model’s head. Most of them don’t deserve it.

CRUD is an implementation detail, and entities are not tool boundaries either.

Your model does not need create_customer, update_customer, delete_customer, list_customers, create_invoice, update_invoice, delete_invoice, list_invoices, and the same pattern repeated until the tool list becomes an API reference manual. It needs a way to say what it wants. The server can own the rest.

You see the same pressure in function calling, tool use benchmarks and agentic workflows. Tool selection costs something, and so does reasoning over parameter schemas. Every operation you expose directly is another thing the model has to route around.

The MCPToolBench++ paper describes this bottleneck: tool descriptions and parameter schemas take long token lengths, which limits how many MCP tools an LLM can process in a single run.

Use one execution tool for agent actions

The interface I keep coming back to is boring:

{
  "operation": "customer.create",
  "payload": {
    "name": "Acme",
    "status": "lead"
  }
}

One tool executes operations by name. Call it execute, run, dispatch, whatever you like.

What matters is that the public MCP server surface stays small while the operation set grows behind it. Today you support ten operations, tomorrow a hundred, and the model still sees the same execution tool. New capabilities become entries in a server-side operation registry instead of new top-level tools fighting for context.

The registry holds the real complexity:

  • operation name;
  • input schema and examples;
  • safety flags and validation rules;
  • batching, response shaping, rollback.

Those details belong there, not in every model turn.

Use one schema tool for agent planning

The obvious objection is fair: if the model only sees one execution tool, how does it know the payload shape?

Give it a second tool:

{
  "operation": "customer.create"
}

describe returns the schema, examples, and constraints for one operation. Only the one it asked about.

For context management this means the model pulls a schema when it needs it instead of preloading the whole product surface before it knows the task. For simple calls it can try execute directly and learn from a good validation error. For complex calls it can ask describe first.

The model still gets the whole API, just paced to the task.

Errors should repair the next call

Once complexity moves behind an operation registry, errors become part of the interface.

A bad error says:

{ "error": "Invalid payload" }

A worse error dumps the entire schema for the whole operation family.

A useful error points at the failing path and gives the smallest repair:

{
  "code": "validation_error",
  "path": "payload.status",
  "message": "Expected one of: lead, active, archived",
  "fix": "Use status: \"lead\" for a new prospect."
}

The model needs enough information to make the next call correct, and nothing like a lecture. An MCP server’s job is to cut the useless turns between intent and action.

Responses are part of the contract

Raw responses leak context too. If your server sends back every default flag, timestamp, empty field, nested SDK object and transport detail, all of that becomes the next input the model has to reason over.

So shape responses for agents:

  • omit default false/null noise;
  • flatten common read shapes;
  • return IDs and summaries when that is enough;
  • make verbose mode explicit;
  • keep raw SDK output as an escape hatch instead of the default.

The model should spend context on the user’s task, not on your vendor’s JSON habits. For a data-heavy server, I wrote up how we keep MCP tool responses from burning the token budget.

This scales better than endpoint-shaped MCP tools

The two-tool pattern has costs. You still need a real operation registry, plus validation, permissions, batching, rate limits, idempotency and careful response design.

But it scales in the dimension that matters for AI agents. Adding operations or entities does not grow the MCP tool list, and adding optional fields does not force every conversation to carry another schema blob.

The model keeps two stable affordances:

  • execute an operation;
  • describe an operation.

Everything else is server-side interface design.

The principle

I think of MCP as LLM infrastructure, a context protocol, more than a plugin protocol for AI tools.

If you expose twenty tools because your REST API has twenty endpoints, you are making the model pay for your backend architecture. Connect ten MCP servers built that way and the tool surface becomes its own context problem.

A good MCP server lets the model carry the smallest useful interface while still reaching the full system behind it: one tool to do the work, one tool to ask for the shape, and everything else behind the server.

That is the approach I used in my Notion MCP Server GitHub repository: dozens of operations, two public tools. The package is published as notion-mcp-server on npm. Nothing about it is specific to Notion. The pattern fits any MCP server that would otherwise turn into a schema dump with a transport layer.