Qora API — AI API Gateway for Developers

AI API Gateway for Developers

One clear API workflow for your apps, scripts and automations.

What Is the Model Context Protocol (MCP)? Connect Your AI to Real Tools

Cover image titled Model Context Protocol (MCP) with the subtitle The USB-C for AI tools & data sources, and pills for MCP, Tools and Interop.

The Model Context Protocol (MCP) is an open standard that lets any AI application plug into tools and data through one client/server interface. Instead of writing a separate Slack, database, or filesystem integration for every assistant, you write one MCP server — and every MCP-compatible client can use it.

This guide covers what the protocol actually specifies, how it composes with function calling rather than replacing it, and a minimal server you can run in five minutes.

The problem MCP solves: N clients × M integrations

Before MCP, every AI host had its own plugin format. Claude Desktop, an IDE assistant, a LangChain script, and your internal chatbot each needed their own adapter for Slack, Postgres, Jira, and the local filesystem. Five hosts and eight data sources is 40 bespoke adapters, each with its own auth handling, retry logic, and schema quirks.

That matrix has a second, worse cost: maintenance. When Slack changes a response shape or an OAuth scope, you patch every adapter separately. MCP collapses the matrix to a sum. Five hosts plus eight servers is 13 implementations, and one vendor change means one patch.

There is a third cost that only shows up once you build AI agents: discovery. An agent that can only call the tools you hardcoded at build time cannot use a new tool without a redeploy. MCP servers advertise their capabilities at runtime, so a client can enumerate what is available on each connection and react when the list changes.

What MCP is: client/server over JSON-RPC 2.0

MCP has three roles. The host is the AI application (an IDE, a chat app, your backend). The client is a connector inside the host, one per server. The server is a program that exposes capabilities. Client and server exchange JSON-RPC 2.0 messages — requests with an id, responses, and one-way notifications.

Every connection begins with an initialize handshake. The client sends a date-stamped protocol version and the capabilities it supports; the server replies with its own. This is the design decision that makes MCP durable: a server can add a feature that older clients simply never see, instead of breaking them. Protocol versions are dates, not semantic numbers, so negotiation is a comparison rather than a compatibility guess.

The methods you will actually see in logs:

  • Toolstools/list, tools/call. Model-controlled actions with side effects.
  • Resourcesresources/list, resources/read, resources/subscribe. Addressable read-only data.
  • Promptsprompts/list, prompts/get. Reusable templates the user picks.
  • Change notificationsnotifications/tools/list_changed, so clients re-fetch instead of caching forever.
  • Server-to-client callssampling/createMessage (the server asks the host’s model to generate) and elicitation/create (the server asks the user for a missing argument). These are the least-known part of the spec and the reason a plain HTTP API wrapper is not an MCP server.

Two transports carry these messages, and choosing between them is a deployment decision, not a preference:

TransportHow it worksUse it whenAuth boundary
stdioJSON-RPC over the stdin/stdout of a child process the host spawnsLocal developer tools, IDE and desktop clients, filesystem or shell accessOS process boundary; no network exposure
Streamable HTTPJSON-RPC over HTTP POST, with optional SSE streaming on the same endpointRemote or shared servers, multi-tenant SaaS, anything behind a load balancerOAuth 2.1 bearer tokens with audience validation
HTTP+SSE (legacy)Long-lived SSE stream for server messages plus a separate POST endpoint for client messagesOnly for pre-2025 servers you cannot upgradeTwo endpoints to secure and correlate

Streamable HTTP replaced the original HTTP+SSE transport because two endpoints made session correlation and horizontal scaling painful. If you are writing a new remote server today, use Streamable HTTP.

MCP vs function calling: interface vs invocation

These are routinely described as competitors. They are not, and the distinction matters because it tells you what each one can and cannot fix.

Function calling is a model behavior. You place a JSON Schema in the request; the model may answer with a structured call object instead of prose; your code executes it. It specifies nothing about where that function lives, how it authenticates, or how your app learned it exists.

MCP is an integration protocol. A server advertises capabilities, a client discovers them, and both sides speak JSON-RPC 2.0 over a negotiated transport. It specifies nothing about how the model decides to call anything.

DimensionFunction callingMCP
What it standardizesThe model’s output format for “call this with these arguments”The interface between an AI app and a tool provider
Who defines the contractYou, per provider, inside every request payloadThe server, discovered at runtime via tools/list
Where tools liveIn your processAnywhere: local child process or remote service
Discovery, auth, lifecycleNot coveredCovered — handshake, capability negotiation, OAuth
Reuse across appsCopy-paste per appAny MCP client works unchanged

The composition point is concrete: the output of tools/list is already JSON Schema, and the input to function calling is already JSON Schema. Converting one to the other is a rename, which is exactly what the bridge later in this article does.

Three consequences that surprise people implementing this for the first time:

  • MCP does not make a model smarter. If your model picks the wrong tool, MCP will not fix it. MCP fixes integration, not reasoning — validate tool-selection quality separately before shipping an agent.
  • You can use MCP with zero function calling. Resources and prompts need no model-side invocation. A client that only reads resources is fully compliant.
  • Some providers now accept a remote MCP server URL directly as a tool. The provider hosts the client loop and you write no bridge. Decision rule: use provider-hosted MCP for prototypes and read-only servers; run your own client when you need allowlists, audit logs, or a human approval step, because hosted loops give you little control over which tools are exposed or when the loop terminates.

Anatomy of an MCP server: tools, resources, prompts

The three primitives differ by who decides to use them, which is the fastest way to classify anything you are building:

  • Tool — the model decides. Has side effects, takes validated arguments, returns content. Anything a user would expect to be asked about first is a tool.
  • Resource — the app or user decides. Read-only, addressed by URI, and attachable to context before the model is even called.
  • Prompt — the user decides. A named template the client surfaces, typically as a slash command.

Here is a complete, runnable server using the official Python SDK. It exposes two tools, one resource, and one prompt over stdio:

# server.py — minimal MCP server (official Python SDK)
# pip install "mcp[cli]"
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("repo-tools")

@mcp.tool()
def read_file(path: str) -> str:
    """Read a UTF-8 text file from the workspace."""
    with open(path, encoding="utf-8") as f:
        return f.read()

@mcp.tool()
def count_lines(path: str) -> int:
    """Count the lines in a text file."""
    with open(path, encoding="utf-8") as f:
        return sum(1 for _ in f)

@mcp.resource("repo://readme")
def readme() -> str:
    """Expose the README as an addressable, read-only resource."""
    with open("README.md", encoding="utf-8") as f:
        return f.read()

@mcp.prompt()
def review(file: str) -> str:
    """A reusable template the user can invoke by name."""
    return f"Review {file} for bugs and list concrete fixes."

if __name__ == "__main__":
    mcp.run()   # stdio transport; use transport="streamable-http" to serve remotely

Two details worth copying into production. First, the docstring becomes the tool description the model sees — treat it as prompt engineering, not documentation. Second, MCP lets a tool declare annotations such as readOnlyHint, destructiveHint, idempotentHint, and openWorldHint. Clients use these to auto-approve safe reads and force confirmation on destructive or internet-reaching calls. They are hints from the server, so they improve UX but are not a security boundary.

Connecting an AI app to an MCP server

The client side is shorter than most people expect, and the identical code works against any server — local or remote, yours or someone else’s:

# client.py — discover and call tools on any MCP server
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    params = StdioServerParameters(command="python", args=["server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()                 # negotiate capabilities

            tools = await session.list_tools()
            for t in tools.tools:
                print(t.name, "-", t.description)

            result = await session.call_tool("count_lines", {"path": "README.md"})
            print(result.content[0].text)

asyncio.run(main())

The lifecycle is initializeinitialized notification → tools/listtools/call. If you are wiring a desktop or IDE client instead of writing code, the configuration is the same shape everywhere — a command, arguments, and environment variables:

{
  "mcpServers": {
    "repo-tools": {
      "command": "python",
      "args": ["C:/tools/server.py"],
      "env": { "WORKSPACE": "C:/repo" }
    }
  }
}

That file is why MCP spread quickly: the same server block drops into desktop chat clients and IDE assistants, which is also how you connect Cursor, Cline and Continue to a custom endpoint.

How a gateway exposes MCP behind one OpenAI-compatible endpoint

In practice you hit two problems at once. Your application speaks /v1/chat/completions, while MCP servers speak JSON-RPC. And you do not want one credential, base URL, and rate limit per backend.

A gateway resolves both by doing three jobs. It aggregates many MCP servers into one tool namespace, prefixing names to avoid collisions (github__create_issue versus jira__create_issue). It converts tools/list output into the provider’s function-calling schema so any model can call them. And it presents a single OpenAI-compatible surface, so MCP-capable backends become reachable from code that only knows one request shape.

That is the role qoraapi.com plays as an AI API relay: one key and one endpoint in front of many models, so the bridge below does not care which vendor answers.

# Bridge MCP tools into OpenAI-compatible function calling, one gateway key.
import asyncio, json
from openai import OpenAI
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

client = OpenAI(base_url="https://qoraapi.com/v1", api_key="YOUR_KEY")

def to_openai_tools(tools):
    """MCP already advertises JSON Schema; the conversion is a rename."""
    return [{"type": "function",
             "function": {"name": t.name,
                          "description": t.description or "",
                          "parameters": t.inputSchema}}
            for t in tools]

async def agent(question: str):
    params = StdioServerParameters(command="python", args=["server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            mcp_tools = (await session.list_tools()).tools
            messages = [{"role": "user", "content": question}]

            for _ in range(5):   # bound the loop; never let an agent spin forever
                r = client.chat.completions.create(
                    model="gpt-4o", messages=messages, tools=to_openai_tools(mcp_tools))
                msg = r.choices[0].message
                messages.append(msg)
                if not msg.tool_calls:
                    return msg.content
                for call in msg.tool_calls:
                    res = await session.call_tool(
                        call.function.name, json.loads(call.function.arguments))
                    messages.append({"role": "tool",
                                     "tool_call_id": call.id,
                                     "content": res.content[0].text})
    return None

print(asyncio.run(agent("How many lines are in README.md?")))

Two production notes. Cache the tool list per session and invalidate it on tools/list_changed rather than calling tools/list before every turn. And bound the loop, as above — an unbounded tool-calling loop is the most common way an agent turns a bug into a bill.

Security and discovery: least privilege and capability manifests

The security model is easy to get wrong because it is not where people look. An MCP server runs with your credentials, not the model’s. A Postgres server configured with a superuser DSN hands the model superuser. Instructions like “never delete rows” in a tool description are prompt text, not an access control.

RiskWhere it originatesControl that actually works
Over-privileged serverServer configuration and DSNsRead-only database roles, allowlisted filesystem roots, scoped API tokens
Confused deputyA server accepting and forwarding a token it was not issuedValidate token audience; never pass a client token upstream
Prompt injection via tool outputUntrusted content inside a tool resultTreat results as data, not instructions; require confirmation for destructive calls
Tool-name collisionsAggregating several servers into one namespaceNamespace prefixes and per-server allowlists
Stale tool surfaceClients caching tools/list indefinitelySubscribe to tools/list_changed; pin server versions

Capability manifests are how you enforce the top row. Treat each server as declaring a contract: which tools exist, which annotations they carry, and which scopes the server’s own credentials need. Then let the client decide what to expose to the model at all. A useful default is to publish read-only tools automatically, require human approval for anything marked destructive, and refuse openWorldHint tools in unattended runs.

Finally, log every tools/call with the tool name, arguments, calling user, and outcome. When a prompt injection does get through, that log is the difference between a five-minute diagnosis and a rewrite.

Frequently asked questions

Is MCP replacing function calling?

No. They operate at different layers and are normally used together. Function calling is how a model emits a structured call; MCP is how a tool provider advertises, transports, and authorizes that tool. An MCP server’s tools/list output converts directly into a function-calling tool definition, so adopting MCP usually means adding a discovery layer in front of the schemas you already send.

Do I need MCP if I only have one AI app?

Only if you have more than one tool, or expect the tool surface to change. A single app calling two static internal functions is simpler with plain function calling. MCP pays off when you add a second client, add a third integration, or need runtime discovery — because at that point the alternative is editing and redeploying the host for every tool change.

Can I use MCP with a model that does not support tool calling?

Yes, for part of it. Resources and prompts involve no model-side invocation at all, so a non-tool-calling model can still consume MCP-provided context and templates. Tools are the one primitive that requires a function-calling-capable model and an execution loop in your client.

Is MCP only for local tools?

No, though that is where it started. stdio is the simplest transport because the OS process boundary handles isolation, but Streamable HTTP serves the same protocol over the network with OAuth 2.1 tokens. The tradeoff is that remote servers need real authorization design — audience validation, per-tenant scoping, and no token passthrough — whereas local stdio servers inherit the permissions of the process that spawned them.

Conclusion

MCP standardizes the interface between AI applications and the systems they touch. It does not replace function calling; it feeds it, by turning scattered per-app integrations into discoverable servers any client can reuse. The practical sequence is short: expose one read-only resource and one tool in a local stdio server, connect a client, convert tools/list into your provider’s tool schema, then put a single OpenAI-compatible gateway in front so the model behind it is a string you can change.

Keep the credentials least-privileged, bound your agent loop, and log every tool call. Do those three things and MCP becomes what it is meant to be: plumbing you configure once instead of integrations you maintain forever.

Related reading

Build AI features with one clear API

Qora API gives you a single, focused gateway to connect your apps, scripts and automations to AI. Start with one request.

qoraapi.com · AI API gateway for developers

Comments

One response to “What Is the Model Context Protocol (MCP)? Connect Your AI to Real Tools”

  1. […] What Is the Model Context Protocol (MCP)? Connect Your AI to Real Tools […]

Leave a Reply

Your email address will not be published. Required fields are marked *