teachyou.ai academy
← All posts
MCPOpenAPItool callingAI agentsAPI integration

MCP vs OpenAPI Tool Calling: When to Use Each

Pramod Dutta · Jun 25, 2026 · 16 min read

The short answer to mcp vs openapi is that they operate at different layers: OpenAPI tool calling turns an existing REST API spec into function-call definitions an LLM can invoke directly, while MCP (Model Context Protocol) is a standing client-server protocol that gives a model discoverable tools, resources, and prompts over a persistent connection. If you already have an OpenAPI spec and just need the model to hit a handful of endpoints once per request, OpenAPI tool calling is less machinery. If you're building a tool that many different agents and clients will reuse, with state, streaming updates, or file-like resources involved, MCP is the better fit. The rest of this article walks through why, with working code for both.

What OpenAPI Tool Calling Actually Is

OpenAPI tool calling is not a separate protocol. It is a convention: you take an OpenAPI 3.x spec (or write a JSON Schema by hand that looks like one), convert each operation into a "tool" definition in the format your model provider expects, and pass that array of tools in your chat completion request. The model returns a tool_use block with arguments matching your schema, your application code calls the actual REST endpoint, and you feed the response back into the conversation.

There is no protocol server. No handshake. No persistent connection. Every "tool" is just a JSON Schema object plus a name and description, sitting in memory in your application process, and your application code is the thing that decides what actually happens when the model asks to call it.

A minimal example using the Anthropic API and Python:

import anthropic
import httpx

client = anthropic.Anthropic()

tools = [
    {
        "name": "get_weather",
        "description": "Get current weather for a city",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name"},
                "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["city"]
        }
    }
]

def call_weather_api(city, units="celsius"):
    resp = httpx.get(
        "https://api.example.com/v1/weather",
        params={"city": city, "units": units}
    )
    return resp.json()

message = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "What's the weather in Lisbon?"}]
)

for block in message.content:
    if block.type == "tool_use" and block.name == "get_weather":
        result = call_weather_api(**block.input)
        # feed result back as a tool_result block in the next turn

If your API already publishes an OpenAPI spec, you skip hand-writing the input_schema blocks. Most spec-to-tool converters walk the paths object, turn each operation's parameters and requestBody into a JSON Schema, and use the summary or description field as the tool description. The mapping is mechanical because OpenAPI parameter schemas are already JSON Schema-compatible.

The important thing to notice: the "tool" here is stateless and single-shot. The model asks for get_weather(city="Lisbon"), gets one JSON blob back, and that's the entire interaction. There is no concept of the tool holding a session, streaming partial results, or exposing anything other than request/response pairs.

What MCP Actually Is

MCP (Model Context Protocol) is a client-server protocol, originally released by Anthropic and now used across multiple model providers and agent frameworks. Instead of your application code owning both the tool definitions and the execution logic in one process, MCP splits it into two long-lived roles:

  • An MCP server is a separate process (or hosted service) that exposes tools, resources, and prompts over a defined transport, usually stdio for local servers or Streamable HTTP for remote ones.
  • An MCP client, embedded in the host application (a chat app, an IDE, an agent runtime), connects to one or more servers, asks them what they offer, and routes model tool calls to the right server.

The protocol itself is JSON-RPC 2.0 based. When a client connects, it performs an initialize handshake, then can call tools/list to discover available tools, resources/list for readable resources like files or database rows, and prompts/list for reusable prompt templates the server ships. Discovery happens at connection time, not at code-authoring time, which is the structural difference from OpenAPI tool calling.

Here is a minimal MCP server in Python using the official SDK:

from mcp.server.fastmcp import FastMCP
import httpx

mcp = FastMCP("weather-server")

@mcp.tool()
def get_weather(city: str, units: str = "celsius") -> dict:
    """Get current weather for a city."""
    resp = httpx.get(
        "https://api.example.com/v1/weather",
        params={"city": city, "units": units}
    )
    return resp.json()

@mcp.resource("weather://history/{city}")
def weather_history(city: str) -> str:
    """Return the last 7 days of weather as CSV."""
    resp = httpx.get(f"https://api.example.com/v1/history/{city}")
    return resp.text

if __name__ == "__main__":
    mcp.run(transport="stdio")

Notice two things this file does that OpenAPI tool calling has no equivalent for. First, get_weather is decorated once and the SDK derives the JSON Schema from the Python type hints and docstring; you never hand-write a schema. Second, weather_history is a resource, not a tool: it's addressable by URI, cacheable, and the host application can list it in a file-picker style UI without the model needing to call anything. OpenAPI specs describe operations, not addressable content, so there's no clean analog.

On the client side, connecting from a host application looks like this:

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

server_params = StdioServerParameters(
    command="python",
    args=["weather_server.py"]
)

async def run():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            result = await session.call_tool("get_weather", {"city": "Lisbon"})
            print(result.content)

That session.initialize() call is the handshake OpenAPI tool calling doesn't have. It negotiates protocol version and capabilities, and it's what lets an MCP server evolve, add tools, or change behavior without every client needing a code change, since the client discovers the current tool list live.

The Core Architectural Difference

Strip away the code and the difference comes down to where the contract lives and when it's evaluated.

With OpenAPI tool calling, the contract is a static schema baked into your prompt at request time. You, the application developer, own the full lifecycle: fetching the spec (or writing the schema), converting it to the provider's tool format, executing the HTTP call when the model asks, and handling auth, retries, and pagination yourself. The model provider's API has no idea an external service exists; it only sees JSON Schema and a name.

With MCP, the contract is negotiated live between a client and a server that both speak the protocol. The server owns tool execution, auth to its own backend, and can change its tool list between sessions without you touching the calling code. The client only needs to speak MCP once to work with any MCP server, the same way a browser only needs to speak HTTP once to work with any website.

This makes MCP closer to a runtime plugin system, and OpenAPI tool calling closer to a compile-time function signature. Neither is strictly "better" for reasoning quality: once the model has a JSON Schema and a tool name in context, it calls the tool the same way regardless of what's on the other end. The difference is entirely in how much infrastructure you build and how reusable the result is.

When OpenAPI Tool Calling Wins

You already have a REST API with an OpenAPI spec. If your product's backend already publishes openapi.json, generating tool schemas from it is close to free. You don't need a new server process, a new transport, or a new deployment target. You add a conversion step in code you already control.

The integration is one-off and internal to a single application. If only your chat app will ever call this tool, and no other agent, IDE, or teammate's project needs it, building a full MCP server is overhead you won't recoup. A tool definition plus a function is simpler to reason about, test, and deploy than a separate long-running process with its own transport and lifecycle.

You need tight control over exactly what gets exposed to the model. Because you hand-author or filter the schema yourself, it's easy to expose three of an API's forty endpoints and hide the rest. MCP servers can do this too, but the default posture of "wrap the whole API" is more common with off-the-shelf OpenAPI-to-tool converters, so you have to actively curate either way.

Latency and process count matter. OpenAPI tool calling adds zero extra processes. Your application makes an HTTP call directly. An MCP server, especially over stdio, means spawning and managing a subprocess, or over HTTP, means standing up and monitoring another service. For a serverless function calling a single internal API, that's often not worth it.

You're prototyping. Stub out a Python function, write a JSON Schema by hand, wire it into your chat loop. You can have working tool calling in under fifty lines without installing an SDK or thinking about transports.

When MCP Wins

Multiple clients need the same tools. If your team is building an internal tool that should work inside a coding agent, a support chatbot, and a Slack bot, writing it once as an MCP server and connecting three clients to it beats maintaining three separate OpenAPI-to-tool conversion layers that can drift out of sync.

The tool needs state across calls. MCP sessions persist for the life of a connection. A server can hold a database cursor, a browser session, or an authenticated context between tool calls within the same session. OpenAPI tool calling is fundamentally stateless request/response; any state has to be smuggled through parameters or an external store, which gets awkward fast for things like "keep this browser tab open across five tool calls."

You want resources, not just actions. MCP's resource primitive lets a server expose file-like, addressable content (a document, a database row, a log stream) that a host application can show in a picker UI or attach to context without necessarily invoking the model at all. There's no OpenAPI equivalent; every OpenAPI operation is an action, even the read-only GET ones are represented as function calls, not as content.

You need live discovery and versioning without redeploying every client. Because clients call tools/list at connection time, a server can add a new tool or deprecate an old one, and every connected client picks it up automatically the next time it initializes. With OpenAPI tool calling, if the underlying spec changes, every application that generates tool schemas from it needs to regenerate and often redeploy.

You're integrating with third-party developer tools. A growing number of IDEs and agent runtimes ship an MCP client out of the box. Publishing an MCP server is how you get your product surfaced inside those tools without asking every vendor to write a bespoke OpenAPI integration for you. This is the main reason MCP adoption has grown quickly among dev-tool companies: it's a single integration point instead of N bespoke ones.

You need streaming or server-initiated updates. MCP's transport layer supports notifications from server to client, useful for progress updates on long-running operations. Plain OpenAPI tool calling has no notion of a callback; the model waits for one response per call.

Same Tool, Two Ways: A Direct Comparison

To make the trade-off concrete, here's the same capability, "search a knowledge base," implemented both ways.

OpenAPI-style, in the calling application:

tools = [{
    "name": "search_kb",
    "description": "Search the internal knowledge base for articles",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string"},
            "limit": {"type": "integer", "default": 5}
        },
        "required": ["query"]
    }
}]

def search_kb(query, limit=5):
    resp = httpx.post(
        "https://internal-api.company.com/v1/kb/search",
        json={"query": query, "limit": limit},
        headers={"Authorization": f"Bearer {get_service_token()}"}
    )
    return resp.json()

Everything, including the auth token fetch, lives in the process that also holds your conversation loop. If a second team wants this same capability in their own agent, they copy this code or import it as a library, and now there are two places that can drift.

MCP-style, as a standalone server:

from mcp.server.fastmcp import FastMCP
import httpx, os

mcp = FastMCP("kb-search")

@mcp.tool()
def search_kb(query: str, limit: int = 5) -> list[dict]:
    """Search the internal knowledge base for articles."""
    resp = httpx.post(
        "https://internal-api.company.com/v1/kb/search",
        json={"query": query, "limit": limit},
        headers={"Authorization": f"Bearer {os.environ['KB_SERVICE_TOKEN']}"}
    )
    return resp.json()["results"]

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=8080)

Now any team's agent that speaks MCP points at http://kb-server:8080 and gets the same tool, the same auth handling, and the same behavior, with one place to fix bugs. The cost is that you now operate a service: health checks, deployment, and a network boundary that didn't exist before.

Auth and Security Differences

OpenAPI tool calling inherits whatever auth your application already uses to call the underlying API: bearer tokens, API keys, OAuth client credentials, whatever you've already wired up in your HTTP client. There's no protocol-level auth concept to learn because there's no protocol, just your code calling an endpoint.

MCP, particularly over HTTP transports, has its own authorization spec built on OAuth 2.1, with the server acting as an OAuth resource server and the host application's client handling the token flow with the user. This matters most for remote MCP servers that multiple external users will connect to, where you need per-user consent and scoped tokens rather than one shared service credential. For local stdio servers running on a developer's own machine, this layer is usually skipped since the process already runs with the user's own privileges.

The practical security review question is the same either way: what can this tool actually do on my behalf, and can the model be tricked by tool output into calling something it shouldn't (prompt injection via tool results is a real risk in both models). MCP does not remove this risk by being a protocol; it just changes who's responsible for validating input and sanitizing output, since that logic now lives in a server you may not have written yourself. Treat third-party MCP servers with the same scrutiny you'd give a new npm dependency with network access, read what it actually does before connecting a production agent to it.

Performance and Context Window Considerations

Both approaches put the tool schema in the model's context, and both cost roughly the same number of tokens for a given tool set, since MCP's tools/list response and a hand-written OpenAPI-derived schema converge on the same JSON Schema shape once they reach the model. The difference shows up in what else rides along.

MCP connections that expose many tools from many servers can bloat context if you connect a host application to a dozen servers and don't curate which tools get exposed per conversation. Because discovery is automatic, it's easy to end up with more tools in context than a task needs, which both costs tokens and increases the chance the model picks the wrong tool. OpenAPI tool calling, because you hand-assemble the tools array per request, tends to naturally stay curated, since someone had to decide to add each tool to the list.

If you're running MCP, the practical fix is the same discipline either way: pass only the tools relevant to the current task, not every tool every connected server offers. Most MCP client SDKs let you filter the tool list from tools/list before handing it to the model, so use that filter rather than relying on the model to ignore irrelevant tools.

Using Both Together

These aren't mutually exclusive in a single system. A common pattern: internal, single-purpose integrations stay as OpenAPI-derived tool calls because they're simple and only your app uses them, while shared capabilities that multiple teams or external tools need to reuse get pulled out into an MCP server. You can also front an MCP server with a tool that itself wraps an OpenAPI spec, an mcp.tool() function whose body calls httpx.get() against a REST endpoint is exactly what the search_kb example above does. MCP doesn't replace REST APIs; it's a distribution layer for making REST (or any backend) callable by any MCP-speaking model client without every client writing its own glue code.

A reasonable default for a growing team: prototype with OpenAPI-style tool calling because it's fast to write and delete. The moment a second consumer wants the same tool, or the tool needs to hold state across calls, or you want it discoverable inside a coding agent or IDE without a custom integration, migrate that specific tool to an MCP server. You don't need to pick one approach for your entire stack.

FAQ

Is MCP a replacement for REST APIs? No. MCP is a protocol for exposing tools, resources, and prompts to LLM clients; it doesn't replace the backend services those tools call. Most MCP servers call REST APIs, databases, or other backends internally, the same as your application code would in an OpenAPI tool-calling setup.

Can I convert an OpenAPI spec directly into an MCP server? Yes, several open-source generators will read an openapi.json file and scaffold an MCP server with one tool per operation. This gets you MCP's discovery and multi-client benefits without hand-writing each tool, though you should still review and prune the generated tool list rather than exposing every endpoint by default.

Does using MCP make tool calls more reliable than OpenAPI-style tool calling? Not by itself. Once the schema and description reach the model, tool selection and argument-filling quality depend on how clear the tool name, description, and parameter names are, not on which protocol delivered them. Write clear tool descriptions regardless of which approach you pick.

Which one should I use for a coding agent that reads and writes files? MCP, generally. Filesystem access benefits from MCP's resource primitive (addressable, listable files) and from the fact that most coding agents and IDEs already ship an MCP client, so you get broad compatibility for free instead of writing a custom OpenAPI-based file tool for each agent you want to support.

Do I need to run a server for MCP even for local, single-user tools? Yes, but it can be a lightweight subprocess over stdio, not a hosted service. A local MCP server for something like reading local files or querying a local SQLite database runs as a child process of your host application and shuts down when the session ends; it's not the same operational burden as running a remote HTTP service.

What happens if an MCP server is slow or unavailable? The client's tool call to that server will time out or error, and your host application needs to handle that the same way it would handle a failed HTTP request in the OpenAPI-style approach: catch the error, surface it to the model as a tool error result, and let the model decide whether to retry or tell the user. MCP doesn't add automatic retry or failover; that logic is still yours to write.

Can a single conversation use both MCP tools and OpenAPI-derived tools at the same time? Yes. Both ultimately produce the same tool-definition shape in the model's context, so a chat completion request can include a mix of tools sourced from connected MCP servers and tools you defined directly from an OpenAPI spec. The model doesn't know or care which source a tool came from.