Introduction
Every AI application that does more than chat eventually runs into the same wall: the model is smart, but it is blind. It cannot see your database, cannot read your ticket tracker, cannot call your internal billing API, and cannot check the status of a deployment. For the model to be useful in a real business context, someone has to wire it up to the outside world, and until recently that wiring was bespoke, brittle, and repeated by every team that tried it. The Model Context Protocol, or MCP, is Anthropic's answer to that wiring problem. It is an open specification, not a product, and it has grown from a November 2024 release into what is now widely treated as the default way to connect large language model applications to tools, data, and services.
This article is written for engineers and technical leads who need more than a marketing overview. We will define MCP precisely, explain the problem it solves and why that problem was structural rather than incidental, walk through its architecture and protocol mechanics in enough depth to reason about it in a design review, show working code, and then zoom out to how MCP changes decisions at the level of software architecture, system design, product development, and the software development lifecycle. Where the protocol has real, documented weaknesses, we will say so plainly, because treating a young standard as flawless is a disservice to anyone building on it.
The Problem: Why AI-Tool Integration Was Broken
Before MCP, giving a model access to an external system meant writing a custom adapter for that specific model and that specific system. If you wanted Claude to query your PostgreSQL database, you wrote a tool schema in Anthropic's function-calling format, implemented the handler, and shipped it. If you then wanted the same capability inside a product built on OpenAI's API, you rewrote the schema in OpenAI's function-calling format and reimplemented the same handler, often with subtly different validation rules and error conventions. Add a second data source, a third model provider, or a new internal team that wants the same integration, and the number of adapters you maintain grows as the product of models and tools rather than the sum. Engineers who lived through the early wave of LLM app development will recognize this as an N*M integration problem: N applications, each needing to talk to M tools, with no shared contract between them.
This is not a new shape of problem in software engineering; it is the same shape that motivated ODBC for databases, POSIX for operating systems, and the Language Server Protocol for code editors. In each case, a combinatorial integration burden was resolved by inserting a standard interface in the middle, so that a database driver, a filesystem, or a language analyzer could be written once and consumed by any compliant client. The Language Server Protocol (LSP) is the closest and most instructive precedent for MCP: before LSP, every editor had to write its own integration for every programming language's tooling; after LSP, a language server author writes one implementation and every LSP-compliant editor, from VS Code to Neovim, can use it. MCP applies the same move to AI applications: a tool or data source is exposed once, as an MCP server, and any MCP-compliant client, whether that is Claude Desktop, an IDE, or a custom internal agent, can consume it without bespoke glue code.
The second half of the problem is less about integration count and more about consistency of behavior. Even within a single model provider, ad hoc tool integrations tend to diverge in how they handle authentication, pagination, error reporting, streaming, and user consent, because every team invents its own conventions. This inconsistency makes tool use hard to secure and hard to reason about at scale: a security review has to evaluate each integration on its own terms because there is no shared contract to audit against. MCP does not eliminate the need for careful security review, but it gives every integration a common shape, so that lifecycle management, permissions, and auditing can be reasoned about once at the protocol level instead of once per integration.
Deep Technical Explanation: MCP Architecture and Protocol Mechanics
MCP defines three roles. The host is the AI application the user actually interacts with - Claude Desktop, an IDE like a Claude Code environment, or a custom agent runtime. The host manages the overall session, the conversation with the model, and user-facing permissions. The client is a component, instantiated by the host, that maintains a stateful, one-to-one connection to a single server. A host typically creates one client per server it wants to talk to, which means a host can be connected to many servers simultaneously through many clients, but each client only ever talks to one server. This one-to-one boundary is a deliberate security property: it prevents one server from silently reaching into a session it was never granted access to. The server is the program that actually exposes capabilities - it might wrap a database, a SaaS API, a filesystem, or an internal microservice, and it can run locally as a subprocess or remotely as a hosted service.
Underneath these roles, MCP is a JSON-RPC 2.0 protocol. Every request, response, and notification exchanged between a client and a server is a JSON-RPC message, which gives MCP a well-understood, language-agnostic wire format rather than a bespoke one. A session begins with an initialize handshake in which the client and server each declare the capabilities they support - whether the server offers tools, resources, or prompts, and whether the client can handle sampling requests, elicitation, or root directory listings. This capability negotiation matters architecturally because it means neither side has to guess what the other supports; features can be added to the protocol over time without breaking older implementations, since a client or server simply does not advertise capabilities it does not implement.
The protocol's real substance is its set of primitives.
On the server side there are three:
- tools - which are callable functions described with a name, a natural-language description, and a JSON Schema for their input, and which the model can choose to invoke to take an action such as writing a file or placing an order;
- resources - which are read-only, URI-addressable context such as a log file, a config value, or a database record, intended to be loaded into context rather than executed;
- prompts - which are reusable, parameterized prompt templates that a server can publish so that clients get a consistent, curated way to kick off a particular workflow;
On the client side, MCP defines the inverse capabilities:
- sampling - which lets a server ask the client's own model for a completion - useful when a server needs a small amount of LLM reasoning as part of fulfilling a request, without embedding its own model credentials;
- elicitation - which lets a server pause and ask the human user for additional input mid-task;
- roots - which let a server learn which directories or URIs it is allowed to operate within;
This bidirectionality is easy to miss on a first read of MCP, and it is one of the more architecturally interesting decisions in the spec: a server is not just a passive function library, it can actively participate in a reasoning loop by requesting help from the model or the user through the client.
Transport and versioning are the parts of MCP that have changed the most since the original November 2024 release, and it is worth tracking that evolution because it tells you where the protocol's real engineering pressure has been. The original spec supported stdio for local subprocess servers and an HTTP+SSE transport for remote ones. The 2025-03-26 revision replaced HTTP+SSE with Streamable HTTP, a single endpoint supporting both POST and GET with optional Server-Sent Events for server-to-client streaming, which simplified deployment behind ordinary load balancers. The 2025-06-18 revision added structured tool output and the elicitation primitive. The most consequential change is the 2026-07-28 specification, which removed protocol-level sessions entirely in favor of a stateless core - eliminating the Mcp-Session-Id header dependency so that any request can be served by any server instance behind standard HTTP infrastructure, alongside a formal extensions framework, a long-running-task mechanism, and tightened OAuth-based authorization. That shift, from a stateful session model to a stateless one, is precisely the kind of decision a distributed-systems engineer should recognize: it trades some conversational simplicity for horizontal scalability, and it happened because production MCP deployments hit the same load-balancing and failover problems that any stateful RPC service eventually hits.
Mental Model: MCP as the Connector Layer, Not the Intelligence Layer
The analogy that MCP's own community reaches for most often is USB - a standardized physical and logical connector that lets any compliant peripheral plug into any compliant port, replacing a world of proprietary cables. It is a reasonable analogy for the outcome MCP produces - a tool built once can be used by many clients - but it slightly overstates how "plug and play" the protocol actually is in practice, since a tool still needs a well-written description and schema before a model can use it reliably. A more precise mental model, and one closer to how working engineers should actually think about it, is the Language Server Protocol comparison introduced earlier: MCP is to AI tool access what LSP is to code intelligence. LSP did not make editors smart about every language; it gave editors a standard channel through which a separately maintained language server could supply that intelligence. MCP does not make a model smart about your internal systems; it gives the model a standard channel through which a separately maintained server can supply that context.
Holding that mental model steady helps avoid a common misconception: MCP is not an agent framework, not a replacement for your REST or GraphQL APIs, and not a reasoning engine. It is a transport and schema layer sitting between a model-facing client and a capability-facing server. Your existing APIs do not go away - an MCP server for your orders system is typically a thin wrapper that translates JSON-RPC tool calls into calls against the REST API you already run for human-facing clients. Keeping this distinction clear when a stakeholder asks "should we replace our API with MCP" saves a lot of confused architecture discussions; the honest answer is almost always that you build an MCP server in front of the API you already have, not instead of it.
Software Architecture Perspective
From a software architecture standpoint, MCP is best understood as an integration-layer pattern, similar in spirit to an API gateway or an enterprise service bus, but scoped specifically to the needs of LLM-driven clients. Adopting it means making an explicit architectural decision about where "tool access" lives in your system. The common and generally recommended pattern is to treat each MCP server as a bounded, single-responsibility service - one server per domain or per backing system, rather than one monolithic server exposing every capability in the company. A server wrapping your CRM should not also own your CI/CD pipeline's tools; keeping servers narrowly scoped mirrors the same bounded-context discipline that motivates microservice decomposition, and it keeps the blast radius of a compromised or misbehaving server small.
A second architectural decision is where MCP servers should run relative to the systems they wrap. Local stdio servers are appropriate for developer tools and IDE integrations where the server and the host share a machine and trust boundary - the classic example is a filesystem or git server launched by an editor. Remote Streamable HTTP servers are appropriate when the capability needs to be shared across users or when the backing system is itself a hosted service; the official Sentry MCP server, for instance, runs on Sentry's own infrastructure and is consumed remotely by any client that wants it. Choosing wrong in either direction has real costs: pushing a shared, sensitive capability into a locally-spawned stdio server means every user runs their own copy with their own credentials scattered across developer machines, while forcing an inherently local capability like filesystem access into a remote server adds unnecessary network hops and a much harder security story.
Finally, MCP pushes a useful architectural question to the surface that many teams skip when they build ad hoc tool integrations: what is the actual contract for this capability, independent of any particular model or client? Writing an MCP server forces you to define tool names, input schemas, and descriptions as a first-class artifact, versioned and reviewable like any other API contract, rather than as inline strings scattered through a prompt-engineering codebase. Teams that treat this contract with the same rigor they apply to a public API - schema review, backward-compatibility rules, deprecation policies - get most of MCP's architectural benefit even before they think about multi-client reuse.
System Design Perspective: Scaling, Statelessness, and Security
System design questions around MCP mostly reduce to three concerns: how the protocol behaves under load, how a session is authenticated and authorized, and how much trust a host should extend to a server it did not write. On load, the 2026-07-28 move to a stateless protocol core is the dominant fact to design around. In the earlier, stateful model, a client's session was pinned to a specific server process via a session identifier, which meant horizontal scaling required sticky routing or shared session storage - the same problem every stateful RPC service has faced for decades. The stateless core removes that constraint at the protocol layer: any compliant server instance behind a standard load balancer can answer any request, because the necessary context travels with the request rather than living in server-side session state. If you are designing a remote MCP server for production traffic, this means you can adopt the deployment patterns you already use for stateless HTTP services - horizontal autoscaling, rolling deploys, and health-check-driven failover - instead of inventing session-affinity infrastructure specifically for MCP.
Authentication and authorization follow a pattern any API designer will recognize: MCP recommends OAuth for remote Streamable HTTP servers, with bearer tokens, API keys, or custom headers as the credential carried on each request. The 2026-07-28 spec tightened this further with what its authors describe as authorization hardening aligned more closely with OAuth and OpenID Connect deployment norms, plus an enterprise-managed authorization capability aimed at organizations that need centralized control over which servers a given user or team can reach. The system design implication is that an MCP server sitting in front of a sensitive backend should be designed exactly like any other OAuth-protected API - validating token scope on every call, never trusting a session identifier as a substitute for re-verifying identity, and logging every tool invocation with enough detail to support an audit.
The trust question is the one most unique to MCP and the one system designers should spend the most time on, because it does not have a close analog in traditional API design: the model sees every tool description from every connected server as part of its operating context, and it cannot reliably distinguish a legitimate instruction from an instruction smuggled into a tool's metadata or into the content a tool returns. This means a system design for MCP needs an explicit threat model for what happens when one connected server is malicious or compromised, and what a compromised server can reach - which is exactly the confused-deputy and cross-server exfiltration concern we cover in more depth in the pitfalls section below. Good system design here treats every MCP server as an untrusted input source by default, gates high-impact tools like file writes, financial transactions, or credential access behind explicit human confirmation, and never assumes that a tool's name or description accurately reflects what it will actually do.
Implementation Walkthrough: Building an MCP Server and Client
Concepts are easiest to internalize with working code. The example below is a TypeScript MCP server, built on the official @modelcontextprotocol/sdk package, that wraps a fictional internal orders service. It exposes two tools - one read-only lookup and one that performs a state-changing action - and runs over the local stdio transport, which is the right choice for a server launched directly by a developer's editor or agent runtime.
// orders-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { ordersClient } from "./orders-client.js";
const server = new McpServer({
name: "orders-service",
version: "1.2.0",
});
server.tool(
"get_order",
"Fetch a single order by its ID, including line items and current status".,
{ orderId: z.string().min(1) },
async ({ orderId }) => {
const order = await ordersClient.findById(orderId);
if (!order) {
return {
content: [{ type: "text", text: `No order found for ID ${orderId}` }],
isError: true,
};
}
return {
content: [{ type: "text", text: JSON.stringify(order, null, 2) }],
};
}
);
server.tool(
"refund_order",
"Issue a full or partial refund for an order. Requires explicit user confirmation upstream".,
{
orderId: z.string().min(1),
amountCents: z.number().int().positive(),
reason: z.string().min(3),
},
async ({ orderId, amountCents, reason }) => {
const result = await ordersClient.refund(orderId, { amountCents, reason });
return {
content: [
{
type: "text",
text: `Refunded ${amountCents / 100} on order ${orderId}. Status: ${result.status}`,
},
],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
The Python equivalent, using the official mcp package's high-level FastMCP interface, shows the same pattern with less boilerplate, since decorator-based tool registration handles schema generation from type hints automatically.
# orders_server.py
from mcp.server.fastmcp import FastMCP
from orders_client import OrdersClient
mcp = FastMCP("orders-service")
orders = OrdersClient()
@mcp.tool()
async def get_order(order_id: str) -> str:
"""Fetch a single order by its ID, including line items and current status".""
order = await orders.find_by_id(order_id)
if order is None:
return f"No order found for ID {order_id}"
return order.to_json()
@mcp.tool()
async def refund_order(order_id: str, amount_cents: int, reason: str) -> str:
"""Issue a full or partial refund for an order. Requires explicit user confirmation upstream".""
result = await orders.refund(order_id, amount_cents=amount_cents, reason=reason)
return f"Refunded {amount_cents / 100:.2f} on order {order_id}. Status: {result.status}"
if __name__ == "__main__":
mcp.run(transport="stdio")
Wiring either server into a host is a matter of configuration rather than code. A host like an agent runtime or an MCP-compatible IDE typically reads a small JSON block that tells it which servers to launch and how:
{
"mcpServers": {
"orders-service": {
"command": "node",
"args": ["./dist/orders-server.js"],
"env": { "ORDERS_API_TOKEN": "${ORDERS_API_TOKEN}" }
}
}
}
Once connected, the host's model can call tools/list to discover get_order and refund_order, and tools/call to invoke them - all without the host application ever needing orders-domain-specific code. The same server, unmodified, works from any other MCP-compatible client, which is the entire point.
Product Development Lifecycle: Where MCP Fits
Product teams building AI-powered features run into MCP earlier than they expect, usually the first time a feature requires the model to act on a live system rather than just answer questions about static context. During discovery and scoping, MCP changes the calculus of build-versus-integrate: a product team evaluating whether to add, say, calendar-aware scheduling to an assistant feature can first check whether a maintained MCP server already exists for the calendar provider in question, rather than scoping a custom integration from scratch. This shifts some early product decisions from "how much engineering time will this integration take" to "which existing servers cover our requirements, and where are the gaps we still need to build".
During the definition and design phase, product managers and engineers need to jointly decide what level of autonomy a given MCP-backed capability should have - whether a tool call executes immediately or requires human confirmation, and what happens when a tool call fails or returns ambiguous results. These are product decisions with direct architectural consequences: a refund tool that a product spec says must always have human sign-off needs the elicitation primitive or an equivalent host-level confirmation step designed in from day one, not bolted on after a security review flags it. Writing these consent and autonomy requirements into the product spec, rather than leaving them as an implementation detail, is one of the more valuable process changes teams should make when they know a feature will touch MCP tools with real-world side effects.
Rollout and iteration look different too, because MCP externalizes part of the feature surface. If your product exposes an MCP server that other teams or even external partners can connect to, you are effectively shipping an API product with its own versioning and deprecation obligations, and the product lifecycle needs to account for that the same way it would for any public API: a changelog, a support channel for integrators, and a policy for how long an old tool version stays available after a new one ships. Teams that treat an MCP server as an internal implementation detail, with no external contract discipline, tend to break downstream agents the first time they rename a tool or change a parameter's required-ness - exactly the kind of breaking change a public API team would never ship without a deprecation window.
Post-launch, the feedback loop for an MCP-backed feature has a distinctive shape: because the model chooses when and how to invoke a tool, usage telemetry needs to capture not just whether users engaged with a feature but whether the model invoked the right tool with the right arguments, and how often it needed a retry or produced an error the user had to work around. This telemetry is a genuinely new product-analytics surface - it did not exist for deterministic UI-driven features - and product teams that skip instrumenting it tend to discover tool-selection problems from support tickets instead of from dashboards.
Software Development Lifecycle: Build, Test, Ship, Operate
For the engineers actually building MCP servers, the development lifecycle gains a few steps that do not exist for a typical internal library or REST endpoint. During design, the tool's name, description, and JSON Schema are not internal implementation details - they are the primary interface the model reasons over, so they deserve the same design scrutiny as a public API's documentation. A vague description like "manages orders" gives the model far less to work with than "fetches a single order by ID, including line items and current fulfillment status", and in practice, description quality is one of the single biggest levers on whether a model calls the right tool at the right time.
During implementation, the official MCP Inspector (run via npx @modelcontextprotocol/inspector) is the standard tool for exercising a server in isolation before wiring it into a full agent loop - it provides a UI for listing a server's tools, invoking them with sample arguments, and inspecting the raw JSON-RPC traffic, which lets you validate schema correctness and error handling without burning model calls or dealing with an LLM's nondeterminism while you are still debugging plumbing. This mirrors a pattern any backend engineer will recognize from testing a REST endpoint with a tool like Postman before wiring it into a frontend.
Testing an MCP server has two distinct layers, and conflating them is a common mistake. The first layer is deterministic and should be tested exactly like any other service: given a specific input, does the tool return the correct output, does it handle malformed input gracefully, does it enforce authorization correctly, and does it fail safely when a downstream dependency is unavailable. Standard unit and integration testing practices apply here without modification. The second layer is behavioral and probabilistic: given a particular user request in context, does the model actually choose to call this tool, with reasonable arguments, at the right point in its reasoning? This layer benefits from small, targeted evaluation sets - a handful of representative prompts run against the live tool description with assertions on which tool gets called and with what arguments - run as part of CI whenever a tool's description or schema changes, since a seemingly innocuous wording tweak can measurably change model behavior.
Shipping and operating an MCP server follows conventional service-ownership practices with AI-specific additions: version the server using the protocol's date-based specification versioning where relevant, log every tool invocation with enough context to support an audit trail (who initiated it, what arguments were passed, what the server returned), and monitor for anomalous call patterns the same way you would monitor for anomalous API traffic, since an unusually high rate of a sensitive tool being called is a meaningful signal whether the cause is a bug, a misconfigured agent, or an active attack.
Trade-offs and Pitfalls
MCP's core trade-off is one every standardization effort makes: it optimizes for reusability and interoperability at some cost to per-integration control. A bespoke, single-purpose integration between a specific model and a specific tool can be tuned exactly to that pairing's needs - custom retry logic, provider-specific prompt formatting, tight coupling to a particular UI. An MCP server has to speak a generic protocol that any client can consume, which sometimes means leaving performance or UX optimizations on the table in exchange for the integration working everywhere. For a narrow, single-team, single-model use case with no plan to reuse the integration elsewhere, the overhead of building a fully spec-compliant MCP server may genuinely exceed the benefit, and a direct function-calling integration can be the more pragmatic choice.
The most serious and well-documented pitfalls are security-related, and they deserve to be taken seriously rather than waved away as theoretical. Because a model reads every connected server's tool descriptions as part of its operating context, a malicious or compromised server can embed instructions inside a tool description or a returned result that the model treats as legitimate guidance - a pattern security researchers, including Simon Willison and the team at Invariant Labs, named tool poisoning shortly after MCP's release. A related "rug pull" variant lets a server present an innocuous tool description at approval time and silently change it later, so that a tool a user or admin approved on day one behaves differently on day seven without triggering a new consent prompt. In one widely reported November 2025 incident, a poisoned MCP integration was used to exfiltrate WhatsApp message history by hiding instructions inside tool metadata that redirected data to an attacker-controlled destination; separately, CVE-2025-54136 ("MCPoison") demonstrated the same rug-pull pattern applied to server configuration files, and CVE-2025-6514 showed that command injection through MCP server configuration could lead to remote code execution on a client machine. None of these are flaws unique to MCP's wire format so much as they are consequences of a more general problem - LLMs cannot reliably distinguish data from instructions - but MCP's multi-server, dynamically-discovered nature widens the attack surface compared to a small set of hand-vetted tools, particularly when multiple servers share the same model context and one can potentially override or intercept calls intended for another.
A second, less dramatic but more common pitfall is operational: teams adopt MCP servers from public registries without the same vendor and dependency scrutiny they would apply to a new production dependency, effectively expanding their software supply chain to include every MCP server they connect. A compromised or poorly maintained third-party server is a supply-chain risk in exactly the sense that a compromised npm package is, and it deserves the same review process - checking maintenance activity, pinning versions, and understanding what the server can actually reach - before it is wired into anything with access to real data or credentials.
A third pitfall is more mundane but shows up constantly in early adoption: treating tool descriptions as an afterthought. Because the model's tool-selection behavior is driven almost entirely by names, descriptions, and schemas, teams that copy-paste terse internal function names and docstrings into MCP tool definitions consistently see worse tool-selection accuracy than teams that write descriptions specifically for a model audience, with explicit guidance on when to use the tool and what its parameters mean.
Best Practices
Scope each MCP server tightly around a single domain or backing system, and resist the urge to build one server that exposes "everything the company has". Narrow scoping limits what a compromised or misbehaving server can reach, makes authorization simpler to reason about, and keeps tool catalogs small enough that a model can actually choose well between them - most models perform noticeably worse at tool selection once a session has dozens of loosely related tools available simultaneously.
Treat tool descriptions and schemas as reviewed, versioned interface contracts rather than implementation details. Write descriptions for the model's benefit, not the developer's: state clearly what the tool does, when it should be used, what each parameter means, and what the caller should expect on success or failure. Run these descriptions through the same kind of review you would apply to public API documentation, and add small model-facing evaluation sets to CI so that a description change that degrades tool-selection accuracy is caught before it ships.
Default every server to the principle of least privilege, and gate any tool with real-world side effects - financial transactions, deletions, external communications, credential access - behind explicit human confirmation using the elicitation primitive or an equivalent host-level check, rather than trusting the model's judgment alone. Treat every connected MCP server, including ones you did not write, as an untrusted input source with respect to prompt injection: validate and sanitize tool outputs before they influence further tool calls, log every invocation for audit purposes, and monitor for tool descriptions that change after initial approval.
Apply ordinary software supply-chain discipline to third-party MCP servers: pin versions, review maintenance activity and provenance before connecting a server with access to sensitive systems, and prefer official or well-audited servers over unmaintained community ones for anything touching production data. Finally, design for the transport and authorization model your deployment actually needs rather than defaulting to the most complex option - a local stdio server with no network exposure is often the right and sufficiently secure choice for developer-facing tools, while a remote server behind OAuth is the right choice only once a capability genuinely needs to be shared across users or systems.
The 80/20 of MCP
If you strip away the specification's growing surface area, the vast majority of the practical value of MCP comes from three ideas working together: a standardized tool schema that any model can reason over, a clean separation between the host/client (which manages the conversation and the user) and the server (which owns a specific capability), and a transport-agnostic JSON-RPC core that lets the same server run locally or remotely without changing its logic. Everything else in the spec - sampling, elicitation, roots, the extensions framework, structured tool output, Tasks - extends that core in genuinely useful but secondary ways. An engineer who deeply understands tool schemas, the host/client/server boundary, and the two transports can build and reason about the overwhelming majority of real MCP integrations without needing to master the full specification on day one.
The corollary is where teams should spend their scarce engineering attention: not on protocol minutiae, but on the quality of tool descriptions and the rigor of the trust boundary around each server. Two servers that are protocol-compliant in identical ways can produce wildly different outcomes in production if one has carefully written, unambiguous tool descriptions and tight authorization, and the other does not. The protocol gives you a consistent shape to build on; it does not give you good judgment about what belongs behind a confirmation prompt, and that judgment is where most of the real engineering work - and most of the risk - actually lives.
Key Takeaways
MCP is worth adopting deliberately rather than reflexively, and the following steps translate the concepts above into things you can do this week.
- Audit before you build. Before writing a new MCP server, check whether a maintained one already exists for the system you need to reach - the ecosystem has grown large enough that duplicating effort is a real risk.
- Scope servers by domain, not by convenience. One server per bounded capability, with the smallest tool surface that gets the job done, keeps both model performance and your security review tractable.
- Write tool descriptions for the model, and review them like API documentation. This single practice has an outsized effect on tool-selection accuracy and is the cheapest lever available to improve reliability.
- Gate irreversible actions behind explicit confirmation. Any tool that spends money, deletes data, or sends communication on a user's behalf should require a human-in-the-loop step, not model discretion alone.
- Treat every connected server as an untrusted input source and every server dependency as a supply-chain risk. Log tool invocations, monitor for description changes, and vet third-party servers before connecting them to anything sensitive.
Conclusion
MCP did not invent the idea of giving language models access to tools; function calling existed well before it. What MCP did was recognize that tool access was about to become a many-to-many integration problem and get ahead of it with a standard, in the same tradition as ODBC and the Language Server Protocol before it. Judging by its trajectory since the November 2024 release - rapid growth in SDK adoption, native support from multiple model providers, and a specification that has already gone through several substantial revisions culminating in the stateless 2026-07-28 core - that bet has largely paid off, and MCP is a reasonable default for teams building AI applications that need to reach beyond the model itself.
None of that makes MCP a solved problem. The protocol's security model is still catching up to the reality that a model cannot fully distinguish trusted instructions from untrusted data smuggled through a tool description or a tool's output, and the incidents documented by researchers over the past two years are a fair warning rather than a footnote. The engineering discipline that made distributed systems and public APIs trustworthy - least privilege, explicit trust boundaries, careful interface design, supply-chain scrutiny, and real auditability - applies to MCP servers just as directly as it applies to anything else you expose to the outside world. Treat MCP as infrastructure that deserves the same rigor as the rest of your stack, and it earns the trust that its rapid adoption suggests it already has.
References
- Anthropic. "Introducing the Model Context Protocol". November 2024. https://www.anthropic.com/news/model-context-protocol
- Model Context Protocol. Official Specification, version 2026-07-28. https://modelcontextprotocol.io/specification/2026-07-28
- Model Context Protocol. "Architecture overview". https://modelcontextprotocol.io/docs/learn/architecture
- Model Context Protocol Blog. "The 2026-07-28 Specification". July 28, 2026. https://blog.modelcontextprotocol.io/posts/2026-07-28/
- Model Context Protocol Blog. "The 2026 MCP Roadmap". March 9, 2026. https://blog.modelcontextprotocol.io/posts/2026-mcp-roadmap/
- Model Context Protocol. GitHub organization and SDK repositories. https://github.com/modelcontextprotocol
- JSON-RPC Working Group. "JSON-RPC 2.0 Specification". https://www.jsonrpc.org/specification
- Microsoft. "Language Server Protocol Specification". https://microsoft.github.io/language-server-protocol/
- Anthropic. "Building Effective Agents". December 2024. https://www.anthropic.com/research/building-effective-agents
- Willison, Simon. "Model Context Protocol has prompt injection security problems". April 9, 2025. https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/
- Invariant Labs. "MCP Security Notification: Tool Poisoning Attacks". April 2025. https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks
- OWASP. "MCP Security Cheat Sheet". OWASP Cheat Sheet Series. https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html
- Speakeasy. "MCP core concepts: tools, resources, prompts, and transports". https://www.speakeasy.com/mcp/core-concepts/