AI Development

How Claude Code, Codex, and Other AI Coding Agents Call MCP Tools: JSON-RPC, Tool Schema, and Permissions (2026)

About 16 min read

The protocol piece already split Schema from Function Calling. This one lands only on coding agents: the model fills arguments; the host sends JSON-RPC. Connect, list, schema, then permission are four steps — not the sentence “hook up MCP”.

Before you read

Step What it locks Who executes
Config .mcp.json / config.toml stdio or http; a url needs type
Discover tools/list + inputSchema The host lists; the model never sees the wire
Call arguments on tools/call Model fills JSON; host sends RPC
Permission mcp__server__tool / enabled_tools Connected ≠ auto-run

MCP is a host-to-server protocol. The model sees tool names and parameter Schema.

The host calls MCP, not the model

Claude Code, Codex, and Cursor are MCP clients. After a session starts, the host connects, sends JSON-RPC 2.0 tools/list, and hands each tool’s inputSchema to the model. The model only emits argument JSON. The host is what sends tools/call and runs side effects. Without that hop, MCP is a config file.

  • Config: how the server starts. stdio is a local process. http is remote. Both are objects, not a verbal agreement.
  • Schema: order_id must pass inputSchema. A wide object invites invented keys.
  • Permission: Claude writes rules as mcp__server__tool. Codex uses enabled_tools and an approval mode. Cursor uses mcpAllowlist. Connecting a server does not open those gates.

Schema versus Function Calling at the protocol layer: MCP Tool Schema vs Function Calling. A coding agent’s own tool JSON: AI coding agents and JSON tool calling. Why Schema is a gate: Why AI needs JSON Schema. This text does not redefine those words. It only shows how the host runs MCP.

First object: server config

Claude Code’s project file is .mcp.json at the repo root. It can go in version control. Private entries live in ~/.claude.json. A url must carry type: http (streamable-http is an alias) or sse / ws. A url without type is treated as a broken stdio entry and skipped. Fictional order service — no real secrets:

{
  "mcpServers": {
    "orders": {
      "type": "stdio",
      "command": "node",
      "args": ["./servers/orders-mcp.js"]
    }
  }
}

Codex writes the same fact in ~/.codex/config.toml or a trusted project’s .codex/config.toml, under [mcp_servers.orders]. stdio uses command / args. Remote uses url. Cursor uses .cursor/mcp.json or the user file; the editor and CLI share it. A new project server in Claude Code waits for approval. It does not auto-connect.

Second object: tools/list and inputSchema

Once connected, the host asks which tools exist. That is JSON-RPC, not an HTTP call the model makes:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}

The server returns a table. Each row is a name, a description, and a JSON Schema. The spec requires a tools capability. The list may change with authorization. It must not jitter per connection. The coding agent turns that table into tools the model can see. In Claude Code the callable name is mcp__orders__lookup_order. A plugin-bundled server also embeds the plugin name. When the table is large, official clients defer tools and expand them on search. Do not dump the whole table into context by default.

{
  "name": "lookup_order",
  "description": "Look up an order by ID. Confirm before any refund.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "pattern": "^[A-Z]-\\d{4}$" }
    },
    "required": ["order_id"],
    "additionalProperties": false
  }
}

Third object: the model fills args, the host tools/call

The model sees the name the host published. In a Claude Code session a call looks like this:

{
  "type": "tool_use",
  "id": "call_3c91",
  "name": "mcp__orders__lookup_order",
  "input": { "order_id": "A-1042" }
}

The host checks input against inputSchema, then translates it into the RPC sent to the server. The wire name is the registered lookup_order, without the mcp__ prefix:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "lookup_order",
    "arguments": { "order_id": "A-1042" }
  }
}

On success, return content. On failure, set isError and write a reason as an object. Do not drop a prose sentence. The model reads the write-back, not the socket.

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      { "type": "text", "text": "{\"order_id\":\"A-1042\",\"status\":\"paid\"}" }
    ],
    "isError": false
  }
}

Fourth object: execution permission

The MCP spec says a tool call should be deniable. Coding agents write that denial as rules, not as a prompt. Claude Code uses the full callable name:

{
  "permissions": {
    "allow": ["mcp__orders__lookup_order"],
    "ask": ["mcp__orders__create_note"],
    "deny": ["mcp__orders__refund_order"]
  }
}

Official order is still deny, ask, allow. A bare mcp__* deny removes MCP from context. Codex uses enabled_tools as an allow list, disabled_tools as a later deny list, and auto / prompt / approve as the default, overridable per tool. Cursor asks by default; mcpAllowlist in permissions.json is written as orders:lookup_order. The Shell / Git / filesystem doors: Coding Agent permission model. MCP is a fourth table, not a substitute for those three.

Five steps to an executable MCP

Do not start from “please hook up MCP”. Write objects:

  1. Write the server config first. Pick stdio or http. Put type on every url. Secrets stay in environment variables, not the repo.
  2. Run tools/list yourself. Note names and required fields. A tool without Schema does not go to the model.
  3. Tighten inputSchema. pattern, enum, additionalProperties: false. A lookup must not grow a refund key.
  4. Write permission per tool. Read-only lookup on allow. Writes on ask. Refund on deny. Codex: crop the table with enabled_tools first.
  5. Keep three JSON traces of one success: tool_use, tools/call, result. If they line up, the hop is real.

How Schema becomes a gate: Agents API + JSON Schema. This text only asks that every MCP step on a coding agent is an object the host sends.

To review an MCP miss, spread three JSON traces

A failed call is usually a name, Schema, or permission mismatch — not a model that “cannot call MCP”.

  • format inputSchema next to arguments. Look for missing fields and extra keys.
  • With JSONPath extract, pull $.params.name and $.input.order_id.
  • JSON Diff a pass and a deny. Fix the rule and Schema before you fix the prompt.
  • Check the host name mcp__orders__lookup_order against the RPC name lookup_order. Plugin servers add a plugin segment.
  • Do not paste config, tokens, or session logs into a public formatter. JSONSort runs locally in the browser.

If the three traces rebuild against Schema, you collected MCP. Otherwise you collected “I clicked Connect”.

Common traps

  • Treating connect as call. Connect only means a process or HTTP path is up. Without list, Schema, and permission, the model still cannot call.
  • Treating MCP as Function Calling. To the model both are fill-in JSON. To the host, MCP adds a JSON-RPC hop. The protocol piece already split the words. Here you only need to see the hop.
  • A url without type. Claude Code skips it as broken stdio. The remote server vanishes.
  • Configuring a server and never cropping tools. A large table enters context. Official deferral and enabled_tools exist because of that.
  • Defaulting to “auto-approve every MCP”. Cursor’s approve-all, Claude bypass, and Codex auto-everywhere are for isolated environments. Daily sessions write per tool.
  • Treating project .mcp.json as a private file. It is shared in version control. Teammates will see a pending approval. Secrets stay in user-scoped config.

FAQ

Does the coding agent send JSON-RPC itself?

No. The model fills a tool name and arguments. The host builds id, method, and params. If your log shows tools/call, that is the client, not the model.

Which gate runs first, inputSchema or permission?

Shape first, then execution. If Schema fails, do not send RPC. If Schema passes, ask or deny can still fire. The doors lock different things.

stdio or http?

Local files or a local process: stdio. A hosted service: http. Claude Code prefers http for remote; SSE is deprecated. WebSocket fits a server that pushes events.

What if there are too many tools?

Claude Code can defer MCP tools and expand them on search. Codex crops the table with enabled_tools. Do not dump fifty tools into one session.

Can a subagent use these MCP tools?

It depends on the product. Claude Code permission and allowed-tools need the full callable name. Agents API subagents inherit MCP, not your function tools. Do not assume one table is copied into every child session.

Conclusion

A coding agent calling MCP is not the model hitting the server. The host lists tools, the model fills JSON, the host then tools/call. Schema and permission are two doors.

Write down which tool name, Schema, and permission rule A-1042 would take. If you can point at three objects, MCP is connected. Models change. Those three should settle first.

Further reading

Changelog: First published