An agent does not need unrestricted HTTP access to issue a refund. It needs a command whose authority, inputs, and effects are small enough to review.
Design MCP tools as bounded business commands. Their safety should come from enforced contracts and durable state transitions, even when the model chooses the wrong command, repeats a call, or supplies misleading text.
This third article turns the series’ tenant-scoped order service into a usable tool surface. The design objective is not maximum API coverage. It is a set of operations whose permitted effects remain understandable during retries, human review, and partial failures.
Table of contents
Open Table of contents
Choose verbs that bound authority
OpenAPI targets translate MCP requests into HTTP requests, and exposed operations require an operationId. Lambda targets use tool definitions and an invocation-specific event/context format. Source: OpenAPI schema targets; AWS Lambda function targets.
Those integrations can expose existing functionality, but existing endpoints are not automatically appropriate agent tools. A generic execute_sql, call_url, or update_record operation transfers too much authority into model-selected arguments.
Prefer get_order_summary, prepare_refund, and submit_refund. Each names an effect, has a bounded input space, and can be assigned a clear risk tier. A narrow verb still needs backend authorization; its advantage is that the authorization question is reviewable.
Separate preparation from execution where humans must approve the result. Preparation can calculate a proposed refund against current order state without moving money. Submission references a persisted proposal and approved intent. Do not create an approval tool that lets the same agent manufacture human authority.
Figure 1. Recommended contract design narrows the available effect before policy evaluates a call.
Descriptions should explain when the operation is appropriate, its side effects, and the meaning of failure. They guide tool selection; they do not enforce safety. Every material restriction described in prose needs a corresponding backend check or an explicit reason it cannot be guaranteed.
Avoid arbitrary destination URLs and unrestricted object paths. Even a legitimate business command can become an exfiltration tool if its output destination is model-controlled.
Make schemas describe enforceable contracts
AWS documents OpenAPI schema limitations, including unsupported composition features, and warns that downstream model tool specifications impose additional constraints. Source: OpenAPI schema targets.
Validate the exact schema through the chosen target and model client. Do not assume that a schema accepted by a local validator is supported throughout Gateway, protocol translation, and model tool selection.
Use explicit names, simple structures, bounded strings, and constrained enumerations. Represent money in integer minor units with an explicit currency, using currency-specific rules in the backend. Do not use an ambiguous floating-point amount and expect the model to infer rounding or decimal conventions.
Here are the two refund commands as Lambda-target tool definitions, in the inline schema format Gateway accepts:
[
{
"name": "prepare_refund",
"description": "Calculate a refund proposal for one order in the caller's tenant without moving money. Use before submit_refund. Returns a proposal_id and the refundable amount. Does not submit or approve anything.",
"inputSchema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Order identifier, e.g. ord_8F3K2. Max 32 characters."
},
"amount_minor": {
"type": "integer",
"description": "Requested refund in minor units (cents). 1 to 50000."
},
"currency": {
"type": "string",
"description": "ISO 4217 code of the order currency, e.g. EUR."
},
"reason_code": {
"type": "string",
"description": "One of: damaged, late, not_received, goodwill."
}
},
"required": ["order_id", "amount_minor", "currency", "reason_code"]
}
},
{
"name": "submit_refund",
"description": "Submit a previously prepared and human-approved refund proposal. Fails if the proposal is unapproved, expired or changed. Reuse the same idempotency_key when retrying.",
"inputSchema": {
"type": "object",
"properties": {
"proposal_id": {
"type": "string",
"description": "proposal_id returned by prepare_refund."
},
"idempotency_key": {
"type": "string",
"description": "Operation key from the orchestrator; identical on every retry."
}
},
"required": ["proposal_id", "idempotency_key"]
}
}
]
Three details carry the design. Tenant identity is absent from both: it comes from trusted execution context. submit_refund has no amount: the backend resolves the proposal, verifies ownership and approval, and takes the monetary effect from persisted intent, so a second mutable amount never exists. And constraints such as the 50,000 ceiling and the reason-code list sit in descriptions because the Lambda tool-schema format has no enum, pattern or maximum keywords. The model reads them; the backend and Cedar must enforce them. Source: AWS Lambda function targets.
Enforce required fields, size bounds, and unexpected-field handling in backend code as well. Distinguish malformed input, forbidden action, stale proposal, conflicting idempotency key, and unknown outcome. Give the agent a safe next step for each error without revealing another tenant’s data.
Bound responses too. Return the fields needed for the task, with explicit pagination and truncation indicators. A tool response is untrusted content from the agent’s perspective: customer notes can contain instructions. Keep such text distinguishable from authoritative status and approval metadata.
Make retries return the same business result
A network timeout means the caller lacks an answer; it does not establish that execution failed. If the agent retries with a new operation identity, a refund can happen twice.
Create a stable operation identifier in trusted orchestration and reuse it across transport retries. A model-generated key can be accepted as input, but cannot be the sole mechanism preventing two distinct keys from causing the same prohibited business effect.
Persist a record keyed by tenant, command version, and idempotency key. Store a canonical digest of the intended operation. The same key and digest returns the existing operation or result; the same key with a different digest is a conflict.
With DynamoDB, the claim is a conditional write. The condition makes “check, then insert” a single atomic step:
import hashlib
import json
import boto3
from botocore.exceptions import ClientError
table = boto3.resource("dynamodb").Table("refund-operations")
class IdempotencyConflict(Exception):
pass
def claim(tenant_id: str, key: str, command: dict) -> dict | None:
"""Return None if this call owns execution, else the earlier operation record."""
digest = hashlib.sha256(json.dumps(command, sort_keys=True).encode()).hexdigest()
pk = f"{tenant_id}#submit_refund.v1#{key}"
try:
table.put_item(
Item={"pk": pk, "digest": digest, "state": "EXECUTING"},
ConditionExpression="attribute_not_exists(pk)",
)
return None # first caller: execute, then record COMMITTED or UNKNOWN
except ClientError as err:
if err.response["Error"]["Code"] != "ConditionalCheckFailedException":
raise
existing = table.get_item(Key={"pk": pk}, ConsistentRead=True)["Item"]
if existing["digest"] != digest:
raise IdempotencyConflict("same key, different refund intent")
return existing # EXECUTING, COMMITTED or UNKNOWN: never execute again
Within one database, coordinate the idempotency record and state transition in a transaction with appropriate uniqueness and concurrency controls. Define what concurrent callers observe while execution is pending. Avoid a check-then-insert sequence that lets both requests pass.
Figure 2. Recommended state model separates approval validity, execution ownership, and duplicate resolution.
External payment execution is harder. A database transaction cannot atomically commit an unrelated provider’s refund. Use the provider’s idempotency mechanism where available, persist outgoing intent durably, and reconcile ambiguous outcomes before repeating an effect.
Keep “unknown” as an explicit state. A worker crash after sending a refund request should trigger reconciliation, not an automatic transition to “safe to retry.” Document how an operator resolves an outcome when the external system offers no reliable lookup or duplicate protection.
Inject failures before dispatch, after dispatch, after external completion, and before local result persistence. Demonstrate the resulting business effects and recovery path. Passing a sequential duplicate test is insufficient evidence for crash safety.
Treat long-running work as a durable resource
A tool request should not keep a connection open merely because its business workflow includes a queue, external settlement, or human review. Model long-running execution as an application-owned job.
Submission returns a durable operation identifier and a state such as pending approval or queued. Separate status and cancellation tools authorize access to the job using trusted tenant context. A guessed job identifier must not reveal another customer’s result.
Persist the accepted command, schema version, tenant, originating subject, approval reference, and retry identity. Define whether a worker rechecks current membership or executes under a deliberately bounded delegated grant. Either choice needs expiry and revocation semantics.
Cancellation is a request with state-dependent meaning. A job may be cancellable before dispatch but irreversible afterward. Report the distinction honestly rather than returning “cancelled” while an external refund continues.
Use an outbox or equivalent durable handoff when committing business intent and publishing work across systems. Replayed queue deliveries must resolve to the same operation. Callbacks need authenticated origins, ownership correlation, and duplicate handling.
These are application workflow guarantees. An MCP session or Gateway policy session does not replace the job ledger, transaction state, or approval record.
Bind approval to the exact proposed effect
An approval should attest to a persisted intent digest: tenant, operation, object, amount, currency, destination, relevant version, and expiry. The human interface should show that intent from trusted application data, not solely from a model-written summary.
Separate requester, approver, and executor capabilities. High-impact tools may require an approver who cannot modify the proposal or deploy the executing code. Decide which business risks justify that operational cost rather than imposing identical review on every read.
At submission, verify the approver’s authority, approval freshness, intent match, and any single-use condition. Recheck business preconditions atomically with execution. If order state changes materially, invalidate the proposal or require a new review.
Approval is not a general waiver. It must not permit cross-tenant access, bypass transaction constraints, or authorize later parameter changes. For recurring workflows, use a narrowly defined standing authorization with limits and expiry instead of silently reusing an old approval.
Alter one approved field, replay a consumed approval, revoke the approver, and race two submissions. The backend should produce defined outcomes without duplicate or unapproved effects. Record whether revocation applies immediately or at the next documented freshness boundary.
Promote tools through a risk-tiered registry
Maintain an approved tool registry as an operating artifact distinct from the deployed Gateway configuration. Its purpose is to connect contracts to owners and evidence, not merely list names.
Figure 3. Recommended promotion assigns evidence requirements before a tool becomes available to agents.
A practical starting classification is:
| Tier | Example | Minimum promotion evidence |
|---|---|---|
| Read | Order summary | Tenant isolation, output minimization, pagination |
| Reversible write | Internal case label | Authorization, concurrency, retry behavior |
| Consequential write | Refund submission | Exact-intent approval, durable execution, reconciliation, containment |
Risk depends on effect, not HTTP method. A read that returns sensitive bulk data may deserve stronger controls than a reversible write. A “preview” endpoint that sends an email is still a write.
For every entry, retain owner, target type, schema digest, semantic version, permissions, tenant provenance, approval rules, idempotency semantics, maximum output, and incident stop procedure. Require reviewers to inspect both the tool contract and backend enforcement.
Policy validation uses Gateway tool schemas to check action references and input types. Schema consistency is useful, but cannot establish whether an external business effect is correct. Source: Validation and analysis overview.
Compare the registry’s approved schema and target configuration with deployment state. Treat an unexpected new operation, endpoint, credential mapping, or schema change as drift requiring review. A stable tool name can hide a material increase in authority.
What this means in practice
A safe tool is a small business protocol with explicit failure semantics. Its contract explains the effect; its backend enforces ownership, approval, and duplicate handling; its registry entry connects those guarantees to evidence.
Choose one broad write tool and replace it with a persisted proposal plus a narrow submission command. Demonstrate changed-intent rejection, concurrent duplicate handling, and recovery after an ambiguous external result before adding more verbs.