Блог • AI / Agent
Стек AI Agent 2026: как сочетать LLM, MCP, Function Calling и JSON Schema?
You are wiring an AI Agent and the docs mention LLM, MCP, Function Calling, and JSON Schema at once. All four look like they “let the model call tools,” so the choice feels like pick-one.
They are not four substitutes. The LLM reasons, Function Calling serializes the decision into executable JSON, MCP reaches external services and returns structured results, and JSON Schema is the contract language across those three layers.
В этой статье:
- What each layer owns—and what it must not own
- How one tool call flows from the model to MCP and back
- How Function Calling parameters stay aligned with MCP inputSchema
- A reference architecture you can ship, and what breaks when a layer is misplaced
- How to debug the JSON on this path locally in JSONNote
Запомните это: The 2026 Agent stack no longer argues about “whether to use JSON.” It argues about which layer owns the decision, the execution, and the contract. The model thinks, Function Calling serializes, MCP executes, JSON Schema checks every hop. Below: responsibilities → data flow → each layer → composition → misplacement.
What the four layers do
Draw the boundaries first. When they blur, debugging becomes “the model is bad” or “MCP is broken”—usually neither is. The seam was never drawn.
| Layer | Owns | Input | Output |
|---|---|---|---|
| LLM | Reason, plan, decide the next step | Conversation, tool results, system prompt | Keep talking, or “call this tool” |
| Function Calling | Turn “which tool, which args” into an API object | tools array + conversation | tool_calls[].arguments JSON |
| MCP | Discover tools, run the call, return structured results | tools/call + arguments | structuredContent JSON |
| JSON Schema | Describe and validate JSON at every hop | Schema document | Pass / failure reason |
One line: the LLM thinks, Function Calling says, MCP does, JSON Schema checks.
How to write JSON Schema, and how Tool Calling differs from Structured Output, is in Why AI Agents need JSON Schema. This piece only covers how the four layers connect.
Data flow of one tool call
Take “look up a ticket by id.” The user says “check T-1042.” The Runtime does not throw that sentence at the ticket system. It walks these six steps.
-
1
Runtime builds the request
Send the conversation, system prompt, and tools array to the LLM API. Each tool’s parameters field is a JSON Schema.
-
2
The model does Function Calling
It returns name=get_ticket and arguments as a JSON string—not prose like “please look that up for me.”
-
3
Runtime validates input
After json.loads, validate against the same schema. Retry or reject on missing fields or wrong types. Do not send dirty args downstream.
-
4
MCP executes
The Runtime puts the validated arguments into MCP tools/call. The Server queries the store and returns structuredContent.
-
5
Runtime validates output
structuredContent must match the MCP outputSchema. Only then is it fed back to the model.
-
6
The model gives the final answer
That can be natural language, or another Structured Output pass with a separate schema for the final JSON.
Note the two validates: one stops bad model args, one stops a bad Server payload. Skip either and the next layer treats dirty data as fact.
LLM: reasoning and decisions only
The LLM is the only layer that “thinks.” It reads context and decides to answer, ask, or call a tool. It should not touch databases, payment APIs, or the filesystem.
By 2026, vendor model APIs share the same semantics: you send tools, the model returns tool_calls or final text.
OpenAI uses Function Calling / Responses API, Anthropic uses Tool Use, and DeepSeek keeps OpenAI field names. The gaps are field names and Strict support—not whether output should be structured.
Treat the LLM as an executor and two things happen: secrets enter the prompt, and side effects cannot be audited. Leave execution to MCP. The model only outputs “who I want to call, with which args.”
Function Calling: tool choice at the model API
Function Calling is not a protocol. It is the shape of a model API response. The next step must be a machine-readable function name plus arguments—not a paragraph.
{
"model": "gpt-5",
"messages": [
{"role": "user", "content": "Look up ticket T-1042"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_ticket",
"description": "Fetch a support ticket by id",
"parameters": {
"type": "object",
"properties": {
"ticketId": {
"type": "string",
"pattern": "^T-[0-9]+$"
}
},
"required": ["ticketId"],
"additionalProperties": false
}
}
}
]
}
The model returns arguments as a string, not a parsed object. The Runtime must loads + validate itself. Strict Schema does not change that—API-level conformance is not business-layer validation. It is the same class of problem as 2026 Structured Output’s response_format, except the constrained object is tool arguments instead of the final answer.
Function Calling ends here. It does not care how the tool is implemented or what the result looks like. Who receives the arguments is the Runtime’s job.
MCP: the protocol between Agent and the outside world
MCP (Model Context Protocol) is the standard conversation between the Agent Runtime and external services: list tools, call tools, take structured results. The 2026-07-28 spec dropped the session handshake. Each request is self-describing, so a Server can sit behind ordinary HTTP load balancing. Background: AI Conference 2026 highlights.
{
"name": "get_ticket",
"description": "Fetch a support ticket by id",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"ticketId": {
"type": "string",
"pattern": "^T-[0-9]+$"
}
},
"required": ["ticketId"]
},
"outputSchema": {
"type": "object",
"properties": {
"ticketId": { "type": "string" },
"status": { "type": "string", "enum": ["open", "pending", "closed"] },
"assignee": { "type": ["string", "null"] }
},
"required": ["ticketId", "status"]
}
}
The Runtime puts Function Calling arguments that already passed validation into tools/call as-is. The Server’s structuredContent must pass outputSchema. The Client validates again—same engineering problem, data now comes from the Server instead of the model.
Official tools spec: MCP Tools. The inputSchema root is still an object; after 2026-07-28, outputSchema may be any JSON value.
JSON Schema: the contract across all three layers
JSON Schema is not a fourth “feature module.” It is the shared description language of the other three layers. The current mainstream is 2020-12.
| Where it appears | Field | Constrains | Who validates |
|---|---|---|---|
| Function Calling | tools[].function.parameters | arguments the model generated | Runtime (before calling MCP) |
| Structured Output | response_format.json_schema | The model’s final answer | Runtime (before business code) |
| MCP input | inputSchema | arguments on tools/call | Runtime + Server |
| MCP output | outputSchema | structuredContent | Runtime (before feeding the model) |
The most common composition bug: Function Calling parameters and MCP inputSchema are written twice, and field names or enums disagree. The model generates against A, the Server validates against B, and it looks like “the model always fails the tool.”
The fix is one contract, two mounts: generate parameters and inputSchema from the same schema object. If the final answer also goes downstream, keep a separate Structured Output schema. Do not reuse the tool-input schema for that.
Reference architecture: how to compose them
In production the default wiring looks like this. A local function or your own HTTP API can stand in for MCP at first—the boundaries must not move.
| Component | Does | Does not |
|---|---|---|
| LLM API | Reason; return tool_calls or final JSON | Hold secrets or hit internal systems directly |
| Agent Runtime | Assemble tools, validate, route to MCP, feed back, loop | Treat natural language as args, or skip validate |
| MCP Server | Run side effects, return JSON that matches outputSchema | Parse the prompt or plan for the model |
| Schema catalog | Single source; feeds both API tools and MCP | Hand-write two “almost the same” JSON files |
schema = catalog.get("get_ticket") # single source
req.tools = [{ "type": "function",
"function": { "name": "get_ticket",
"parameters": schema.input } }]
resp = llm.chat(req)
args = json.loads(resp.tool_calls[0].arguments)
validate(args, schema.input) # stop bad model args
content = mcp.call("get_ticket", args) # MCP executes
validate(content.structuredContent, schema.output)
req.messages.append(tool_result(content)) # feed back, next turn
The loop can run for many turns: the model sees structuredContent and then picks the next tool or the final answer. The schema catalog stays put. What grows is the pile of tool results in messages.
What happens when a layer is misplaced
The four layers compose because each does one job. Slide a duty to the neighbor, and the outage looks like a model-quality problem.
| Mistake | Looks like | Actually causes |
|---|---|---|
| Let the LLM hit HTTP itself | One less framework, ships faster | Secrets in context; no audit trail; swapping models rewrites every call |
| Use MCP as if it were Function Calling | The protocol also has name / arguments | The model never sees a tools list, so it will not pick tools reliably |
| Ask for JSON in the prompt, skip schema | The demo runs | No fixtures; fields drift; downstream parse breaks daily |
| Write parameters and inputSchema separately | Both sides “work” | Enums / required keys disagree → random tool failures |
| Trust the model output, skip validate | Strict / JSON Mode is on | Missing fields, truncation, and type errors hit business code |
Rule of thumb: the model only emits decision JSON; execution happens only in MCP (or a backend you name); both directions need a schema.
Debug this path with JSONNote
This path has at least three JSON documents: model arguments, MCP structuredContent, and the final Structured Output. All three should format / validate / diff locally. Keys and samples never need to leave the machine.
-
1
Format the arguments
Paste tool_calls.arguments into JSON Formatter first, to catch broken syntax and truncation.
-
2
Validate against the same schema
Paste parameters / inputSchema and the arguments into JSON Schema. The failure path is exactly where the Runtime should reject.
-
3
Diff two calls
After a prompt or schema change, use JSON Diff to see which fields moved in arguments or structuredContent.
-
4
Share the debug scene
Use URL Hash sharing to put a tool call in the link. A teammate opens it and reproduces—nothing hits a server.
FAQ
Do Function Calling and MCP have to be used together?
No. Local functions and HTTP APIs can back a tool. MCP standardizes discovery, invocation, and structuredContent; Function Calling only lets the model pick a name and arguments. They often combine, but each can stand alone.
How many JSON Schemas should I write?
At least two: one for tool input (Function Calling parameters must match MCP inputSchema) and one for tool output or the final answer (MCP outputSchema or Structured Output response_format). Do not let the model side and the MCP side drift apart.
Is Function Calling enough without MCP?
Enough for a single-process demo. Once tools are reused by multiple agents or runtimes, or you need to scale out, skipping MCP means inventing tools/list, auth, and structured returns yourself. The 2026 default is Function Calling to choose, MCP to execute.
The model already returned JSON—do I still validate in the Runtime?
Yes. Function Calling arguments and MCP structuredContent can still miss fields, have wrong types, or be syntactically broken. API-level conformance is not business correctness. Validation is the Runtime’s job, not a guarantee from the model or the Server.
Итог
The 2026 Agent stack fits in three sentences:
The LLM decides, Function Calling serializes, MCP executes. JSON Schema is the shared contract—not a fourth optional extra.
Freeze one input schema and one output schema. Mount the same objects on the model API and the MCP Server. Validate both hops in the Runtime. Models and protocols will keep changing. The contract and the validation layer are what take an Agent from demo to production—JSONNote’s local format / schema / diff tools are there when you debug that JSON.