MCP Is the Standard Now: Building Cross-Client Servers
Back to all articles
AI Agents
9 min read10 min read

MCP Is the Standard Now: Building Cross-Client Servers

ChatGPT, Claude, Cursor and VS Code all speak MCP in 2026. What changes when your tools serve every client: descriptions, token budgets, error design and auth.

Debasish Maji
Debasish Maji
AI Engineering Lead
August 25, 2026
MCPModel Context ProtocolAI AgentsTool UseAI EngineeringLLMs

MCP won, and that changes how you should design tools

For most of 2025 and early 2026, the argument against building on the Model Context Protocol was reasonable: it was an Anthropic protocol, Claude was the main consumer, and writing to a single-vendor spec is how you end up rewriting things.

That argument is over. The MCP documentation now lists Claude, ChatGPT, Visual Studio Code, Cursor, MCPJam and others as clients. When the two largest model providers consume the same tool protocol, it stops being a vendor integration and becomes infrastructure.

The practical consequence is not "MCP is good now." It is that the unit of reuse in agent engineering has moved. For two years the reusable artefact was a prompt. Now it is a server. That is a bigger shift than it sounds, and most teams have not restructured around it.

What actually changes when a tool becomes cross-client

When you wrote function-calling handlers for one model, you could cheat. You knew the context window, you knew how that model handled ambiguity, you knew roughly how it would phrase arguments, and you tuned descriptions against it.

A cross-client MCP server gives up all of those assumptions. Your search_orders tool will be called by models with different context sizes, different tool-selection behaviour, and different tolerance for large responses. Some will be aggressive agentic models running long autonomous loops; others will be a developer poking at it from an IDE.

Three design rules follow, and they are the difference between a server that works everywhere and one that works on your laptop.

Rule 1: The tool description is your API contract, and it is read by a model

This is the part experienced engineers underrate. You are not writing documentation for a human who will read the whole page. You are writing the only thing a model sees when deciding whether to call your tool.

Bad, and extremely common:

Python
@server.tool()
def search_orders(query: str, limit: int = 10) -> list[dict]:
    """Search orders."""

A model reading that has no idea what query accepts. Free text? An order ID? A customer email? SQL? It will guess, and it will guess differently across clients, which is exactly the failure mode you cannot reproduce locally.

Better:

Python
@server.tool()
def search_orders(query: str, limit: int = 10) -> list[dict]:
    """Find orders by customer email, order ID, or product name.

    Use this when the user asks about order status, history, or refunds.
    Do NOT use this for inventory questions; use check_inventory instead.

    Args:
        query: Customer email (jane@acme.com), order ID (ORD-12345),
               or product name (partial match, case-insensitive).
               Does not accept SQL or date ranges.
        limit: Max results, 1-50. Default 10. Prefer small values;
               large result sets crowd out context.

    Returns:
        Orders sorted newest first. Each has: id, status, total_cents,
        currency, created_at, customer_email. Empty list if no match.
    """

The additions that matter most are the negative instructions ("Do NOT use this for inventory") and the concrete argument examples. Tool confusion, where the model picks a plausible-but-wrong tool, is the single most common failure in multi-tool setups, and the fix is almost always in the description rather than the code.

The rule I use: if two tools in your server could plausibly answer the same question, each description must say when not to use it.

Rule 2: Budget your response size in tokens, not rows

A REST endpoint returning 500 rows is fine. An MCP tool returning 500 rows can be catastrophic, because that response lands directly in a context window that something else also needs.

Worse, you no longer control which context window. A response that is 4% of a 1M-token window is 30% of a 200k one.

So return the smallest useful payload and give the model a way to ask for more:

Python
MAX_INLINE_RESULTS = 10

@server.tool()
def search_orders(query: str, limit: int = 10) -> dict:
    rows = db.search(query, limit=min(limit, 50))
    shown = rows[:MAX_INLINE_RESULTS]
    return {
        "results": [compact(r) for r in shown],
        "shown": len(shown),
        "total_matches": len(rows),
        # Tell the model how to get the rest instead of dumping it.
        "more_available": len(rows) > len(shown),
        "next_step": (
            f"Call get_order(id) for full detail, or narrow the query."
            if len(rows) > len(shown) else None
        ),
    }

def compact(row):
    # Six fields the model can act on, not the 40-column DB record.
    return {
        "id": row.id, "status": row.status,
        "total_cents": row.total_cents, "currency": row.currency,
        "created_at": row.created_at.isoformat(),
        "customer_email": row.customer_email,
    }

Two things are happening here. The payload is trimmed to fields a model can reason about, and the response teaches the model what to do next. Returning next_step is unusual in API design and extremely effective in tool design, because the consumer is a reasoning system that will follow instructions in the payload.

This is the same discipline as managing context deliberately. Every token your tool returns is a token unavailable for reasoning.

Rule 3: Errors are prompts, not status codes

When a REST call fails, a human reads the error. When an MCP tool fails, a model reads it and decides what to do next. An unhelpful error produces a retry loop; a helpful one produces a correction.

Python
# Useless: the model retries the same broken call.
raise ValueError("Invalid input")

# Useful: the model can fix it on the next attempt.
raise ValueError(
    "No customer found for 'jane@acme'. That looks like a truncated "
    "email. Try the full address, or call find_customer(name='Jane') "
    "to look up their email first."
)

Write every error message as an instruction to the model about what to try next. This one change removes a surprising share of agent loops, and it costs nothing.

Authentication is where cross-client gets genuinely hard

Everything above is craft. Auth is architecture, and it is where most teams get stuck.

The problem: a locally-run server inherits the developer's machine credentials. A remote server serving many users cannot. The moment your server is consumed by ChatGPT, Claude and an IDE simultaneously, "whose credentials are these" stops being obvious.

The failure mode is specific and serious. A tool that runs with ambient service-account permissions will happily execute a request that originated from a prompt injection. If your server can read any customer's records and the model can be talked into asking for the wrong one, your tool is the vulnerability, not the model.

The rule is straightforward and frequently violated: authorise at the tool boundary, against the end user, on every call.

Python
@server.tool()
def search_orders(query: str, ctx: Context, limit: int = 10) -> dict:
    user = ctx.authenticated_user          # never a service account
    if not user:
        raise PermissionError("Not authenticated.")

    # Scope in the query itself. Do not fetch then filter.
    rows = db.search(query, limit=limit, tenant_id=user.tenant_id)
    return {"results": [compact(r) for r in rows]}

Scope in the query, not after it. "Fetch then filter" means the wrong data was already in memory, one refactor away from being returned.

And treat every tool argument as untrusted input, because it is: those arguments were written by a model that read content you may not control. This is the prompt injection problem arriving through a new door, and the standard production defences apply. Parameterised queries, least privilege, allowlists on anything that touches a filesystem or a shell.

When MCP is the wrong choice

MCP is a distribution mechanism. It is worth the overhead when a capability needs to be consumed by clients you do not control.

Skip it when:

  • The tool is single-app and internal. If only your agent calls it, native function calling is less machinery. Use ordinary function-calling patterns.
  • You need sub-100ms latency. The protocol hop is real. An in-process function call is faster.
  • The operation is irreversible and unattended. Payments, deletions, production deploys. Not because MCP is unsafe, but because broad exposure plus autonomous invocation is a bad combination. Keep a human in the loop.

The best candidates are read-heavy, useful across surfaces, and cheap to call: search over internal docs, querying observability data, reading tickets, checking build status.

The shift worth internalising

For two years the reusable artefact in AI engineering was the prompt. Teams built prompt libraries, versioned them, tested them.

Prompts do not transfer well. They are tuned to a model's quirks and they rot with each release.

Servers transfer. A well-built MCP server for your internal docs works in ChatGPT, in Claude, in Cursor, in VS Code, and in whatever ships next year, without modification. It survives model upgrades because it does not encode anything model-specific.

If you are deciding where to spend engineering effort in agent infrastructure, that is the argument. Invest in the layer that does not need rewriting when the model changes. You can compare how different models behave on the LLM leaderboard, and the thing that stands out is how quickly the ranking churns. Build on the part that does not.

Frequently Asked Questions

Is MCP only for Anthropic models?

No. The MCP documentation lists Claude, ChatGPT, Visual Studio Code, Cursor, MCPJam and others as clients. Support from both major model providers is what moved it from a vendor integration to a de facto standard, and it is the main reason building an MCP server is now a reasonable investment.

Should I replace all my function calling with MCP?

No. MCP is a distribution mechanism, worth the overhead when a capability must be consumed by clients you do not control. For a tool only your own application calls, native function calling is simpler and faster. Reach for MCP at the point where a second consumer appears.

What is the most common reason MCP servers fail in production?

Tool confusion, where the model picks a plausible but wrong tool. The fix is almost always the description rather than the code: state explicitly when not to use each tool, and give concrete examples of valid argument values. Overly large responses are a close second, because they crowd out the context the model needs to reason.

How should MCP tools handle authentication?

Authorise at the tool boundary, against the end user, on every call, and scope the data access inside the query rather than filtering afterwards. Never run tools with ambient service-account permissions. A tool with broad standing access will execute requests that originated from prompt injection, which makes the tool the vulnerability rather than the model.

Does MCP add meaningful latency?

Yes, there is a real protocol hop compared to an in-process function call. For most tool calls this is negligible next to model inference time, but if you need sub-100ms tool execution inside a tight loop, call the function directly and skip the protocol.

Where to go from there

If you want the fundamentals before building a server, start with the MCP tutorial, then the guide to building AI agents for how tools fit into a full agent loop.

If you would rather build one live, I run a 2-hour hands-on workshop most Sundays where we build working systems with real code and no slides. ₹499 / $19 with a full refund guarantee. See what's coming up →

Found this helpful?

Share it with others who might benefit

TweetShare

Related articles

📚 Continue Learning