Blog · · 10 min read

Agent tool schema design for legacy product actions

Agent tool schema design for legacy product actions

Giving an agent direct access to a legacy API often produces the wrong action with valid JSON. Broad CRUD endpoints hide business rules, descriptions overlap, and transport errors say little about the actual outcome. One loose path parameter can even turn prompt injection into unintended file access, as a reported MCP filesystem server issue illustrates.

Agent tool schema design puts a narrow contract between the model and the existing product. The contract names one business action and limits its inputs. It also carries trusted authorization context outside model control and returns a typed outcome. The legacy service stays in place; only the interface available to the agent changes.

Use a tool gateway, not raw endpoints

A product API and an agent tool solve different interface problems. A product API usually exposes resources for application developers who understand the domain. An agent tool must help a model choose the correct capability and construct a valid request from incomplete natural-language context.

Put a tool gateway between the agent runtime and existing services:

User request
  -> agent runtime
  -> tool schema selection
  -> policy and validation gateway
  -> legacy API adapter
  -> existing service
  -> typed tool outcome
  -> agent runtime

The gateway does not replace the service's authorization or validation. It adds a model-facing contract and rejects unsafe or ambiguous calls before they reach the old endpoint. The service remains the final authority for business state.

OpenAI's function-calling guide defines function tools with a name, description, JSON Schema parameters, and optional strict enforcement. This format handles model-to-application calls, but schema validity does not make a product capability safe. The contract still needs a clear business intent, effect boundaries, policy checks, and outcomes the workflow can interpret.

Why broad legacy APIs confuse agents

Legacy endpoints often assume knowledge that is not written into the request schema. A human developer knows that changing a ticket's status to closed requires a resolution, that only a manager may refund a settled payment, or that assigning an account to another region triggers downstream work. A model sees fields and descriptions.

Most avoidable failures come from a short list of interface defects:

  1. A generic update endpoint represents many unrelated actions.
  2. Similar tools have overlapping names or descriptions.
  3. Free-form strings accept values the product does not support.
  4. Required preconditions are hidden in service code or tribal knowledge.
  5. Identity, tenant, and permission data appear as model-controlled arguments.
  6. HTTP success is mistaken for business success.

The MCP issue above reported vague mutation descriptions, unbounded path strings, and highly overlapping directory-tool descriptions. The author connected those conditions to broad mutation scope, path traversal risk, and nondeterministic tool selection. The report covers one server, so it is not a universal benchmark. It does show a concrete design problem: a model cannot reliably honor a boundary that the contract never states.

Anthropic makes the same interface point from a model-usage perspective. Its tool prompt-engineering appendix recommends treating tool definitions with the same care as prompts, including example usage, edge cases, input requirements, and clear boundaries between similar tools.

Apply agent tool schema design to user intent

Start with a real product action, not an endpoint. Define the smallest capability that completes one recognizable job and that the system can authorize, observe, and retry as a unit.

A ticketing API might provide this endpoint:

PATCH /tickets/{ticket_id}
{
  "status": "...",
  "assignee_id": "...",
  "priority": "...",
  "resolution": "..."
}

Do not expose it as update_ticket with every field optional. Split it into capabilities such as assign_ticket, change_ticket_priority, resolve_ticket, and reopen_ticket. Each tool can then state when it applies, what it changes, and what preconditions must hold.

Inventory product capabilities before writing schemas

Take one existing workflow and list its user-visible actions. For each action, record:

  • the legacy endpoints or jobs it invokes
  • the state that must exist before it runs
  • the records or external systems it may change
  • the permission required from the current actor
  • whether a safe preview is possible
  • whether retrying is safe
  • the business outcomes the caller needs to handle

Exclude endpoints that only support internal plumbing. Give the model tools for the actions it may legitimately choose rather than for every network request behind those actions.

Use one sentence to test the boundary: "This tool lets an authorized support operator assign one open ticket to one eligible teammate." If the sentence contains several unrelated verbs or cannot state an authorization boundary, split the tool again.

Write a bounded input schema

A function calling schema should make invalid requests difficult to express. Prefer required identifiers, enums, length limits, and explicit optional fields over free-form objects. OpenAI's guide recommends clear names and parameter descriptions, enums and object structure that make invalid states unrepresentable, and strict mode where applicable.

The assignment capability can use this model-facing definition:

{
  "type": "function",
  "name": "assign_support_ticket",
  "description": "Assigns one open support ticket to an eligible teammate. Use only after the user identifies the ticket and intended assignee. Does not change status, priority, or message content.",
  "parameters": {
    "type": "object",
    "properties": {
      "ticket_id": {
        "type": "string",
        "description": "Stable support ticket ID, not a display title."
      },
      "assignee_id": {
        "type": "string",
        "description": "Eligible teammate ID selected from an authorized lookup result."
      },
      "expected_version": {
        "type": "integer",
        "minimum": 1,
        "description": "Version last read by the agent, used to reject stale updates."
      },
      "dry_run": {
        "type": "boolean",
        "description": "When true, validates the assignment without changing the ticket."
      }
    },
    "required": ["ticket_id", "assignee_id", "expected_version", "dry_run"],
    "additionalProperties": false
  },
  "strict": true
}

The description says what the tool does and what it does not do. The closed object rejects undeclared fields, while expected_version makes stale-state handling explicit and dry_run lets the product preview the effect before committing it. Validate identifier prefixes and lengths in the gateway instead of assuming the model-facing schema enforces every business rule.

Do not put actor_id, tenant_id, role, or access token in this schema. Those values must come from the authenticated product session. If the model can choose them, they are arguments rather than controls.

Attach effect and policy metadata outside the prompt

The schema tells the model how to call the tool. The gateway still needs runtime metadata that the model cannot alter. Keep a registry entry beside each implementation:

tool: assign_support_ticket
effect: write
resource: support_ticket
authorization: support.ticket.assign
approval: required_when_cross_team
retry: idempotent_with_action_key
timeout_ms: 3000
audit: full

Treat this as application configuration, not a function parameter. The gateway uses it to decide whether to allow the call, request approval, apply a timeout, and record an audit event. A model-produced explanation can help a reviewer, but it cannot grant permission.

For write tools, generate an action key before calling the adapter. Pass that key through to any downstream service that supports idempotency. If the legacy API cannot accept it, store the key and result in the gateway so a resumed agent run can retrieve the prior outcome instead of repeating the mutation.

Return typed business outcomes

An agent should not have to infer workflow state from arbitrary text or HTTP status. Normalize legacy responses into a small outcome contract:

{
  "outcome": "applied",
  "action_id": "act_01jz8k2p9m",
  "resource": {
    "type": "support_ticket",
    "id": "tkt_a1b2c3d4e5f6",
    "version": 18
  },
  "message": "Ticket assigned to the requested teammate.",
  "retryable": false
}

Use a controlled outcome set such as preview, applied, rejected, conflict, not_found, temporarily_unavailable, and unknown. Add machine-readable details for the states your workflow can repair. For example, a conflict can include the current version and tell the agent to read again. A rejected result can identify the missing permission without exposing sensitive policy internals.

Reserve unknown for the dangerous case where the gateway cannot tell whether the legacy operation completed. Do not retry that state automatically. Reconcile it by action key, query the product record, or send it to a human-owned recovery queue.

Adapt without leaking legacy complexity

The adapter translates one stable tool contract into whatever the existing product requires. It may call several endpoints, map old field names, or invoke a background job. Keep those details behind the gateway.

A minimal execution sequence looks like this:

validate_schema(call.arguments)
context = trusted_session_context()
policy = authorize(context.actor, context.tenant, tool, arguments)
if policy.requires_approval:
    return approval_requested(tool, arguments, policy)
if arguments.dry_run:
    return adapter.preview(arguments, context)
action = reserve_action_key(run_id, tool, arguments)
result = adapter.execute(arguments, context, action.key)
return normalize_outcome(result, action)

Run schema validation and authorization again at execution time, even if an earlier planning step checked them. Agent runs can pause, users can lose permissions, and product state can change between planning and execution.

Handle failure and recovery explicitly

Failures should map to a workflow decision rather than a generic exception. Use these rules:

  • Reject invalid arguments without calling the legacy service.
  • Return rejected for a denied policy decision and never ask the model to bypass it.
  • Return conflict when an optimistic version check fails, then read fresh state before reconsidering the action.
  • Retry temporarily_unavailable only within a fixed attempt and time budget.
  • Reuse the action key for every retry of the same intended mutation.
  • Escalate unknown instead of assuming success or failure.
  • Store the selected tool, validated arguments, actor, tenant, policy result, action key, outcome, and timestamps in the audit record.

A legacy service may return a 200 response with a body saying no record changed. Another may time out after committing the write. The adapter must understand those product-specific semantics and convert inconsistent transport behavior into outcomes the agent workflow can handle.

Verify the tool contract before enabling writes

Anthropic recommends testing how the model uses tools across many example inputs and then changing the definitions when recurring mistakes appear. Build that loop into release criteria.

Start with a table-driven test set:

CaseExpected behavior
Clear valid assignmentSelect assign_support_ticket with exact IDs
User asks to close and assignAsk for confirmation or use two separate tools
Unknown teammate nameCall an authorized lookup tool before assignment
Model supplies another tenant IDIgnore the value because tenant context is not an argument
Stale ticket versionReturn conflict; read current state before retrying
Duplicate execution after resumeReturn the prior result for the same action key
Unauthorized cross-team moveReturn rejected or request configured approval
Timeout with uncertain commitReturn unknown; reconcile before another write

Then replay real product requests in dry-run mode. Measure tool selection, argument validity, denied calls, conflicts, recovery behavior, and unintended side effects separately. A high task-completion rate can hide a dangerous mutation path, so failures need their own assertions.

Before enabling a write tool, require all of the following:

  • one business action per tool
  • a description that states use and non-use cases
  • bounded inputs with no model-controlled identity or credentials
  • server-side authorization and business validation
  • a preview or approval path for sensitive effects
  • typed outcomes, including uncertain completion
  • idempotent retry handling or an explicit no-retry rule
  • audit records and recovery ownership
  • adversarial tests for ambiguous requests and injected instructions

Avoid the common shortcuts

Wrapping an OpenAPI document is not the same as designing agent tools. It can be a useful inventory, but exposing every endpoint preserves the ambiguity you need to remove.

Do not combine reads and writes in one tool merely to reduce the tool count. The runtime should be able to apply different policy, approval, retry, and audit rules to each effect class.

Do not rely on a long description to compensate for an unbounded schema. Descriptions guide selection; validation enforces shape. The reported MCP filesystem issue shows why both boundaries matter.

Do not return prose alone. A friendly message is useful for the user, but the workflow also needs a stable outcome code, resource version, retryability, and action identifier.

Keep reviewing the contract after launch. Tool traces expose confusion that static review misses. Tighten names, boundaries, schemas, and examples when the same mistake recurs. Keep the tool name and output contract stable when possible so evaluations and downstream workflow logic remain comparable.

What to do next

Pick one high-value write action in your product and apply this agent tool schema design method. Write the one-sentence capability boundary, list its preconditions and effects, and build a dry-run adapter with a typed outcome before connecting it to an agent. Test ten valid requests, ten ambiguous requests, and ten unauthorized or stale-state requests. Enable the real mutation only when the gateway rejects every unsafe case and can reconcile an uncertain result without repeating the action.

References

  • OpenAI function calling guide supports the function-tool structure, JSON Schema parameters, strict mode, naming guidance, bounded inputs, and function-definition best practices.
  • Anthropic: Building effective agents supports treating tool definitions as interface design, documenting boundaries and edge cases, testing model usage, and iterating on recurring mistakes.
  • MCP servers issue #3752 is a practitioner report about vague descriptions, overlapping tools, and unbounded path parameters in one filesystem server. It supports the failure example, not a general incidence claim.

Be first in line.

Join the waitlist and we'll email you the moment it's ready. No sales call.

Try the demo →
or talk to us