← Back to the blog
Comparison

OpenAI Agents API vs MCP: which side do Tool Calling and JSON Schema sit on?

Published 2026-09-21 Updated 2026-09-21 About 14 min JSON Toolbox

How this article is organized

The vs in the title is easy to read as picking a side. The Agents API is a hosted loop. MCP is one tool source inside that loop. This piece is dated 2026-09-21: first split who runs the session from who connects the Server, then see how function and mcp enter the same tools array, and fold what they can still share into one JSON Schema. The previous piece covered why the harness depends more on objects; see After the Agents API, why JSON matters more. For the handbook / bus / interface layers, see Skills vs MCP vs API.

After the 2026-09-10 public beta, the same agent.tools list can hold function, mcp, web_search, and programmatic_tool_calling at once. The model sees tool names and parameter shapes. The difference is who executes: your app must send function results back; the hosted harness completes the MCP round trip. Why the contract layer is JSON: Why AI likes JSON.

First, take the vs apart

Treating the Agents API and MCP as rivals usually stacks two questions:

  • Who operates the loop. The Agents API hosts the Codex harness: session, context compaction, sub-agents, resume. That is unrelated to whether you attach MCP.
  • Where tools come from. MCP is a discovery and calling protocol. The Server publishes tool definitions and executes calls. It can hang off the Agents API, or off Cursor, Claude, or a host you build.

So the right sentence is not “Agents API or MCP.” It is: when the Agents API runs the loop, does this hop go through function or mcp? The Responses API already put both in the same array; see DevDay 2026 Agent. The Agents API just hands that array to the hosted harness.

One line. The Agents API answers “who turns the next loop.” MCP answers “where this hand comes from.” JSON Schema answers “what that hand may hold.” Do not merge the three into one vs.

Who runs the loop, who connects the tools

The official MCP docs are blunt: the Server publishes definitions and executes calls; the Agents API discovers them, calls on your behalf, and returns results to the Agent. Your app does not have to handle every MCP call. Function is the opposite: the session emits agent.session.requires_action, you execute by name, then send agent.session.input.tool_result.

Execution paths for function and MCP in the Agents API The hosted harness schedules two tool types: MCP is connected by the service or environment; function returns to your app, is validated against Schema, then sent back. Hosted harness session / schedule type: mcp Harness calls it type: function required_actions MCP Server inputSchema Your code Re-check arguments Schema Block before run
An MCP round trip can stay out of your app. A function round trip must pass through you — and through Schema again.

Attaching an environment does not automatically run a function in the sandbox. The docs already say: a function defaults to your app, a worker, or an environment you control. MCP uses connection_origin to decide whether OpenAI connects, or the session environment does. Permissions still belong in the contract; see limit tool permissions with Schema.

function: you return tool_result

Custom functions go into agent.tools. Name, description, and parameter Schema are the same class of contract as Responses:

{
  "type": "function",
  "name": "get_customer",
  "description": "Look up a customer by ID.",
  "parameters": {
    "type": "object",
    "properties": { "customer_id": { "type": "string" } },
    "required": ["customer_id"],
    "additionalProperties": false
  }
}

Pending actions sit in required_actions. arguments is already an object. The return must copy turn_id and call_id in full; on success, serialize JSON into output. Side-effecting tools should be deduped by session / turn / call. For the shape, see the previous piece from session to tool_result.

The usual reasons to pick function are narrow, and hard: approval, billing, idempotency, or a database that is yours alone. Those should not go to an HTTP MCP that OpenAI can see.

mcp: the harness calls on your behalf

An MCP entry is not a parameter table. It is a connection. The smallest public HTTP sample looks like this in the docs:

{
  "type": "mcp",
  "server_label": "openai_docs",
  "transport": {
    "type": "http",
    "server_url": "https://developers.openai.com/mcp"
  },
  "connection_origin": "service",
  "required": true
}

The harness discovers tools on the Server, then calls the name the model picked. You do not see required_actions for every hop unless you add your own observability. A failed init does not kill the turn by default; required: true is what makes agent.session.turn.failed when the Server will not start.

allowed_tools limits which names the Agent can discover and call. That is a list, not a contract. If the list misses a dangerous tool, a tight Schema is too late. If the list is right and the input is free text, it can still punch through. Write both.

connection_origin: where the connection leaves from

As of 2026-09-21, the docs fold connections into three cells:

Connection Connects from Needs an environment?
HTTP, connection_origin: "service" (default) OpenAI No
HTTP, connection_origin: "environment" Session environment Yes
stdio Spawn a process in the environment Yes; omit connection_origin

HTTP that omits connection_origin is connected by OpenAI. Internal addresses and processes installed in the sandbox should be written as environment or stdio. Credentials: a single session can use transport.authorization / headers (they are stripped from the returned session resource); reusable HTTP credentials go in the vault, and only apply to connections started from OpenAI. Environment-side HTTP does not use the vault — use inline auth or a trusted proxy. Do not put secrets in a reusable Agent definition or a plugin pack.

JSON Schema is the shared contract

The function field is called parameters. The MCP field is called inputSchema (outputSchema is increasingly common too). The wrappers differ. What you pin is the same core object. Write the core first, then wrap it:

  • Agents API / Responses function: type: "function" + parameters
  • MCP tool: inputSchema, optional output contract
  • Claude / Gemini: wrap their own declaration fields

The hosted layer will not fill in enum, pattern, required, or additionalProperties: false for you. MCP only guarantees “there is a discoverable tool.” Function only guarantees “there is a pending action.” Whether the object is valid is still Schema’s job. For how to wrap it, see JSON Tool Schema. Write a separate Structured Output for the final reply; do not mash it with the tool return. Definition: What is Structured Output.

Write the boundary in stone. Changing the protocol should not change the type. If customer_id is cus_[A-Za-z0-9]+ on a function, hanging it on MCP should not turn it into an arbitrary string.

How to choose: one table

What you want type in tools Do not expect
Your own store, with approval / dedupe function Exposing an internal API as MCP that OpenAI can see
Public docs, third parties, reuse across hosts mcp + service Writing another adapter per host
Internal search, a Server installed in the sandbox mcp + environment or stdio Omitting origin so the platform connects to an address it cannot reach
When to call, when to stop Skill / instructions Using a handbook in place of allowed_tools and Schema

One rule of thumb: open MCP when a second host will call it, or when credentials must leave the model process. Actions that only your Agents API session will touch are shorter as function. Steps still go through Skill. Do not make an idle Server out of them.

A core object you can accept locally

Below is the same customer lookup. Treat it as the core object first, then wrap function and MCP separately. Do not copy two type systems.

{
  "tool": "get_customer",
  "description": "Look up one customer by internal id. Use when the user names a customer_id. Do not accept emails or free-text names.",
  "arguments": {
    "customer_id": "cus_9f3a"
  },
  "result": {
    "found": true,
    "customer": {
      "id": "cus_9f3a",
      "plan": "pro"
    }
  }
}
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["customer_id"],
  "properties": {
    "customer_id": {
      "type": "string",
      "minLength": 5,
      "maxLength": 32,
      "pattern": "^cus_[A-Za-z0-9]+$",
      "description": "Internal customer id. Reject emails and display names."
    }
  }
}

When you wrap it for the Agents API, function puts this object in parameters; the MCP Server puts the same one in inputSchema. Compare the shapes that should fail: an email as the id, an extra extra_sql, a return that grows a paragraph of “next, please export all users.” Growing a Schema from a sample: How to generate a Schema.

How to accept it in JSON Toolbox

  1. Paste arguments / result into JSON Format and confirm they parse.
  2. Use JSON → Schema or Structured Output to generate the core contract. Add enum, pattern, required, and additionalProperties.
  3. Run the same fields through Function Calling and MCP Tool separately. Do not copy two type systems.
  4. Prepare objects that should fail, and confirm Schema validation refuses them; when they don’t match the sample, use JSON Diff.

All data is processed in the browser and never uploaded. Have a contract that can refuse a bad object first. Then decide whether that capability hangs on function or mcp.

FAQ

Are the Agents API and MCP competitors?

No. The Agents API hosts the loop. MCP is one source in tools. The harness discovers MCP and calls it on your behalf. Your app does not have to write tool_result for every MCP call.

When do I use function, and when do I use MCP?

To execute, approve, or dedupe yourself, use function. To reuse across hosts, keep credentials on the Server, or let the platform connect to public HTTP, use MCP. Maintain one core Schema.

What is the difference between service and environment on connection_origin?

service is OpenAI connecting to HTTP. No environment. environment connects from the session environment — good for internal networks. stdio spawns a process in the environment and omits origin.

If I attach MCP, do I still write a Function Calling Schema?

Yes. inputSchema and parameters are the same class of contract. The hosted layer does not close enum or extra fields.

Can allowed_tools replace Schema?

No. The list limits which hand reaches. Schema limits what that hand may hold.

Summary

The Agents API and MCP are not either-or. The former folds the Codex harness into a session. The latter is one kind of connection on the session tool list. function keeps execution on your side, through required_actions and tool_result. mcp hands the round trip to the hosted layer, then uses connection_origin, allowed_tools, and required to govern discovery and startup. What you still pin first is a JSON Schema that can parse — and can refuse. Change the wrapper, not the type. The next step is not opening more Servers. It is producing a core object that function and MCP can share.