MCP Deep Dive, Part 5: Designing Custom MCP Tools Your Agents Actually Use Right
Most MCP tools mirror your REST API and confuse the model. Here's how to design task-shaped tools an agent picks right, calls cleanly, and trusts.
- Author
- Randhir Jassal
- Published
- Reading time
- 15 min read
- Views
- 21 views
The difference between an agent that works and one that flails is almost never the model. It's the tools. Give a capable model forty CRUD tools that mirror your REST API and it will pick the wrong one, mis-format the arguments, and drown in raw rows. Give it a dozen task-shaped tools with sharp descriptions and it just works. This part is the craft of designing tools an agent uses right.
This is Part 5 of a 15-part deep dive on Model Context Protocol (MCP). Parts 3 and 4 built the server and the client — the machinery. This part is about the design of the tools themselves, which is where most MCP projects quietly succeed or fail. We'll use the same Mattrx servers, and every rule comes with the before that confused the model and the after that didn't.
TL;DR
| Aspect | REST-mirror tools (before) | Task-shaped tools (after) |
|---|---|---|
| Granularity | 1 tool per endpoint (~40) | ~12 intent-shaped tools |
| Naming | proc1 / vague | verb_noun, specific |
| Description | "processes data" | what + when + returns |
| Input | free-form strings | enums, formats, required |
| Output | raw rows / giant JSON | compact, answer-shaped |
| Side effects | invisible | annotated (read/write/destructive) |
| Errors | "500" | actionable guidance |
- Design tools around the agent's intent, not your REST endpoints — ~40 CRUD tools → ~12 task tools.
- The description is a prompt — the model selects tools by reading them; say what, when, and what it returns.
- Constrain the input schema (enums, formats, required) — every constraint is a mistake the model can't make.
- Shape the output for reasoning — return the few numbers plus a hint, not 500 raw rows.
- Annotate side effects (read-only / destructive / idempotent) so the host can gate dangerous tools.
- Errors that teach — actionable messages get a corrected retry; "500" gets a hallucinated workaround.
- Curate the toolset — too many tools degrade selection; expose per task.
- Well-shaped schemas keep the tool-call error rate at 0.8% (from mis-formatted arguments).
- Answer-shaped outputs help hold context at 3.5k tokens (down from 14k).
- Task-shaped tools cut wrong-tool selection and helped drop agentic p95 to 1.8s.
The one mental shift: a tool surface is not your API — it's a menu you're writing for a reader who decides in one shot and never asks a clarifying question. Design each tool so the model can select it, call it, and reason over its result without guessing.
The running example: the Mattrx toolset
Mattrx's mattrx-analytics, mattrx-reports, and mattrx-admin servers expose the tools our agents use. The first cut mirrored our internal REST API — forty tiny CRUD operations — and the agents were unreliable: they picked the wrong tool, passed the wrong arguments, and buried themselves in raw event rows. Redesigning the tools (not the model, not the loop) is what made the agents trustworthy. Here's how, rule by rule.
Anatomy of a good tool
get_campaign_kpis
+-- name: verb_noun, specific -> the model FINDS it
+-- description: what + when + returns + -> the model SELECTS it correctly
| how it differs from neighbours
+-- input: typed, enums, formats, required -> the model CALLS it correctly
+-- output: compact, answer-shaped, + hint -> the model REASONS cheaply
+-- annotations: read-only / destructive -> the host GOVERNS it
+-- errors: actionable, corrective -> the model RECOVERS
Every section below is one line of that anatomy.
1. Design around intents, not endpoints
Before
The first toolset was a 1:1 mirror of the REST API — one tiny tool per endpoint.
// BEFORE: mirror the REST API. The agent must orchestrate five calls to answer
// one question — and frequently picks the wrong one.
[McpServerTool(Name = "get_campaign")] public Task<Campaign> GetCampaign(string id, ...);
[McpServerTool(Name = "get_campaign_budget")] public Task<Budget> GetBudget(string id, ...);
[McpServerTool(Name = "get_campaign_events")] public Task<Events> GetEvents(string id, ...);
[McpServerTool(Name = "get_campaign_ctr")] public Task<double> GetCtr(string id, ...);
[McpServerTool(Name = "get_campaign_spend")] public Task<decimal> GetSpend(string id, ...);
After
One tool shaped like the question the agent actually asks.
// AFTER: a tool shaped like an intent — "assess this campaign's health."
[McpServerTool(Name = "get_campaign_kpis")]
[Description("Return a campaign's KPI snapshot (CTR, spend, budget pacing, conversions) for a range.")]
public async Task<CampaignKpis> GetCampaignKpis(string campaignId, string range, CancellationToken ct)
=> await campaigns.GetKpisAsync(principal.TenantId, campaignId, DateRange.Parse(range), ct);
Diagnostic: your REST API is designed for programmers who read docs and compose calls. A tool surface is a menu for a model that reasons in one shot. Forty CRUD tools force the model to orchestrate and mis-select; one intent-shaped tool answers the question directly.
Mattrx metric: collapsing ~40 endpoint-mirroring tools into ~12 intent-shaped ones cut wrong-tool selection dramatically and is part of why agentic p95 dropped to 1.8s — the model reaches the answer in fewer, righter calls.
2. The description is the prompt
Before
A vague name and a description that tells the model nothing about when to use it.
[McpServerTool(Name = "proc")]
[Description("Processes campaign data.")] // the model has no idea when to reach for this
After
The description says what it does, when to use it, what it returns, and how it differs from its neighbours.
[McpServerTool(Name = "get_campaign_kpis")]
[Description("""
Return a campaign's KPI snapshot: CTR, spend, budget pacing, and conversions for a date range.
Use this to assess how a campaign is performing or to diagnose a metric drop.
Returns aggregate numbers only — for the raw event stream, use query_events instead.
""")]
public Task<CampaignKpis> GetCampaignKpis(string campaignId, string range, CancellationToken ct);
Diagnostic: the model chooses tools by reading their descriptions — the description is a prompt injected into its decision. A good one states the purpose, the trigger ("use this to..."), the return shape, and the boundary with adjacent tools so the model doesn't confuse get_campaign_kpis with query_events.
Mattrx metric: sharpening descriptions — especially the "use this when / not that" boundaries — was the cheapest accuracy win we made; the model stopped calling the event-stream tool when it wanted a summary.
3. Constrain the input schema
Before
Free-form string parameters the model has to guess at.
// BEFORE: what values does "type" accept? "format"? The model invents them.
public Task<Report> CreateReport(string type, string format, string range, CancellationToken ct);
After
Enums, required fields, and described formats. The SDK turns these into JSON Schema the model must satisfy.
public sealed record CreateReportArgs(
[property: Description("The report to generate.")] ReportKind Kind, // enum: Performance | Attribution | Spend
[property: Description("Output format.")] ReportFormat Format, // enum: Pdf | Csv
[property: Description("ISO-8601 range, max 90 days, e.g. 2026-06-01/2026-06-30.")] string Range,
[property: Description("Email to notify on completion (optional).")] string? NotifyEmail);
Diagnostic: every constraint you encode is a mistake the model cannot make. An enum beats a free string (the model picks from the allowed set), a required field beats optional-and-guess, and a described format beats hoping the model matches yours. The schema is a guardrail, not just documentation.
Mattrx metric: typed, constrained inputs are the main reason the tool-call error rate sits at 0.8% — the model can't pass a report kind that doesn't exist or a malformed range, because the schema won't let it.
4. Shape the output for reasoning
Before
Return the raw table and make the model sort it out.
// BEFORE: 500 rows x 30 columns dumped into the model's context.
return await db.QueryAsync("SELECT * FROM campaign_events WHERE campaign_id = @id", ct);
After
Return the few numbers that answer the question, plus a short interpretive hint — nothing else.
// AFTER: answer-shaped output. Small, relevant, and cheap to reason over.
return new CampaignKpis(
Ctr: 0.021, CtrDelta: -0.006, // the drop the user is asking about
Spend: 4_120m, BudgetPacing: 0.82,
Conversions: 318,
Window: range,
Note: "CTR down 22% vs the prior period; spend is on pace."); // a nudge, not raw data
Diagnostic: a tool result lands in the model's context, gets reasoned over, and gets paid for in tokens. Return the handful of numbers that answer the question plus a one-line interpretation — not 500 rows the model has to summarize and you have to pay to send. (Raw drill-downs belong in a separate, paged tool — see Part 3's query_events.)
Mattrx metric: answer-shaped tool outputs are a big part of holding context at 3.5k tokens (down from 14k) — the model gets the signal, not the whole table.
5. Annotate side effects
Before
A read and a write look identical to the model — and to the host's approval layer.
[McpServerTool(Name = "get_campaign_kpis")] public Task<CampaignKpis> GetCampaignKpis(...);
[McpServerTool(Name = "delete_audience")] public Task<Deleted> DeleteAudience(...); // looks just as safe
After
Annotate what a tool does — read-only, has an effect, or destructive — so the host can gate the dangerous ones.
[McpServerTool(Name = "get_campaign_kpis")]
[McpToolAnnotations(ReadOnly = true)] // safe: the host may call freely
public Task<CampaignKpis> GetCampaignKpis(...);
[McpServerTool(Name = "create_report")]
[McpToolAnnotations(ReadOnly = false, Idempotent = false)] // has an effect: enqueues work
public Task<ReportQueued> CreateReport(...);
[McpServerTool(Name = "delete_audience")]
[McpToolAnnotations(ReadOnly = false, Destructive = true)] // dangerous: host requires confirmation
public Task<Deleted> DeleteAudience(...);
Diagnostic: the model — and the human-approval layer in the host — need to know which tools merely look and which ones act or destroy. Annotations let a destructive tool require confirmation while a read-only tool runs freely. (These are hints; real enforcement is authorization, which is Part 7 — never trust an annotation as your security boundary.)
Mattrx metric: side-effect annotations let the host auto-run read tools and require a human tick for destructive ones — the reason an over-eager agent has never dropped an audience in production.
6. Errors that teach
Before
An opaque failure the model can't act on.
throw new Exception("Query failed"); // the agent gives up or invents a workaround
After
An error that names the problem and points at the fix — a second chance to steer the model.
if (DateRange.Parse(range).Days > 90)
return ToolResults.Error("range_too_wide",
"Date range exceeds the 90-day maximum. Narrow the range, or use get_campaign_kpis for a summary.");
Diagnostic: an error message is another prompt. "range_too_wide: max 90 days, narrow it" gets a corrected retry on the next turn; "error 500" gets a give-up or a confidently wrong workaround. Write errors for the reader who will act on them.
Mattrx metric: actionable tool errors turn most failed calls into a successful retry within the same agent run — the model reads the guidance and fixes its own call, which is another contributor to the 0.8% final error rate.
Curate the menu
BEFORE: ~40 tools mirroring the REST API
get_campaign, get_campaign_budget, get_campaign_events, get_campaign_ctr,
get_campaign_spend, get_campaign_audience, list_campaigns, ... (x40)
-> the model must orchestrate, and often picks the wrong one
AFTER: ~12 task-shaped tools, named for intent
get_campaign_kpis (assess health) query_events (drill in, paged)
compare_campaigns (A vs B) create_report (action; not idempotent)
...
-> the model reads a short menu and picks in one shot
More tools is not more capability — past a point it's less, because tool selection degrades as the menu grows. Curate the toolset to the intents an agent actually has, and expose different toolsets to different agents rather than one giant catalog to all of them.
The numbers, in one place
| Metric | REST-mirror tools (before) | Task-shaped tools (after) |
|---|---|---|
| Tool count | ~40 (endpoint mirror) | ~12 (intent-shaped) |
| Wrong-tool selection | frequent | rare |
| Tool-call error rate | 6% | 0.8% |
| Context tokens / call | ~14,000 | ~3,500 |
| Agentic p95 | 4.2s | 1.8s |
| Destructive-tool accidents | possible | gated by annotation + approval |
Tool-design checklist
- Shape each tool around an intent the agent has, not a table or endpoint.
- Name it
verb_nounand specific so the model can find it. - Write the description as a prompt: what, when, returns, and how it differs from neighbours.
- Constrain inputs with enums, formats, required — the schema is a guardrail.
- Return compact, answer-shaped output plus a short hint; page the raw drill-downs.
- Annotate side effects (read-only / idempotent / destructive) for the host to gate.
- Make errors actionable — name the problem, point at the fix.
- Curate the toolset per agent; fewer, sharper tools beat a complete catalog.
The honest stuff: when to relax the rules
- A thin internal API with one hardcoded caller. If no model ever selects among your tools, you don't need menu design — a direct call is fine.
- Chasing completeness. Exposing all 40 CRUD tools "to be thorough" actively hurts; selection accuracy falls as the menu grows. Curate.
- Over-shaping output. Compact is good; lossy is not. Don't strip the numbers the model needs to reason — shape, don't amputate.
- Business rules living only in descriptions. A rule the model "should" follow in prose is a suggestion. Enforce it in the tool and in authorization (Parts 7/8), not just the text.
- The god-tool. Fifteen optional parameters on one mega-tool confuses the model as much as forty tiny ones. Right-size to intents, not to extremes.
- Treating descriptions as API docs. Write for the model's selection decision, not for a human reading a reference page. Different audience, different prose.
- Skipping annotations on write tools. An unannotated destructive tool is an accident waiting for an over-eager agent. Annotate every tool that acts.
The model to carry forward
Design the tool for the reader who decides in one shot. Name it so it's found, describe it so it's chosen, type it so it's called right, shape its output so it's cheap to reason over, annotate it so it's safe to run, and word its errors so it's recoverable. A good MCP tool is a good prompt with a typed signature — and getting the tools right does more for agent reliability than any model upgrade.
Three habits that make tools trustworthy:
- Shape tools to intents, not endpoints. One tool per question the agent asks — not one per table.
- Treat the description as a prompt and the schema as a guardrail. Together they decide whether the model uses the tool correctly.
- Curate ruthlessly. Fewer, sharper tools beat a complete API surface every single time.
In Part 6 we move from designing tools to securing who can call them: authentication for MCP with OAuth and Entra ID, done right.
Continue the series — MCP Deep Dive
- Why Model Context Protocol Kills Integration Glue Code for Good
- Inside the MCP Architecture: Hosts, Clients, and Servers
- Build a Production-Grade MCP Server From Scratch
- Build an MCP Client That Connects to Any Tool (and Any Model)
- Custom MCP Tools Your AI Agents Can Actually Trust (you are here)
- MCP Authentication With OAuth and Entra ID, Done Right
- Scoped Authorization for MCP Tools (Least Privilege for Agents)
- Securing MCP Against Prompt Injection and Tool Abuse
- Streaming and Long-Running Tools Over MCP
- Debugging and Observability for MCP in Production
- Rolling MCP Out Across the Enterprise
- Building MCP Servers in C# and .NET 9
- Hosting MCP on Azure at Real Scale
- Wiring MCP Into OpenAI and Agent Frameworks
- Running MCP in Production — Lessons From Mattrx
Further reading
- MCP Deep Dive, Part 4: Build an MCP Client That Connects to Any Tool (and Any Model)
- MCP Deep Dive, Part 6: MCP Authentication With OAuth and Entra ID, Done Right
- Context Engineering for Enterprise AI, Part 1: Context Management (Why RAG Alone Isn't Enough)
Designing an agent's toolset and want a second pair of eyes on the granularity or schemas? I'm always happy to compare notes — reach me at randhir.jassal@gmail.com.
Get the next issue
A short, curated email with the newest posts and questions.