Lately I’ve been reading about MCP design looking for advice.
What I’ve found is recommendations for descriptions, examples, context, and explanations about when a tool should or should not be used.
And while reading it, something rang a bell.
Not because the advice is wrong.
But because it felt strangely familiar.
Even with terminology and technology being different, the underlying problems were not.
I’ve spent most of my career seeing these same patterns appear under different names.
- Ambiguous interfaces.
- Hidden side effects.
- Poorly defined boundaries.
- Consumers being forced to infer more than they should.
The more I thought about it, the more I realized the “problem” is not specific to MCP.
It’s an uncertainty problem.
And software engineering has been reducing uncertainty for decades.
The Consumer Changed. The Goal Didn’t.
Many of the lessons currently being “rediscovered” in MCP ecosystems are not new.
- Tell Don’t Ask
- Command Query Separation
- Design by Contract
- Intent-driven naming
- Explicit state transitions
- Strong typing
These ideas existed long before LLMs.
The interesting part is not the principles.
It’s the consumer.
Traditionally, interfaces were consumed by humans.
Or more precisely, by code written and maintained by humans.
When an interface was ambiguous, developers compensated by:
- Reading documentation
- Inspecting source code
- Asking questions
In other words, developers built context over time.
Agents do none of those things.
They:
- Read a schema
- Read a description
- Reason
- Act
For an agent, the interface is not documentation.
It is the product.
Therefore, ambiguity in the interface becomes ambiguity in behavior.
Immediately.
Silently.
And because of that, design principles become much harder to ignore.
The Goal Is Not Simplicity
One thing I repeatedly found in the discussions is the desire to make things simpler.
- Fewer words
- Shorter descriptions
- Smaller interfaces
- Less information
That is optimizing the wrong thing.
Simple systems can be confusing while Complex systems can be clear.
Consider the following:
process(data)
Simple. Short. Easy to write.
And Almost meaningless.
Now compare it to:
activate_pending_user_account(user_id)
Longer. More specific.
But Much clearer.
The first one requires interpretation, the second communicates intent.
The goal is reducing uncertainty, not information.
Those are very different objectives.
Good design is not measured by how little information exists.
It is measured by how little inference is required.
Every time a consumer must infer:
- Intent
- Applicability
- Side effects
- Ownership
- Domain boundaries
Uncertainty increases.
If this applies to human engineers, it applies even more to agents.
The goal is not reducing information.
The goal is reducing uncertainty.
Those are very different objectives.
In my experience, good design is about reducing complexity as complexity is often inherent to the problem.
The real challenge is reducing ambiguity and creating enough clarity for better decisions to emerge.
Tell, Don’t Ask
In object-oriented design, instead of asking for state and making decisions elsewhere, you tell what outcome you want and allow the object to enforce its own rules.
The same principle applies surprisingly well to tool design.
A tool should communicate intent directly.
Consumers should not need to inspect multiple candidates and infer which capability actually applies.
The capability should be evident from its definition.
Given the next example:
disqualify
What does that mean?
- Remove it?
- Reject it?
- Mark it as lost?
- Close it?
The consumer is now responsible for discovering the domain.
Now compare it to:
mark_deal_lost
Intent is explicit. Ambiguity is reduced.
The decision moved into the interface design where it belongs.
If a consumer must reason extensively about which tool applies, you don’t have a description problem.
You have a boundary problem.
Command Query Separation (CQS)
A capability either returns information or changes state.
Not both.
Imagine a tool that retrieves information but also performs a state mutation as a side effect.
The consumer invokes it expecting a read.
A write happens anyway.
Now the consumer has an incomplete model of what changed and recovery becomes guesswork.
This is not new.
It is the exact failure mode CQS was designed to prevent.
The difference is that a human developer can eventually discover the side effect by reading code.
An agent cannot.
Whenever possible, reads and writes should remain separate at the interface boundary.
And when state changes occur, they should be explicit parts of the contract, not buried inside a paragraph of descriptive text.
Reducing uncertainty often means making behavior obvious.
Positive Constraints, Not Blacklists
From what I’ve read, this is where many discussions seem focused on the wrong optimization.
I’ve seen recommendations revolve around adding detailed explanations of when a tool should not be used.
Descriptions begin to look like this:
Do not use for active opportunities
Do not use for converted leads
Do not use for archived records
Do not use for previously qualified prospects
These statements are not wrong.
The problem is that they scale poorly.
The set of invalid situations is often unbounded.
Every new workflow, state, exception, or capability …
Adds another item to the list. Or creates the risk of bleeding into neighboring domains.
Eventually, descriptions become a historical record of every mistake the system has ever made.
Positive constraints work differently.
- Describe valid cases instead of invalid ones
- State preconditions
- Define applicability
- Make the valid set explicit
This is not an AI pattern.
It is the same philosophy behind:
- Design by Contract
- Strong typing
- Enumerated values
- Explicit state machines
The valid set is usually much smaller than the invalid one.
And much easier to reason about.
If a tool requires a large not_for section, it is often a signal that the interface is compensating for a design decision that should have been made upstream.
Reducing uncertainty is usually easier when we define what is true rather than enumerating everything that is not.
Intent And Namespace Do Most Of The Work
One of the most common mistakes I see is exposing data operations instead of domain capabilities.
Look at these:
update_lead
set_status
modify_record
These expose implementation details.
Consumers must understand the data model before understanding the domain.
On the other hand, these:
mark_deal_lost
convert_lead_to_customer
archive_duplicate_record
These expose intent.
To summarize:
- A tool named after a data operation exposes implementation
- A tool named after an outcome exposes meaning
Namespaces amplify this effect.
A tool under:
deals.mark_lost
is easier to reason about than:
mark_lost
before the description is even read.
The namespace already eliminates entire categories of ambiguity.
The consumer’s search space becomes smaller.
The amount of required inference decreases.
And again, this is not an AI-specific pattern.
It is the same discipline that makes a service layer understandable to another engineer six months later.
Many AI Failures Are Interface Failures
A surprising number of AI failures are framed as reasoning failures.
But more often than not, they are interface failures.
- The agent selected the wrong tool because intent was ambiguous.
- The agent produced an unexpected outcome because side effects were hidden.
- The agent entered an invalid state because constraints existed in prose rather than in the contract.
These are not new classes of failures.
They are familiar interface design failures exposed by a new type of consumer.
The consumer changed.
The uncertainty did not.
We are simply seeing it sooner.
Remember the AI multiplier idea?
I think this is another example of it.
Ambiguous interfaces do not become clearer because an agent is using them.
They become more expensive to ignore.
A Different Checklist
Most MCP discussions eventually arrive at a question like:
Did I describe this tool well enough?
I think there are more useful questions.
- Does this tool have a single, unambiguous intent?
- Does it read state, write state, or both?
- Are valid inputs expressed positively in the schema?
- Does the namespace make the domain boundary obvious?
- Can I explain when this tool applies without relying on a list of exceptions?
- Does this tool expose a domain capability or merely a data operation?
If those answers are clear, the description often writes itself.
If they are not, additional prose rarely fixes the underlying problem.
Food for Thought
The more I think about MCPs, the less I think this is a story about tool descriptions.
Descriptions matter.
Good descriptions absolutely help.
But descriptions should clarify a good interface, not compensate for a poor one.
If a tool requires increasingly long explanations, examples, exceptions, and caveats, it is worth asking whether the problem is really the description.
Or whether the interface is asking the consumer to infer too much.
The deeper pattern is much older.
- Tell Don’t Ask
- Command Query Separation
- Design by Contract
- Intent-driven naming
All of them are attempts to achieve the same thing:
Reduce uncertainty.
The agents are new.
That goal is not.