Guides

Agentic Systems

How to design MCP tools for agents

MCP tools best practices from production: why one consolidation layer in front of your vendor servers beats wiring each of them into the agent, and how large each tool should be.

By Tirth Gajjar · Founder & CTO

14 min

At a glance

MCP tools are the operations an MCP server exposes for a model to call, and the model chooses among them from their definitions. When several sources answer the same questions, put one consolidation MCP between your agents and the vendors. Expose fewer, larger tools that return structured JSON, authenticate every request as the user, rate-limit by caller, and read the call log to decide the next batch mode or schema change.

On Solarpunk, wiring each service straight to the model broke tool choice at roughly thirty tools and let a vendor change break something unrelated, so Bigcircle put one uniform layer of typed tool contracts between the model and every integration.

Who this is for
Engineers building AI systems and technical leads reviewing the implementation.
Topics
  • Model Context Protocol (MCP)
  • Tool / Function Calling
  • Structured Output & Constrained Decoding
  • Context Window
  • AI Agent
  • LLM Observability & Tracing

Published Updated

MCP tools are the operations an MCP server exposes for a model to call. Each has a name, a description and a JSON Schema for its inputs, and the model decides when to call one, so the model reads the tool list before every answer. Design them around the questions your users ask rather than the endpoints a vendor ships: fewer, larger tools, and one consolidation layer in front when two or more sources can answer the same kind of question.

Key facts

Definition

MCP tools are the operations an MCP server exposes for a model to call, and a consolidation layer exposes a few larger ones shaped around the questions users ask, with code deciding which source is authoritative.

How Bigcircle does it

  1. Bigcircle recommends that layer when two or more sources can answer the same kind of question, because otherwise the model chooses a source on every request.
  2. On Solarpunk, Bigcircle replaced direct wires with one uniform layer of typed tool contracts after tool choice went wrong at roughly thirty tools in context.
  3. Bigcircle recommends that the layer return structured JSON, authenticate every request as the user, rate-limit by caller, and log every call, refusals included.

Comparison

One read-only source with a single owner can stay on a direct wire, and the consolidation layer becomes worth running once a second source can answer the same question.

The layer exposes a small number of larger tools shaped around those questions, and it decides in code which source is authoritative for each one. It returns structured JSON, authenticates every request as the user, and logs every call. A direct wire stays reasonable for one read-only source with a single owner, and it stops being reasonable once a second source for the same question arrives.

CriterionDirect vendor wiresConsolidation layer
Who chooses the sourceThe model, on every questionA rule in code for each question type: which system is authoritative, and which is the fallback
Tool listThe union of each vendor server's tools, loaded on every turnFewer, larger tools shaped around the questions users ask
OutputEach vendor's own shapes and error formsJSON with defined fields, units, and as-of dates
AuthReasonable when the vendor's server already authenticates as the user and you can read its logsEvery request authenticates as the calling user, and the layer holds its own credential for each vendor
MaintenanceEach wire brings its own error shapes, and a vendor change can break something unrelatedOne owner for every vendor integration, behind one set of tool contracts
When it fitsOne read-only source with one owner, user auth and logs you can read, or a prototype. Revisit when a second source for the same question appearsTwo or more sources can answer the same kind of question
How Bigcircle handles thisOn Bigcircle's Solarpunk build, direct wires were the failure. Selection held until roughly thirty tools, then a confident result came from the wrong systemBigcircle replaced those wires with one uniform tool layer of typed contracts, and each plan step searches it for the few tools that apply

The default goes the other way because each vendor server is built to be complete for its own product. It mirrors the vendor's API, so it ships many small tools, one per endpoint, and if you connect six of those the agent holds the union of their definitions on every turn. Every question then starts with the same work of deciding which system has the answer and whose version to trust. The model does that work in tokens, differently each time, and when it picks wrong nothing in the answer says so.

This guide covers what the direct approach costs, what the consolidation layer should own, and how large one tool should be. It then covers structured output, authentication under the 28 July 2026 MCP specification, rate limits and logging, and how the call log decides the next change. AI agent permissions covers who may call a tool, and MCP prompts versus tools for agent playbooks covers how an agent finds the right workflow in a large catalog.

What does wiring every vendor server cost?

Every MCP tool definition the client loads is text in the context window, on every request, whether the task needs it or not. With one server that cost is small enough to ignore. With several servers of many tools each, the definitions become a large share of what the model reads before the user's question. Several of them describe near-identical operations from different vendors that the model has to tell apart from their descriptions alone.

The token cost is the visible part, and the expensive part is the decision each question now carries: which system has the answer, and whose version to trust.

We saw that failure on Solarpunk, a product that works across more than a hundred tools. With every tool described in the prompt, selection held until roughly thirty tools and then went wrong with no sign in the output. The model returned a confident result from the wrong system, and that error cost more than the context did because it did not look like an error.

Maintenance degrades in the same arrangement, for a different reason. Each direct wire brings its own credential handling, its own error shapes and its own prompt fragments, and on Solarpunk a vendor's change to one response surfaced as a break somewhere unrelated, days later. That is why the build put one uniform tool layer between the model and every integration, with nothing wired point-to-point and each tool a typed contract with declared inputs, outputs and side effects.

Every vendor server, or one layer in front of themIllustrative model. The sources are the same in both halves. The choice of source moves.Wired directAgentpicks a sourceCRMWarehouseData vendor AData vendor BDocumentsTicketsEvery tool from every server sits in context, and every question starts with a choice of source.ConsolidatedAgentasks a questionResearch MCPfind_companycompany_profiledeal_historylog_note, preview firstThe same six sourcessource choice lives in codeThe layer answers which source to trust once, in reviewed code, instead of once per question.

The six sources and four tool names in the figure are examples chosen to show the shape. They are not a count from any system, and the figure does not claim that the problem starts at six.

The two halves reach the same data, and they differ in where the choice of source is made. In the top half the model makes that choice on every question, while in the bottom half an engineer makes it once, in code someone reviewed. The model asks a question and gets an answer from the source the team already decided to trust.

What should the consolidation layer own?

The layer is an MCP server your team runs between the agents and the vendor systems. A proxy that re-exports each vendor's tools under one name does not count, because it moves the long tool list without shortening it. The MCP specification, as of its 28 July 2026 revision, expects such proxies to hit name collisions, such as two servers that each expose a search tool. Its suggested fix is to prefix each name with a server identifier, which leaves both near-identical tools in the list for the model to choose between.

ResponsibilityWhat the layer doesWhat happens when the agent does it instead
Source selectionA rule in code for each question type: which system is authoritative, and which is the fallbackThe model re-decides per question, and two users get answers from different sources
Entity resolutionResolves a fuzzy name to one record ID before any lookupAnswers blend two records for similar names, each correctly sourced and about the wrong entity
Output shapeReturns JSON with defined fields, units and as-of datesThe model parses prose and misreads a figure
IdentityAuthenticates every request as the calling userA shared key reaches the data, and the audit trail cannot say who asked
Rate limitsLimits by user and by tool, before the upstream vendor doesA reasoning loop spends the vendor quota for everyone
Call logRecords every call, allowed or refusedNobody can see what the model actually asked for
WritesReturns a preview before committing a changeThe model commits a change nobody reviewed

Entity resolution is the row easiest to leave to the model, and the one that hurts most when it goes wrong. On Prospex, our prospect research engine, collection was never the hard part. The hard part was resolving one company or person across four source types whose schemas share nothing, where a wrong merge carries into every later inference. The profile that comes out is coherent and well sourced, and it describes the wrong person. Put resolution in the layer as its own step, and make every other tool take the resolved ID rather than a name.

The write row has the same logic as the rest of the table. On Skionis, our DevOps agent on live AWS, the agent reaches the account only through purpose-built tools, with no general shell and no path by which a plan step becomes an arbitrary API call. An engineer approves an architecture diagram before anything is applied. Keep reads and writes in separate tools, so a permission policy can allow one without the other. Have the write tool return a preview, so the guardrails and the reviewer have a concrete change to approve.

How large should an MCP tool be?

A tool should match a question a user asks, not an endpoint a vendor ships. The vendor's API is organized around its data model, with separate calls for the company, its funding rounds, the investors in a round, and notes. A user's question crosses those boundaries, so a server that mirrors them makes the model plan and join four calls to answer one question.

Here is the same question answered with each of the two shapes:

Skinny, endpoint-shaped toolsLarger, question-shaped tool
Exampleget_company, list_rounds, list_round_investors, search_notescompany_profile with an include list
Calls per questionSeveral, planned by the modelOne, planned by your code
Where the join happensIn the model's reasoning, in tokensIn the server, in code you can test
Definitions in contextOne per endpoint, per vendorOne per question type
Typical failureA step skipped, or two results joined on the wrong keyThe tool returns more than the question needed

Larger does not mean general. A run_sql tool or a call_api tool that takes any endpoint is one tool in the list and every operation in practice. The model has to write the query, the policy cannot tell a read from a destructive write, and the log records a string instead of an intent. Keep the tool's scope to one question type, with typed options such as which sections to include, a date range, and an enum for the region.

This is roughly what a question-shaped tool's contract looks like, with illustrative fields:

code
{
  "name": "company_profile",
  "description": "One company's profile from our authoritative sources. Use after find_company has returned a company_id. Not for comparing several companies; use compare_companies.",
  "inputSchema": {
    "type": "object",
    "required": ["company_id"],
    "properties": {
      "company_id": { "type": "string" },
      "include": {
        "type": "array",
        "items": { "enum": ["funding", "investors", "notes", "scores"] }
      },
      "since": { "type": "string", "format": "date" }
    }
  }
}

The description names the prerequisite and the near-miss, and it needs both. When two tools could plausibly answer a question, the description is all the model has to tell them apart. The line "not for comparing several companies" prevents a wrong pick that a longer list of the tool's features would not.

Split a tool when two uses need different permissions, different rate limits or different owners. A tool that reads CRM notes and a tool that writes them should never be one tool with a mode flag, since the permission check would then depend on an argument the model chose.

Why return structured JSON and not prose?

Return JSON with defined fields, not descriptive text. Prose reads well in a demo and hands the model a parsing job on every call, and a misread figure comes back looking like an answer. Scale Venture Partners found the same on the MCP server behind their investment research, where tightening the schemas improved answer quality straight away. The protocol now supports this directly. As of the 28 July 2026 revision a tool can declare an outputSchema, and a server that declares one must return structuredContent that conforms to it. Clients should validate the result against that schema.

Give every group of fields a source and an as-of date, so the agent can say where a number came from. Return explicit statuses for the cases that are not data, such as not_found, not_permitted, stale and rate_limited. An empty array is ambiguous, and the model will often read it as "none exist" and tell the user so, when the truth was that the caller could not see the records.

Keep result sizes bounded, whatever the source holds. A tool that returns every note on a company since founding pushes the rest of the conversation out of the context window. Accept a since date or a limit, and when there is more, return a count and a continuation handle. MCP has no protocol session any more, so that handle is an ordinary argument the agent passes back on purpose. The layer checks the caller's authorization against it again on the next call.

How should the layer authenticate, limit, and log?

One server in front of every source holds more reach than any one vendor server, so its controls have to be tighter. Validate the caller on every request, not once when a client connects. As of the 28 July 2026 specification that is the protocol's default rather than a design choice.

MCP no longer has sessions, the client must send its authorization on every HTTP request, and the server must validate each token and reject one issued for a different server. The shortcut that survives is caching a validation result or accepting long-lived tokens, and either one leaves a revoked user with access until the cache or the token expires.

The same section forbids an MCP server from accepting or passing along any token not issued for it, so the user's token cannot be forwarded to the vendor. The layer holds its own credential for each vendor, inside the layer where the model never sees it, and checks the user's identity against its own policy before it calls out. How to mint a short-lived token for each agent run is covered in MCP authorization.

Rate-limit by user and by tool, inside your own layer. The same specification requires servers to rate-limit tool calls. A reasoning loop can fire a burst of calls in seconds. Without a limit in your layer, the first thing to stop it is the vendor's quota, which then fails for every user at once. When the layer limits a caller, return rate_limited with a retry time, so the agent waits instead of switching to a worse source to get an answer.

Log every call, allowed or refused, with at least these fields:

  • the tool and its version;
  • the calling user and the agent or client;
  • the validated inputs;
  • the sources the layer read;
  • latency and result size;
  • the status returned, including refusals.

The refusals in that log matter as much as the successes. A tool that is refused often is either mis-permissioned or mis-described, and without the refused calls in the log you cannot tell which. Keep the log in the same observability stack as the rest of your services, because the question after an incident is usually which tool call touched which record.

How does the call log change the tools?

Read the call log on a schedule, because it shows how the model actually uses your tools, which is rarely how you expected when you wrote them. The signal is usually plain. Scale saw repeated calls over a single tool on their research server, added a batch mode, and cut sessions from an hour to minutes. Build that loop on purpose rather than waiting for an incident to send you to the log.

The call log decides the next change to the toolsFramework. A loop, run on a schedule, not after an incident.Log every calltool, caller, inputs,latency, result sizeRead the logrepeats, wrong picks,retries, empty resultsChange the toolsbatch mode, merge, split,tighter schema, renameRe-run the eval setsame questions,compare tool pathsA repeated call over the same tool in one conversation is the signal for a batch mode.Two tools picked in the same situation, one of them wrong, is the signal to merge or rename.

The loop is a framework, not a measurement from any system. Run it on a schedule rather than after an incident, and look for these patterns:

Pattern in the logWhat it meansChange
One tool called many times in a conversation with different IDsThe question is about a set, and the tool only takes oneAdd a batch mode that takes a list
Two tools alternate for the same question type, and one result is discardedTheir purposes overlapMerge them, or rewrite both descriptions with the near-miss
The agent fetches a large result and uses a few fieldsThe tool returns more than the question needsAdd an include list or filters
Validation errors cluster on one argumentThe schema is unclearUse an enum, add an example, or make the field required
An empty result followed by a retry with a different spellingEntity resolution is missingAdd a resolve step and require the ID
A tool nobody callsIt costs context and answers nothingRemove it

After each change, re-run a fixed set of real questions and compare the tool paths, not only the final answers. A change that keeps the answers correct and doubles the calls per question has made the system slower, and an answer-only check will not show it. Our evals guide covers how to build that question set from real failures.

Pin the tool list in CI as well, so a rewritten description fails a pull request before it reaches the agent, since the description is the instruction the model reads. MCP server testing covers that snapshot, and the agent eval that re-runs your question set when the snapshot changes.

When is wiring a vendor server directly the right call?

Consolidation has a cost: a service to run, an owner, and a release process for tool changes. For some setups it is not worth it, and the questions below run in order, with the first yes deciding.

Wire the vendor server directly, or consolidate firstFramework. Stop at the first yes.Do two or more sources answerthe same kind of question?Consolidate. Put the source rule in code.YesNoDoes a call need the user's ownidentity, a rate limit, and your log?Consolidate, or put a gateway in front.YesNoWould the agent hold more thana few dozen tools at once?Consolidate, and add discovery.YesNoWire the vendor server directlyand log its calls where you can read themA direct wire is a fine answer for one read-only source with one clear owner.Revisit when a second source for the same question arrives.

The "few dozen tools" threshold in the third question is our working range, set near the roughly thirty where Solarpunk's selection went wrong. It is not a measured cutoff, and a model or a tool set with very distinct tools may hold more.

A direct wire is fine for one read-only source with one clear owner, provided the vendor's server already authenticates as the user and you can read its logs. A prototype that proves a workflow is worth building also qualifies. Revisit the choice when a second source for the same kind of question appears, since from then on the model chooses between sources on every question and that choice belongs in code.

If the answer is to consolidate, start from the questions rather than the vendors. Collect what users actually ask the agent, group the questions into types, and write down for each type which source is authoritative and which is the fallback. That list becomes the source rule in code. Build entity resolution first, as its own tool, and then one question-shaped tool per type that takes the resolved ID.

The agents calling this layer may also need shared definitions, so that "active customer" means the same thing in every client. That is a context problem as much as a tool problem, and our guide on shared business context for every AI agent covers it. Bigcircle's MCP servers work covers the consolidation layer.

Common questions

What are MCP tools?#

MCP tools are the operations an MCP server offers for a model to call, such as a lookup or a write. Each tool has a name, a description and a JSON Schema for its inputs, and it may declare an output schema. The model reads those definitions and decides when to call a tool, so the description is the only thing it has to tell two similar tools apart.

What are the best practices for designing MCP tools?#

Shape each tool around a question users ask, not a vendor endpoint. Give it typed options rather than a free query string, and a description that names its prerequisite and its near-miss. Return JSON with sources, as-of dates and explicit statuses, and keep results bounded. Split reads from writes, authenticate every request as the user, rate-limit by caller, and log every call, refusals included.

Should we connect every vendor's MCP server to our agent?#

Only when each vendor answers a different kind of question and you have few of them. When two or more sources answer the same question, the model has to pick a source and decide whose data to trust on every request. That costs tokens and produces inconsistent answers. Put a consolidation layer in front instead, and make the source choice once, in code.

What is an MCP consolidation layer?#

It is an MCP server your team runs between your agents and the vendor systems. It exposes a small set of question-shaped tools and decides which source is authoritative for each question. It resolves names to record IDs, returns structured JSON, authenticates each request as the user, applies rate limits, and logs every call. It does not re-export each vendor's tools under one name, because that keeps the long tool list.

How many tools should a consolidation MCP expose?#

Start with one tool per question type your users actually ask, plus a resolve tool for entities. There is no fixed limit, but on Bigcircle's Solarpunk build direct tool calling degraded at roughly thirty tools. If the list is heading past a few dozen, add discovery or merge overlapping tools, and remove any tool the call log shows nobody uses.

Does fewer, larger tools mean one general query tool?#

No. A tool that runs any SQL or calls any endpoint is one entry in the list and every operation in practice. The model has to write the query, and the permission policy cannot tell a read from a destructive write. Keep each tool scoped to one question type with typed options, and keep reads and writes in separate tools.

How should a consolidation MCP handle authentication?#

Authenticate every request as the calling user. Since the 28 July 2026 MCP specification there are no sessions, so the client sends its token on every request and the server must validate it each time. The layer may not forward the user's token to a vendor, so it holds its own vendor credentials where the model never sees them. It records the user on every call, so the audit trail can say who asked.

How do we know when to change a tool?#

Read the call log on a schedule and look for patterns. Repeated calls over one tool with different IDs call for a batch mode. Two tools alternating for the same question call for a merge or better descriptions. Validation errors on one argument call for a clearer schema. After each change, re-run a fixed set of real questions and compare the tool paths as well as the answers.

Further reading