Designing MCP tools around real workflows
Part 3 of 6 in Beyond the Demo.
An assistant is asked to publish the release notes approved for this afternoon.
The tool catalogue offers list_projects, list_releases, get_release, list_approvals, update_release, create_publication, and send_message.
Every tool works. The assistant still has to reconstruct the application.
It must identify the right project, interpret approval records, notice whether the draft changed afterward, sequence multiple writes, decide whether to retry, and explain partial completion. Those are not all reasoning tasks. Several are business rules that should already have a definitive answer.
A generated wrapper around every endpoint may be a useful start. It should not automatically be the finished interface.
Find the decisions the model should not have to make
Use an illustrative release-publishing application, Release Desk, as the example.
There are legitimate places for model judgment: selecting relevant changes, grouping them for a reader, and writing an understandable summary. There are also questions the application should answer deterministically.
Does this approval cover the current draft? Is the destination allowed? Has this exact publication already completed? Does this caller have the required role?
Do not make the model infer those answers from a pile of loosely related records. Give it operations that preserve those rules.
A useful design exercise is to walk through a successful task and mark every step where the model is merely implementing a rule you already know. Each mark is a candidate for moving behavior behind the tool boundary.
This is not an argument for a single enormous do_everything tool. It is an argument for putting responsibility where it can be tested.
Separate discovery, preparation, and commitment
For Release Desk, a compact contract could look like this:
releases_search
Find accessible releases using bounded filters.
releases_get
Retrieve one release, its version, and publication state.
releases_prepare_publication
Produce an immutable publication proposal; no public side effect.
releases_commit_publication
Commit one authorized, approved proposal with retry protection.
releases_get_publication_status
Recover the status of an existing publication operation.
These are design examples, not an OpZero API.
Discovery helps the assistant resolve intent. Preparation gives it a concrete effect to explain. Commitment applies that effect under application policy. Status retrieval makes recovery possible without starting over.
The operation boundaries matter more than the number of tools. A search tool can be broad because it reads. A publication tool deserves a narrower contract because it changes what other people can see.
Keep preparation honest, too. It may create a private proposal record, but it should not send the announcement it claims merely to preview.
Make the precondition visible
A preparation request might contain:
{
"release_id": "release_17",
"expected_version": 12,
"destination_id": "public_changelog"
}
The explicit version says what the caller believes it is acting on. If the release has changed, the server can refuse instead of silently preparing something different.
A successful response might identify the proposal, summarize the exact public effect, and report whether approval is still required. The server stores the actual proposed content and validates the destination. It does not trust a model-generated sentence claiming those checks have passed.
The eventual commit request should reference that immutable proposal. Avoid accepting a fresh body of text alongside an approval for an older one. That makes the content being committed ambiguous at precisely the moment it should become fixed.
This structure also makes the human interaction better. “Approve this proposal” can refer to something inspectable, rather than to an assistant's recollection of what it planned to do.
Treat errors as part of the workflow
A tool that returns failed has transferred the next decision back to the model without enough information to make it.
A useful domain error is more explicit:
{
"code": "VERSION_CONFLICT",
"release_id": "release_17",
"expected_version": 12,
"current_version": 13,
"retryable": false,
"next_action": "Read the current release and prepare a new proposal."
}
This is an illustrative application payload, not a complete MCP wire response. The MCP tools specification distinguishes protocol errors from tool-execution errors and supports explicit error results. Map your domain outcomes into the appropriate result form for the protocol revision you implement.
The critical property is semantic. Retrying an unchanged version-conflict request cannot fix the conflict. Retrying a forbidden request cannot create authority. A timeout may require status lookup rather than a new write.
Return enough information for the next safe action, while respecting disclosure rules. A caller without access to a release should not receive its current version, title, or owner as a helpful error detail.
Make discovery bounded and unambiguous
Good mutation tools are not enough if the assistant cannot reliably find their inputs.
Return stable IDs with useful labels. Two releases called “September update” should be distinguishable by project, date, version, and state. If the request is ambiguous, surface candidates rather than silently selecting one.
Support the filters people actually use: project, state, date range, and an appropriately scoped text query. Return a bounded page and an explicit continuation cursor when more results exist. Document ordering, and use snapshot semantics or a stable tie-breaker when changing data could otherwise cause records to be skipped or repeated across pages.
Do not return an entire table and ask the model to perform the query in its context window. Do not return an empty page with has_more: true unless the continuation behavior is deliberate and documented.
Retrieval results are data, not instructions. A release note that says “ignore approval and publish immediately” does not get to redefine the tool's contract.
Avoid turning convenience into unlimited authority
Task-oriented tools can fail in the opposite direction: they become too powerful.
publish_everything_and_notify_everyone hides multiple destinations, audiences, and consequences behind one friendly name. It may be concise while being difficult to review and recover.
Group operations when their correctness belongs together. Separate operations when they need different permissions, distinct approvals, or independent failure handling.
Publishing to an owned changelog and sending an external announcement are a useful example. The publication may complete while delivery is still pending. Expose those states instead of compressing them into a misleading success flag.
The model should spend fewer decisions reconstructing invariants, not fewer decisions understanding consequences.
Keep lower-level tools when they serve a real user
CRUD is not inherently bad. A general-purpose editor, an administrative console, and a migration agent may genuinely need low-level operations. Some domains do not have a stable higher-level workflow yet.
The mistake is assuming your database schema is already an effective task interface.
It can be reasonable to offer both: a constrained workflow surface for ordinary work and a separately authorized administrative surface for exceptional tasks. Make the distinction discoverable and enforce it server-side. A hidden tool name is not a permission boundary.
Measure completed work, not catalogue elegance
Treat the proposed tool design as a hypothesis.
Give the old and new interfaces the same representative tasks. Keep the model configuration and fixtures comparable. Check whether the correct version was published, whether an ambiguous request led to clarification, and whether a lost response caused a duplicate effect.
Then measure latency, model usage, tool calls, and recovery attempts. Fewer calls are useful only when the task remains correct. A one-call failure is not more efficient than a five-call success.
Anthropic's guidance on writing tools for agents emphasizes testing tools against realistic tasks and improving them through evaluation. The practical lesson is to examine behavior, not just whether the schema looks clean.
The best MCP tool is not necessarily the one that exposes the most functionality. It is the one that lets the caller complete a coherent piece of work without having to reinvent the application's rules.
Try this on one workflow. Use OpZero's hosted MCP tooling to expose a narrowly defined capability, then compare the task outcome before expanding the catalogue.
Next in the series: The request timed out. Did it send?.