Designing MCP tools for agents A developer advocates for designing MCP tools with separate, precise contracts per dataset rather than generic query tools, arguing that explicit tool names and schemas help AI agents select the correct operation. The post distinguishes between splitting tools for different contracts versus splitting servers for domain ownership, and recommends using MCP resources for frequently changing column definitions. Designing MCP tools for agents An MCP server can expose one query data dataset, filters tool and call it finished. The model sees a generic name, a loose filters object and a dataset string. It cannot see that sales takes a date range and returns revenue while inventory takes a warehouse and returns stock levels. Separate tools make those contracts visible: php query sales start date, end date - SalesRows query inventory warehouse, below quantity - InventoryRows The model sees the tool’s name and description when the client calls tools/list . Its JSON schema describes the input, and it can publish an output schema for the client to validate. Those fields are part of the MCP tool contract https://modelcontextprotocol.io/specification/2025-11-25/server/tools , so the model gets useful context before it chooses a tool. Tools and HTTP routes tools-and-http-routes A remote MCP server usually has one Streamable HTTP endpoint. That endpoint exposes a catalog of tools. Creating a new tool does not require a new HTTP route, and splitting a server into separate endpoints solves a different problem. I split tools when the model needs a different contract to choose or call them correctly. Different permissions are another reason: read invoice and issue refund have different blast radiuses even if the backing API puts both under /billing . I split servers when another team owns the domain or it needs its own authentication boundary. In acme-mcp https://github.com/MattJColes/acme-mcp , orders and billing are mounted into one server because they belong to the same product. Analytics is shown as a separate proxied server that another team could deploy independently. OpenAPI can describe HTTP endpoints well. MCP gives agent clients a standard way to discover the subset they may use and invoke it through one protocol, which saves every client from building its own adapter. Describe each dataset where it helps selection describe-each-dataset-where-it-helps-selection A generic query tool works when the datasets share a real contract. It becomes vague once each dataset needs different fields and filters, especially when the business uses different words for the results. A dataset="sales" parameter cannot change the static schema the model saw for the tool. For a small set of important datasets, I prefer separate tools with precise descriptions: python @mcp.tool tags={"analytics"} def query sales start date: date, end date: date - SalesResult: """Return net sales grouped by day for the requested date range.""" ... @mcp.tool tags={"analytics"} def query inventory warehouse: Warehouse, below quantity: int - InventoryResult: """Return stock below a quantity threshold for one warehouse.""" ... The function signatures advertise valid arguments and the return models describe different results. The docstrings use the same words as the people working with that data. Column definitions change more often than the operation itself, and they can be too large for a tool description. MCP resources fit that data better: dataset://sales/schema dataset://inventory/schema | MCP primitive | Who selects it | What I put there | |---|---|---| | tool | model | an operation | | resource | application | data or context | | prompt | user | a reusable template | That control split comes from the MCP server concepts guide https://modelcontextprotocol.io/docs/learn/server-concepts , and it is a useful test for deciding where something belongs. Use discovery when the catalog gets large use-discovery-when-the-catalog-gets-large Separate tools stop helping when a warehouse has hundreds of datasets. Loading every contract gives the model a different problem: most of its starting context is tool definitions it will never use. Then I reduce the initial surface: list datasets query describe dataset dataset id query dataset dataset id, query The first call returns names and short descriptions. The second returns the selected schema and column definitions. The query tool runs only after the model has that context. This follows the MCP guidance on progressive tool discovery https://modelcontextprotocol.io/docs/develop/clients/client-best-practices progressive-tool-discovery while keeping each dataset’s shape explicit. I use separate tools for a small catalog where the datasets differ. Once the catalog is large, progressive discovery saves more context than static per-dataset contracts. Serving skills with the tools serving-skills-with-the-tools A schema describes the data. A skill explains the procedure around it: which resource to read first, how to interpret a company-specific field or what to do with a signed download link. That skill can live in the user’s agent config or in a project repo. It can also live in the MCP server. FastMCP’s Skills Provider https://gofastmcp.com/servers/providers/skills exposes SKILL.md and its supporting files as resources that a client can discover and fetch. I use that in acme-mcp : the server publishes skill://handle-downloads/SKILL.md beside the report tool. The tool returns a signed URL, and the skill tells the client to show it to the user without reading the file into context. Access to both is governed by the same reports tag. I keep executable work in tools. The server can attach changing context and workflow instructions as resources, including a skill. I use a separate server when ownership or the security boundary changes.