AI agents
Function Calling and Typed Action Contracts
Design function calls as typed proposals that code validates before an external action occurs.
By the end you can
- Define typed function calling as an operational contract rather than a capability label
- Contrast Free-form instruction with Typed tool call in “A calendar agent invited the wrong ‘Alex’ to a confidential meeting”
- Trace “Schema validity can produce false confidence” through a concrete execution path
- Produce “Design a safe function boundary” with evidence for “Invalid or ambiguous arguments are rejected before side effects”
Example
A path argument that merely shared a prefix passed validation and read the wrong files
The call was valid JSON. The schema accepted it. The argument was an ordinary path string. It read files it was never meant to reach.
The server was @modelcontextprotocol/server-filesystem, the official Model Context Protocol reference filesystem server. The flaw is CVE-2025-53110, published 2 July 2025 and rated High at CVSS v4 7.3. Elad Beber of Cymulate reported it. The advisory title names the mechanism exactly: “Path validation bypass via colliding path prefix”.
The defect was not in the schema and not in the model. The server's directory-containment check compared path strings by prefix. A well-formed path that merely shared a prefix with an allowed directory passed validation. It then reached files it was never meant to touch. The record states it in one sentence: “Versions of Filesystem prior to 0.6.4 or 2025.7.01 could allow access to unintended files in cases where the prefix matches an allowed directory.”
Note what was present here rather than what was missing. There was a boundary. It ran on every call. It compared the argument against the allowed directories and returned a verdict. What it could not do was decide which resource the string actually designated. That is the check nobody had written.
The type system underneath is ordinary and old. JSON Schema is a declarative language for describing the structure and constraints of JSON data. Its current release is 2020-12. Anthropic's API defines a tool as a name, a description and an input_schema, and that schema is a JSON Schema object. So a validator can reject a malformed call. It cannot tell you the call was meant. A syntactically valid path and the path this caller was entitled to reach are two different checks. Only one of them is automatic.
- Decision at stake: Design function calls as typed proposals that code validates before an external action occurs. The containment check in @modelcontextprotocol/server-filesystem was exactly such a validator, and it ran on every call.
- Hidden assumption: Valid JSON proves that a function call is semantically correct. In CVE-2025-53110 the argument was well formed, the schema accepted it, and the boundary inspected it before the read went through.
- Primary control question: Schema validity can produce false confidence. A prefix comparison is a real check that answers a real question — just not the question of which file this is.
- Evidence to collect: Invalid or ambiguous arguments are rejected before side effects. The advisory dates the repair to versions 0.6.4 and 2025.7.01. Every release before them shipped the boundary in place and open.
Comparison
Which form of typed function calling fits the task?
How much room does the model get? A free-form instruction gives it all of it. A typed tool call narrows it. A deterministic API client leaves almost none. Narrower is not always better, and the cost of narrowing has been measured.
Tightening the output format costs accuracy on the reasoning itself. A 2024 study scored GSM8K exact-match under progressively tighter output formats. Adding an explicit JSON schema to JSON output cut claude-3-haiku from 86.99 to 23.44. It cut gpt-3.5-turbo from 74.70 to 49.25. In the zero-shot results, gpt-3.5-turbo fell from 76.6 in plain text to 49.3 in JSON.
The mechanism is worth more than the numbers, because it is a mechanism a schema author controls. Tam and colleagues went and looked at what the JSON answers contained: “Upon inspection, we found that 100% of GPT 3.5 Turbo JSON-mode responses placed the "answer" key before the "reason" key, resulting in zero-shot direct answering instead of zero-shot chain-of-thought reasoning.” The contract did not merely describe the output. By putting one field ahead of another it removed the step the field was meant to record.
So judge each option by whether invalid or ambiguous arguments are rejected before side effects. Then hold the three shapes apart. Free-form instruction: the model describes an action in natural language — flexible wording, ambiguous parsing, a poor enforcement boundary. Typed tool call: the model selects a declared function and fields — machine-readable, supports validation, values can still be wrong. Deterministic API client: code constructs the request without model choice — strongest control, best for known transitions, no semantic selection, and no way to handle a request nobody anticipated. A schema the model satisfies can still describe the wrong action. And as the GSM8K figures show, the schema itself is part of what the model is answering.
Free-form instruction
The model describes an action in natural language.
- Flexible wording
- Ambiguous parsing
- Poor enforcement boundary
Typed tool call
The model selects a declared function and fields.
- Machine-readable
- Supports validation
- Values can still be wrong
Deterministic API client
Code constructs the request without model choice.
- Strongest control
- Best for known transitions
- No semantic selection
Structured output is an interface contract, not permission to act
Function calling constrains the shape of a model proposal: tool name, arguments, and expected fields. It does not prove that the proposed values are correct, authorized, current, or semantically appropriate. That is not this course's reading of the technology. It is what all three major model APIs say about themselves, in their own developer documentation.
Anthropic's tool-use documentation describes what Claude hands back: “It then returns a structured call that your application executes (client tools) or that Anthropic executes (server tools).” Google's Gemini documentation gives the execution step its own heading, “Execute Function Code (Your Responsibility)”. Underneath it, the division is spelled out: “The model doesn't execute the function itself. Extract the name and args and execute in your application.” OpenAI's function-calling guide makes step three of its five-step flow “Execute code on the application side with input from the tool call”. It states the obligation directly: “When the model calls a function, you must execute it and return the result.”
Three vendors, three documents, one boundary. The model proposes and the application acts. The schema is about shape. Everything about meaning is still yours. The application remains responsible for identity resolution, validation, authorization, execution, and confirmation. In plain terms: work out which record the name refers to — the step that failed in CVE-2025-53110 — check the values, decide whether this user may do it, run it, and tell the user what happened.
The hardest work sits in what the schema never inspects: which record that name actually points to, and what the user is told once the call has run.
Key idea
Schema validity can produce false confidence
A schema can guarantee that `amount` is numeric while permitting the wrong currency, account, or business meaning. A transfer of the right amount to the wrong account passes every schema written for it. This is not a limitation discovered in the field and reported back to the specification. The specification says it first. The 2022 JSON Schema validation draft opens the relevant section with one sentence: “Structural validation alone may be insufficient to allow an application to correctly utilize certain values.”
The same section then does something most schema authors never notice. It makes `format` an annotation rather than an assertion. Under the mandatory Format-Annotation vocabulary, an implementation that also checks the value's semantics “MUST provide options to enable and disable such evaluation and MUST be disabled by default”. So a schema declaring format "email" or format "date-time" is, by default, not enforced at all. It is a note to whoever reads the schema, not a gate on the value.
How far is schema-valid from correct? τ-bench, published in 2024, scored function-calling agents against the end state of a database in two domains rather than against the text of their replies. gpt-4o reached pass^1 of 61.2 in τ-retail and 35.2 in τ-airline, an average of 48.2. The abstract: “Our experiments show that even state-of-the-art function calling agents (like gpt-4o) succeed on <50% of the tasks, and are quite inconsistent (pass^8 <25% in retail).” The consistency figure is the one to sit with. Fewer than a quarter of retail tasks survived eight independent attempts. A single passing trace tells you almost nothing about the next seven. And when the domain policy was removed from the system prompt, gpt-4o's airline pass^1 fell from 33.2 to 10.8. The rules that made those calls correct were never in the schema in the first place.
Validate semantic invariants. Resolve sensitive identifiers outside the model before execution.
Type checks tell you a call is well formed; the JSON Schema draft says so itself, and τ-bench puts a number on how far well formed is from done.
Visual
What the result envelope omits, the model cannot react to
A tool declaration invites a model proposal. Runtime validation stands between that proposal and execution. All four stages are worth tracing separately. Execution and the result envelope need separate owners and separate tests.
The tool declaration is a name, purpose, input schema, and behavior visible to the model. The model proposal is a structured request selecting a tool and arguments. Runtime validation checks type, identity, policy, state, and authority — five distinct questions. The prefix comparison in @modelcontextprotocol/server-filesystem attempted one of them, and answered it with a string test. Execution is a trusted component performing the permitted operation. The result envelope returns status, data, provenance, and recoverability.
The envelope is the last stage and the one most often left thin. It is also the only one the model reads. It is what comes back into the context after the side effect has happened. Whatever it omits is invisible to every subsequent turn: an operation that half-succeeded, a record version that moved, an error class that distinguishes retry from stop. What the envelope leaves out is what the model cannot react to.
- 1
Tool declaration
A name, purpose, input schema, and behavior visible to the model.
- 2
Model proposal
A structured request selecting a tool and arguments.
- 3
Runtime validation
Checks type, identity, policy, state, and authority.
- 4
Execution
A trusted component performs the permitted operation.
- 5
Result envelope
The runtime returns status, data, provenance, and recoverability.
Steps
Design a safe function boundary
One function boundary is enough for this exercise. Pick the tool whose arguments could send money, mail, or a deletion to the wrong place — something that already causes a real side effect — and redesign its contract.
A public benchmark has already collected the failure you are looking for. The Berkeley Function-Calling Leaderboard is built from 2,000 question-function-answer pairs: 1,680 Python, plus 100 Java, 50 JavaScript, 70 REST API and 100 SQL. One documented failure mode is a unit error. GPT-4 emitted annual_interest_rate: 5 for “an annual interest rate of 5%”, where the schema's number field wanted 0.05. The field is numeric, the type check passes, the call is accepted. The loan is not the loan the user asked for. The leaderboard's own summary of the class: “GPT underperforms in scenarios where the parameters are not immediately available in the user question but instead require some implicit conversions.”
The same benchmark scores the call a model should decline to make. Its function-relevance-detection category — 875 irrelevance-detection entries in the BFCL V2 Live set — tests whether a model withholds a call when no supplied function fits. Withholding is a measurable behaviour, not a matter of tone. Make it one of the behaviours you test.
Work through five moves. Name the action precisely, with a verb that reflects the actual side effect. Constrain arguments: prefer enums, stable identifiers, limits, and explicit units. The unit is the whole of the annual_interest_rate mistake. Separate proposal and commit, so the model stages a request before an approved execution step. Return structured results carrying status, resource version, error class, and verification handle. Test ambiguous values: names, time zones, currencies, duplicates, and stale identifiers.
You are finished when you can show two calls from that redesign. One that passes validation and still asks for the wrong thing. One that the boundary refuses before anything irreversible happens.
- 1
Name the action precisely
Use a verb that reflects the actual side effect.
- 2
Constrain arguments
Prefer enums, stable identifiers, limits, and explicit units.
- 3
Separate proposal and commit
Allow the model to stage a request before an approved execution step.
- 4
Return structured results
Include status, resource version, error class, and verification handle.
- 5
Test ambiguous values
Exercise names, time zones, currencies, duplicates, and stale identifiers.
Authorization downstream leaves the model's guess as the last word
Treat every model-generated tool call as untrusted input to a privileged component. This mental model keeps validation and authorization in the correct layer. There is a numbered, vendor-acknowledged case for it.
Injected text could make M365 Copilot disclose information to an unauthorized attacker over a network. Microsoft published the flaw on 11 June 2025 as CVE-2025-32711, “M365 Copilot Information Disclosure Vulnerability”; researchers named it EchoLeak. Microsoft rated it Critical, with a CVSS 3.1 base score of 9.3, and classified it CWE-74: injection into a downstream component. The record's own description reads: “Ai command injection in M365 Copilot allows an unauthorized attacker to disclose information over a network.” NIST's NVD assessed the same vulnerability independently and scored it 7.5 High. Microsoft states that the service-side fix was already fully deployed, with no customer action required.
CWE-74 is the argument compressed into a classification code. The category exists for exactly this shape: content that arrives as data is handed onward to a component that treats it as instruction. A model's tool call is that content. It is generated text, shaped by whatever entered the context, arriving at a component with privileges the text's author does not have.
This also sets the standard for every later schema change, because schema validity can produce false confidence. A well-formed call is only well-formed. The boundary still has to decide whether this caller is allowed to do this thing to this record.
Put authorization downstream of the call and you have made the model's guess the last word on who is allowed to act.
Key takeaways
- Function calling constrains the shape of a model proposal: tool name, arguments, and expected fields. Google's Gemini documentation states it flatly: “The model doesn't execute the function itself.”
- The application remains responsible for identity resolution, validation, authorization, execution, and confirmation. OpenAI's guide: “When the model calls a function, you must execute it and return the result.”
- The JSON Schema 2020-12 validation draft says structural validation alone may be insufficient for an application to use a value correctly, and its Format-Annotation vocabulary leaves semantic format evaluation disabled by default.
- CVE-2025-53110 (High, CVSS v4 7.3, published 2 July 2025): a prefix comparison in @modelcontextprotocol/server-filesystem let a well-formed path argument reach unintended files — the check ran and answered the wrong question.
- τ-bench measured gpt-4o at pass^1 of 61.2 in τ-retail and 35.2 in τ-airline, with pass^8 below 25% in retail, and airline pass^1 falling from 33.2 to 10.8 once the domain policy left the prompt.
- Treat every model-generated tool call as untrusted input to a privileged component: CVE-2025-32711 was classified CWE-74, injection into a downstream component, and rated Critical at CVSS 3.1 9.3 by Microsoft.