Capability: MCP tool (external server)
Tools served over the Model Context Protocol
come from a server the agent does not own. The server defines each tool's
name, description, and inputSchema and implements the handler; the
host/client discovers them via tools/list, chooses which to present to the
model, and invokes them via tools/call. So an optimizer here can change how
the agent perceives and is offered those tools, and nothing else.
If the fix needs a tool's types, required fields, or behavior to change, this
capability has been outgrown: negotiate the change with the server owner, or move
the logic into an agent-owned tool and optimize that with the tools capability
instead.
The edit boundary — read this before proposing anything
| Edit | Owner | Allowed here |
|---|---|---|
tool description, per-parameter description, in-description examples |
client presentation | yes |
which of the server's tools the model sees (add / remove) |
client/host curation | yes |
the wire inputSchema — type, required, enum, maximum, properties (schema) |
server | no |
the handler implementation (code) |
server | no |
a new tool that runs server-side logic (compose) |
server | no |
The reason is not politeness: a candidate carrying a schema or handler edit is
invalid against the real server. It cannot be deployed, it will fail at
tools/call, and the run still pays full rollout cost to score it. The safe
levers below are the whole edit space.
The policy checks the edit's LABEL, not its effect — so stay inside the
boundary deliberately. apply() refuses any edit whose kind is outside the
policy and reports the refusal, so a {"kind": "schema"} or {"kind": "code"}
edit comes back as a visible refusal rather than a silent no-op. But two allowed
kinds can carry a forbidden change through:
- a
paramsvalue is shallow-merged intoparameters, so a value containingproperties,type,required, orenumrewrites the wire schema and is not refused. Writeparamsvalues that touch onlyproperties.<field>.description. - an
addvalue is appended verbatim, so acodekey on it lands unrefused.addmeans expose a tool the server already serves; never invent one.
validate() will not catch either — it checks well-formedness only (see
"Artifact + handlers"), and reports ok: true on a schema-rewritten artifact.
Nothing downstream re-checks the boundary, so the discipline is yours.
The effective policy is policy.json in the capability dir (not
inputs/policy.json) — that is the path cap_evolve.tool_surface.load_policy
reads — else the restricted default above. If an MCP client genuinely supports
client-side schema overrides, widen it deliberately and record why.
The four safe levers
- Re-describe a tool — rewrite a terse server description into the what / when / when-NOT / returns / limits the model reads to select. The highest-leverage edit, because selection is driven almost entirely by name + description.
- Annotate per-parameter docs — pin format, units, and caps in the
description of an existing field, never its
type. - Add in-description examples — a concrete well-formed call so the model
fills arguments correctly. Ex:
get_record(record_id="A-1042"). - Curate the exposed set (
add/remove) — hide overlapping or legacy tools so the needed ones stand out;adda served tool the host isn't surfacing. MCP servers may also change their own list at runtime and emitnotifications/tools/list_changed;add/removehere is your curation of what the model sees, never a change to the server.
Before / after
Re-describe a terse server tool. The server ships "description": "kb search",
so the model cannot tell when it applies.
- "description": "kb search"
+ "description": "Search the internal knowledge base and return matching article
+ snippets with their URLs. Use when the user asks a how-to or policy question
+ that is likely documented. Returns at most 10 hits; refine the query if empty."
Pin a parameter's format without touching the schema. The schema says
{"limit": {"type": "integer"}} and the model sends 1000, so the call fails.
"parameters": { "type": "object", "properties": {
- "limit": { "type": "integer" }
+ "limit": { "type": "integer", "description": "Max hits to return (server caps at 10)." }
} }
Only the field's description is added. Changing its type or adding maximum
would be a schema edit — forbidden here, and (per the boundary section) not
refused for you.
Trim the exposed set. Hide rarely-correct, easily-confused tools so the ones the agent needs stand out:
[ { "tool": "legacy_export_v1", "kind": "remove" },
{ "tool": "legacy_export_v2", "kind": "remove" },
{ "tool": "debug_dump", "kind": "remove" } ]
Failure modes to avoid
- Documenting behavior the server does not have. A description that overpromises — filters, sort orders, or limits the server ignores — produces confident wrong calls. Describe only what the server actually supports.
- Removing a tool the agent needs rarely. Remove for overlap and confusion, not for low call count.
- Trusting server-supplied metadata. Descriptions and annotations arrive from
a third party and are untrusted input to the model: a compromised server can
hide instructions in a
descriptionthe model reads and the user never sees, or slip in tools vialist_changed. Review every description before exposing it. - Widening the schema from here. If the model genuinely needs a constraint the
schema lacks, that is a server change or an agent-owned wrapper (
tools).
MCP surfaces a tool-execution error as a normal result with isError: true
and an actionable message, which the host feeds back to the model so it retries
with fixed arguments; a protocol error is a JSON-RPC failure the model cannot
act on. When re-describing is the only lever, document the failure mode in the
description so the model self-corrects into the recoverable path.
Artifact + handlers
tools.json — the exposed MCP tool defs {name, description, parameters, examples}. scripts/abstract.py sets this capability's restricted policy and
delegates to cap_evolve.tool_surface:
materialize(dir)— flatten to named text components for a text optimizer.apply(dir, edits)— applies edits whosekindis in the policy, returns{changed, refused}.validate(dir)— well-formedness only: non-empty artifact,namepresent, no duplicate names, non-empty descriptions,parametersis an object. It does not check the edit policy.is_empty(dir)— whether the artifact is still an empty seed.
How to run
python scripts/check.py
python scripts/run.py --path <capability_dir>
References
references/concepts.md— the MCP client/server model, the Tool object's fields quoted from the 2025-06-18 spec, why the policy is restricted, the four behavior-hintannotationsand why they are untrusted, human-in-the-loop on sensitive calls, and the tool-poisoning / shadowing /list_changedattack surface, with cited sources. Load before the first edit on a server you don't control, or when you need the spec citation for what the server owns.