Part 7 · One Agent, Five Platforms
Every vendor studio makes it easy to start by clicking "add tool". That's the wrong place to start. If the tools are designed inside one platform, they quietly take on its shape, and the comparison stops being fair before it begins.
So before opening any studio, I'm writing the tool contract: one definition of every tool the invoice-exception agent can call, which all five platforms must host or call unchanged.
The tool list
The nine tools come from the flow in Part 2. Six read, three write.
Read:
-
get_invoice: errorsINVOICE_NOT_FOUND -
get_purchase_order:PO_NOT_FOUND,PO_CLOSED -
get_goods_receipt:GR_MISSING -
run_three_way_match(deterministic):TOLERANCE_EXCEEDED, for information -
get_supplier_history:SUPPLIER_NOT_FOUND -
check_duplicate_invoice:DUPLICATE_INVOICE
Write:
-
request_approval(workflow):APPROVER_NOT_FOUND -
post_invoice_adjustment(ERP, needsapproval_id):APPROVAL_MISSING,PERIOD_CLOSED -
notify_supplier(message, needsapproval_id):APPROVAL_MISSING
The list is deliberately small. Each tool does one business action, with a name a finance person would recognise. There's no generic call_erp or run_query, because a tool that can do anything can't be governed.
Read versus write
The split matters more than any other property of the contract.
Read tools change nothing, so the agent can call them as often as it needs to investigate. run_three_way_match is also deterministic: the same invoice, PO and receipt always give the same answer, because the matching is a rule, not a model judgement.
Write tools are where the risk sits, and each one is guarded differently:
-
request_approvalcreates a task for a person. It's a write, but a safe one: it asks, it doesn't decide. It returns theapproval_idonce a person approves. -
post_invoice_adjustmentandnotify_supplierrequire thatapproval_idas an input. Without a valid one, the process API returnsAPPROVAL_MISSING.
This is the gate from Part 6, expressed in the contract itself. A required approval_id field in the schema means the agent can't even form a valid call without having asked first. The real check still happens in the process API, but the contract makes the rule visible to everyone who reads it.
Input and output schemas
Every tool has an input schema, and every tool in this contract also has an output schema. MCP makes the output schema optional, but when a tool declares one, the server must return structured results that conform to it. That's exactly what we want for comparison: the same invoice must produce the same shape of answer on every platform.
Here is post_invoice_adjustment, trimmed:
{
"name": "post_invoice_adjustment",
"title": "Post invoice adjustment",
"description": "Posts one approved adjustment to an invoice in the ERP. Use only after request_approval has returned an approval_id for this exact invoice, action and amount. Fails with APPROVAL_MISSING if the approval is absent, expired, used or does not match.",
"inputSchema": {
"type": "object",
"properties": {
"invoice_id": { "type": "string" },
"adjustment_type": { "type": "string", "enum": ["price", "quantity", "tax_code"] },
"amount": { "type": "number" },
"currency": { "type": "string" },
"approval_id": { "type": "string" }
},
"required": ["invoice_id", "adjustment_type", "amount", "currency", "approval_id"],
"additionalProperties": false
},
"annotations": { "readOnlyHint": false, "destructiveHint": true, "openWorldHint": false }
}
Three habits are worth copying. Use enums wherever the business has a fixed list. Set additionalProperties: false, so a model can't invent fields. Make the output return identifiers the next step needs, such as a posting_id, rather than prose.
Descriptions written for the agent
The description is the part most teams write last, and it's the part the model actually reads. A good tool description answers three questions: what the tool does, when to use it, and what happens when it fails.
Compare "Posts invoice adjustment" with the description above. The second tells the agent the order of steps, the precondition and the error to expect. That reduces wrong calls far more than any instruction buried in a long system prompt, because it sits right next to the tool the model is choosing.
I keep descriptions free of platform detail. No connector names, no endpoint paths, no vendor terms. If a description only makes sense on one platform, it isn't part of the contract.
Named errors
MCP separates two kinds of errors. Protocol errors, such as an unknown tool or a malformed request, come back as JSON-RPC errors. Tool execution errors, such as a failed API call, invalid input or a business rule, come back inside the tool result with isError: true, so the model can read them and correct itself.
Every business error in this contract is a named tool execution error with a clear next step:
-
GR_MISSING: the goods haven't been received yet. Don't retry now; hold the invoice and say so. -
PO_CLOSED: procurement decides. Summarise and route, don't adjust. -
APPROVAL_MISSING: callrequest_approvalfirst. Never retry the post with a made-up ID. -
PERIOD_CLOSED: the accounting period is locked. Escalate to finance.
The name tells the agent what kind of problem it is; the message tells it what to do next. "Error 500" tells it nothing, so it retries, and retries cost money and time. Part 8 goes deeper into error design.
Annotations are hints, not controls
MCP tools can carry annotations such as readOnlyHint, destructiveHint, idempotentHint and openWorldHint. In this contract, the six read tools are marked read-only. post_invoice_adjustment is marked destructive, because it changes an existing ERP record. notify_supplier is marked open-world, because it reaches outside the company.
They're useful: a client can show a confirmation for non-read-only tools, and a gateway can apply stricter policies to them. But the specification is clear that clients must treat annotations as untrusted unless they come from a trusted server. An annotation describes a tool; it doesn't enforce anything. The approval_id check in the process API is still the control.
How each platform hosts the same contract
The contract is fixed. What changes is how each platform implements it:
- Host it: expose existing APIs as MCP tools on the platform. Series 1 did this for MuleSoft with the MCP Connector, and the bonus post showed how much of an MCP server a good API layer already is.
- Call it: have the platform's agent consume an MCP server hosted somewhere else.
- Map it: where a platform prefers its own tool format, map each tool one to one, keeping names, schemas and errors identical.
For each build, I'll check that all nine tools appear with the same names, the same schemas and the same named errors, and that the write tools still fail without an approval_id. Any difference is a finding, and it goes into the tool exposure score in the scorecard.
Write the contract once. Then every platform has to meet it, rather than the other way round.
Which named error would save your agents the most retries?
This series describes a reference build on a fictional company. Protocol details are based on the MCP 2026-07-28 specification as of October 2026; check current specs before you build.

Top comments (0)