An introductory guide for engineers and technical decision-makers. From concepts to practice, with Python SDK examples.
1. Why MCP?
Before MCP, wiring an AI application to external systems looked roughly like this:
- Want the model to query a database? Write a function calling schema, write a handler, write permission checks.
- Want the model to read files? Do it all again.
- The same "look up a GitHub Issue" capability — Claude Desktop, Cursor, VS Code, your own Agent — each implements it separately.
- As tools accumulate, prompt-maintenance cost grows fast, and cross-application reuse is effectively zero.
This is the classic M × N integration problem: M AI applications × N data sources/tools = M × N integration codebases.
MCP (Model Context Protocol) is an open protocol led by Anthropic that has become the de facto standard. Its goal is to collapse M × N into M + N:
- Data-source/tool vendors only need to implement an MCP Server once;
- AI applications only need to implement an MCP Client once;
- Any Client can plug into any Server.
The official one-liner: MCP is to AI applications what USB-C is to electronics — a single, unified interface that connects everything.
2. The Big Picture in One Diagram
┌─ Tools (model-controlled, side effects)
Server ─────┼─ Resources (application-controlled, read-only context)
/ └─ Prompts (user-controlled, workflow templates)
/
Client
/
Host
\ ┌─ Sampling (Server asks LLM in reverse)
Client ───────────┼─ Elicitation (Server asks user in reverse)
\ ├─ Roots (Client tells Server its working directories)
\ └─ Logging (Server emits logs)
Server
Data layer: JSON-RPC 2.0
Transport layer: stdio (local) / Streamable HTTP (remote, OAuth supported)- Transport layer: Client and Server speak JSON-RPC 2.0, carried over stdio or Streamable HTTP.
- Server primitives: capabilities Server actively exposes to the Host, targeting three different "decision-makers" — the model (Tools), the application (Resources), and the user (Prompts).
- Client primitives: capabilities Client exposes for Server to call back, letting the Server complete agentic workflows without depending directly on an LLM/UI itself.
3. The Three Most Confusable Roles: Host / Client / Server
This is the easiest place for newcomers to get tangled up. The relationship looks like this:
Notice that Server C in the lower right is connected to two Clients at once — a remote Server typically serves multiple Clients, while a local stdio Server is usually 1:1.
| Role | What it is | Examples |
|---|---|---|
| Host | The AI application the user interacts with directly; coordinates multiple Clients | Claude Desktop, Cursor, VS Code, ChatGPT, your own Agent |
| Client | A component inside the Host; each Client maintains a 1:1 dedicated connection to one Server | The Host creates one Client per configured Server at startup |
| Server | A program that exposes tools/data/templates, either local or remote | filesystem, postgres, github, sentry, Slack, etc. |
A common misconception: that a "Server" must be a remote service. Wrong. Server is a protocol role, not a deployment shape. A local subprocess launched over stdio is just as much a Server.
4. The Two Layers of the Protocol: Data + Transport
MCP's design cleanly separates two layers so they can be reused in different scenarios.
1. Data Layer
Built on JSON-RPC 2.0, defining message structure and semantics. It includes:
- Lifecycle management: connection setup, capability negotiation, connection teardown
- Server capabilities: tools, resources, prompts
- Client capabilities: sampling, elicitation, logging, roots
- Notification mechanism: list changes, progress updates, etc.
2. Transport Layer
Responsible for shuttling data-layer JSON messages between the two ends. MCP currently defines two official transports:
| Transport | Use case | Characteristics |
|---|---|---|
| stdio | Local Server | The Host launches the Server as a subprocess and communicates over stdin/stdout. Zero network overhead, ideal for local tools (files, Git, local DBs). |
| Streamable HTTP | Remote Server | Clients send requests via HTTP POST; servers optionally push streaming responses via Server-Sent Events (SSE). Supports standard HTTP auth (Bearer Token, OAuth) — well suited to SaaS-style MCP Servers. |
Older versions of the protocol had a separate SSE transport, but the new spec folds it into Streamable HTTP. For new code, just pick Streamable HTTP.
5. The Three Server Primitives: Tools / Resources / Prompts
This is the part you'll touch most often in day-to-day development. The three are easy to confuse, but their design intent is completely different — the key distinction is "who decides to invoke them".
| Primitive | Control | Analogy | Typical use |
|---|---|---|---|
| Tools | Model-controlled | Like REST POST |
The model decides to invoke; produces side effects: writing a DB, sending a message, calling an API |
| Resources | Application-controlled | Like REST GET |
Passive, read-only data source. The host application chooses when to push which context to the model |
| Prompts | User-controlled | Like a Slash Command | Prebuilt workflow templates the user actively invokes (e.g. /plan-vacation) |
I like to remember it as:
Tools are verbs, Resources are nouns, Prompts are workflows.
Tools: invoked by the model
# Hearing the user say "check the weather in San Francisco", the model decides to call this tool itself
@mcp.tool()
def get_weather(city: str) -> str:
"""Get the current weather for a city"""
return fetch_weather_api(city)Related methods: tools/list (discovery), tools/call (execution).
Resources: read on demand by the application
# The application decides whether to feed this README to the model as context
@mcp.resource("file:///{path}")
def read_file(path: str) -> str:
return open(path).read()Each resource has a URI (e.g. file:///README.md, postgres://mydb/schema) and supports templating (weather://forecast/{city}). Related methods: resources/list, resources/templates/list, resources/read, resources/subscribe.
Tools vs Resources: full data flow and how to choose
Looking only at "who controls it" isn't intuitive enough. Compare the full data flow of the two:
Tools flow (triggered within a model turn):
1. Host starts up → calls tools/list to get schemas
2. Host injects schemas into the tools field of the LLM call
3. User sends a message → LLM reasons → emits a tool_use block
4. Host intercepts → routes to the right Server → sends tools/call
5. Server executes (runs SQL, hits an API) → returns content
6. Host stitches content back into the conversation as tool_result
7. LLM continues generating (may trigger another round, looping 3-6)Resources flow (injected outside the conversation turn):
1. Host starts up → calls resources/list and resources/templates/list
2. Host surfaces resources in the UI (file tree / @ completion / auto-suggestion)
3. Three ways a resource enters conversation context:
a. User manually @-references it
b. Application heuristically injects it
c. Bridged as a Tool so the model can invoke it (detailed below)
4. Host calls resources/read(uri) → Server returns contents
5. Host attaches contents as part of a user message to the LLM
6. By the time the LLM sees it, it's already "in the context" as a factOne-line distinction:
Tools are pulled by the model during an LLM turn; Resources are pushed by the application outside an LLM turn.
"Data, but requires computation" — which should I pick? Decision order:
- Side effects? Yes → must be a Tool (even if just for telemetry).
- Want the model to decide autonomously? → Tool.
- Want the user/application to preselect what gets pushed as context? → Resource Template.
- Client ecosystem considerations: today, Tools are supported in every Host, while UI support for Resources varies a lot. Many Servers expose the same capability as both a Tool and a Resource — Tool for fallback compatibility, Resource for a more elegant presentation on Hosts that support it.
Can Resources also be driven by the LLM? The protocol layer doesn't forbid it — "application-controlled" is design intent, not a technical constraint. The common pattern is Resource-as-Tool bridging: the Host injects two synthetic tools into the LLM's tool list:
{ "name": "list_resources", "description": "List all available MCP resources", ... }
{ "name": "read_resource", "description": "Read a resource by URI", ... }The LLM decides to call read_resource(uri="postgres://schema/users") → the Host forwards it as resources/read. The form is a Tool, but the essence is the LLM driving a Resource. Claude Code does this by default.
What exactly is "application heuristic injection"? A few real-world scenarios:
- Context injection in IDE-style Hosts: every message automatically includes the file currently under the cursor, the few most recently edited files, and convention files like
.cursorrules/CLAUDE.md. - Project-open auto-loading: when a project is opened, automatically read
README.md,package.json,.env.exampleas system context. - Semantic search auto-recall: embed the user message, vector-search across all resources, automatically splice in the Top-K hits.
- Subscription-based live sync: user edits a file → Server pushes
notifications/resources/updated→ Host refreshes the new content into the next turn of context.
None of these require explicit initiation from user or model; the Host decides via its own ruleset.
Prompts: explicitly user-triggered
@mcp.prompt()
def plan_vacation(destination: str, days: int) -> str:
"""Generate a vacation plan"""
return f"Please help me plan a {days}-day trip to {destination}..."In applications like Claude Desktop, Claude Code, and Cursor, Prompts typically surface as / commands or shortcuts in a command palette.
Three commonly confused points about Prompts deserve clarification:
1. "User-only triggering" is a UX convention, not a protocol law. The protocol layer doesn't restrict who calls prompts/get — any party with a session can. "user-controlled" is a UX recommendation given to Hosts in the spec, intended to make users feel that they are actively initiating the action and to discourage models from silently invoking templates. A custom Agent can absolutely prompts/get automatically at a workflow node — technically nothing stops it.
2. The return value is not just a string. What prompts/get actually returns is a messages array:
{
"messages": [
{ "role": "user", "content": { "type": "text", "text": "..." } },
{ "role": "assistant", "content": { "type": "text", "text": "..." } },
{ "role": "user", "content": { "type": "image", "data": "base64...", "mimeType": "image/png" } },
{ "role": "user", "content": { "type": "resource", "resource": { "uri": "...", "text": "..." } } }
]
}You can construct multi-turn few-shot examples, multimodal prompts containing images/audio, or embedded resource references. In FastMCP, having @mcp.prompt() return a string is just a convenience wrapper — it gets auto-wrapped into a single user message. For richer structures, explicitly return list[Message]:
from mcp.server.fastmcp.prompts import base
@mcp.prompt()
def debug_session(error: str) -> list[base.Message]:
return [
base.UserMessage("I ran into this error:"),
base.UserMessage(error),
base.AssistantMessage("Let me help analyze. First, a few clarifying questions..."),
]3. Prompts ≠ a lightweight version of Skills. These two are often conflated, but they solve completely different problems:
| Dimension | MCP Prompts | Claude Skills |
|---|---|---|
| Core problem | Let the user initiate a known task with a template | Let the model autonomously acquire a capability when appropriate |
| Trigger | User (actively selects in UI) | Model (decides based on SKILL.md frontmatter) |
| Form | Protocol messages, fetched over JSON-RPC | A markdown bundle on the filesystem (with optional scripts) |
| Content | Returns a messages array fed to the LLM | Markdown instructions + executable code + resource files |
| Execution | Pure template substitution; no code execution | The Agent may execute scripts inside |
| Control plane | Server primitive | Model-side capability |
A more accurate analogy:
Prompts are like IDE Code Snippets or Notion Templates: user selects → template gets filled in. Skills are like Unix man pages + executable scripts: the model reads the manual → decides whether to use it → may also execute the embedded code.
In a sentence: Prompts are "templates the user can use"; Skills are "capabilities the model can learn".
6. Client Primitives: Letting the Server "Call" the Client Back
The Server isn't just a passive responder — it can also actively request capabilities from the Client. There are four client primitives:
| Client primitive | Purpose | Canonical scenario |
|---|---|---|
| Sampling | Server asks the Host's LLM to run inference on its behalf | Server wants to analyze 47 flight options but doesn't want to embed an LLM SDK, so it has the Client do it |
| Elicitation | Server asks the user for information or confirmation | "Please confirm this $3,000 order," paired with a JSON Schema describing the required fields |
| Roots | Client tells the Server "you may only operate within these directories" | The IDE exposes the currently open project directory to the Server |
| Logging | Server emits logs to the Client for debugging and observability | Print debug info during tool execution |
Why Sampling is elegant: the Server wants to use an LLM but doesn't want to pay for one or be locked to a specific provider, so it reverse-delegates "calling the LLM" to the Client. The Client already has LLM access (the user has paid for/authorized it), so the Client bears the cost and effort, and the user can audit both the prompt and the response in the middle — this is human-in-the-loop by design.
7. Lifecycle: A Full Handshake
With roles and primitives in mind, let's walk through a minimal complete JSON-RPC flow — useful for the next time you need to debug with packet captures.
Step 1. Initialization handshake
// Client → Server
{
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": { "elicitation": {} },
"clientInfo": { "name": "example-client", "version": "1.0.0" }
}
}// Server → Client
{
"jsonrpc": "2.0", "id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": {}
},
"serverInfo": { "name": "example-server", "version": "1.0.0" }
}
}// Client → Server (notification — no id, no return value)
{ "jsonrpc": "2.0", "method": "notifications/initialized" }A few key points here:
- Protocol version negotiation: both sides must use compatible versions, otherwise the connection terminates.
- Capability declarations: the Server explicitly lists the primitives it supports under
capabilities— in this example onlytoolsandresourcesare declared, meaning the Server doesn't support prompts and the Client won't issue anyprompts/*calls. A server that declarestools.listChanged: truewill proactively sendnotifications/tools/list_changedwhenever its tool list changes. {}is not an empty object: it means "I support this capability, with no configurable options" — the minimal form of a declaration.
Step 2. Discover tools
// Client → Server
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }Step 3. Invoke a tool
// Client → Server
{
"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {
"name": "weather_current",
"arguments": { "location": "San Francisco", "units": "imperial" }
}
}Step 4. Server-initiated notification
// Server → Client (tool list changed — no response needed)
{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }8. Hands-On with the Python SDK
The rest uses the official Python SDK (mcp) throughout.
1. Install
uv is recommended:
uv init mcp-demo
cd mcp-demo
uv add "mcp[cli]"Or with pip:
pip install "mcp[cli]"2. A minimal Server: Tools + Resources + Prompts all together
server.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo")
# ---- Tool: invoked by the model; has side effects ----
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# ---- Resource: read on demand by the application ----
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Generate a personalized greeting based on name"""
return f"Hello, {name}!"
# ---- Prompt: explicitly user-triggered ----
@mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
"""Generate a greeting prompt template in different styles"""
styles = {
"friendly": "Please write a warm, friendly greeting",
"formal": "Please write a formal greeting",
"casual": "Please write a casual, easygoing greeting",
}
return f"{styles[style]}, addressed to a person named {name}."
if __name__ == "__main__":
mcp.run() # stdio transport by defaultThree ways to launch:
# Debug: MCP Inspector provides a web UI
uv run mcp dev server.py
# Install into Claude Desktop
uv run mcp install server.py
# Switch to HTTP transport (recommended for production)
# In code, change to mcp.run(transport="streamable-http")3. Structured output: auto-generate schemas with Pydantic
FastMCP auto-derives JSON Schema from type annotations, making return data easier for the model to understand:
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Weather")
class WeatherData(BaseModel):
temperature: float = Field(description="degrees Celsius")
humidity: float = Field(description="percent")
condition: str = Field(description="weather condition, e.g. sunny / cloudy")
@mcp.tool()
def get_weather(city: str) -> WeatherData:
"""Get weather for a city (structured return)"""
return WeatherData(temperature=22.5, humidity=45.0, condition="sunny")4. Context injection: logging, progress, Sampling, Elicitation
Add a Context parameter to a tool and FastMCP will auto-inject it. With it you can do logging, progress reporting, reverse LLM calls, and prompting the user for input:
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import SamplingMessage, TextContent
mcp = FastMCP("Advanced")
# Progress reporting + logging
@mcp.tool()
async def long_task(name: str, ctx: Context, steps: int = 5) -> str:
await ctx.info(f"Starting task: {name}")
for i in range(steps):
await ctx.report_progress(
progress=(i + 1) / steps,
total=1.0,
message=f"step {i+1}/{steps}",
)
return f"Task {name} done"
# Sampling: ask the Client's LLM in reverse
@mcp.tool()
async def summarize(topic: str, ctx: Context) -> str:
"""Have the Host's LLM summarize for me"""
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=f"Summarize {topic} in one sentence"),
)
],
max_tokens=100,
)
return result.content.text if result.content.type == "text" else str(result.content)5. Lifecycle management: set up connections on startup, clean up on shutdown
When you need to initialize long-lived resources like database connection pools at startup, use lifespan:
from dataclasses import dataclass
from contextlib import asynccontextmanager
from mcp.server.fastmcp import Context, FastMCP
@dataclass
class AppContext:
db: "Database"
@asynccontextmanager
async def app_lifespan(server: FastMCP):
db = await Database.connect()
try:
yield AppContext(db=db)
finally:
await db.close()
mcp = FastMCP("MyApp", lifespan=app_lifespan)
@mcp.tool()
def query(sql: str, ctx: Context) -> str:
app_ctx: AppContext = ctx.request_context.lifespan_context
return app_ctx.db.execute(sql)6. Client: stdio connection to a local Server
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(
command="uv",
args=["run", "server.py"],
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("Tools:", [t.name for t in tools.tools])
result = await session.call_tool("add", arguments={"a": 5, "b": 3})
print("Result:", result.content[0].text)
asyncio.run(main())7. Client: Streamable HTTP connection to a remote Server
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
async with streamablehttp_client("http://localhost:8000/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("Tools:", [t.name for t in tools.tools])
asyncio.run(main())9. Concrete Mapping in Claude Code
Theory done — let's see what the three primitives actually look like inside a real Host. Using Claude Code as the example:
Tools → mcp__<server>__<tool> tools
Each Server's tools are renamed and injected into the model's tool list. For example, with the github MCP server installed, the model sees a tool called mcp__github__create_issue.
A noteworthy mechanism is Tool Search (on by default): tool definitions are not stuffed into the context window all at once — the model first sees a ToolSearch tool and looks up tools on demand. So even with 20 Servers installed, the context window doesn't blow up. You can use alwaysLoad: true to force frequently used Servers to stay resident.
Resources → @ references + bridged Tools, both at once
Claude Code's support for Resources is a textbook illustration of the previous section — application-driven and model-driven exist side by side:
Path A: user-initiated @ reference
Can you analyze @github:issue://123 and suggest a fix?
Compare @postgres:schema://users with @docs:file://database/user-modelTyping @ triggers autocomplete, listing available resources from all connected Servers, with fuzzy search. The referenced resource is pulled into the context as an attachment.
Path B: model-initiated invocation (bridged as Tool)
From the official docs:
"Claude Code automatically provides tools to list and read MCP resources when servers support them"
Claude Code automatically wraps resources/list and resources/read as built-in tools exposed to the model — so the model can decide on its own whether to look up resources.
Prompts → /mcp__<server>__<prompt> slash commands
Each Server-defined prompt maps to a slash command:
/mcp__github__list_prs
/mcp__github__pr_review 456
/mcp__jira__create_issue "Bug in login flow" high- Arguments are parsed according to the prompt's schema, space-separated;
- Spaces in server and prompt names are normalized to underscores;
- The messages array returned by a prompt is spliced directly into the conversation.
Support status for client primitives
- Elicitation: pops an interactive dialog (form mode or URL mode); hooks can auto-respond.
- Roots: at Server startup, the
CLAUDE_PROJECT_DIRenvironment variable is set; the Server can also callroots/listto query the current working directory.
Takeaways for Server authors
Different clients vary widely in their support for MCP primitives. If you want your Server to maximize compatibility:
- Tools support is mandatory — this is currently the greatest common denominator across all MCP clients.
- Expose important capabilities twice: write a Tool as the fallback, and also a Resource Template so Hosts that support it can present it more elegantly. For a lookup, for example, provide both a
query_user(id)Tool and auser://{id}Resource Template. - Prompts are a nice-to-have: for Hosts that support Prompts, they noticeably improve the UX for common workflows.
10. MCP vs Function Calling vs Custom Plugin Protocols
This is one of the most common questions in internal tech talks.
| Dimension | Function Calling | Custom plugin protocol | MCP |
|---|---|---|---|
| Standardization | Each LLM vendor defines its own | Single company, internal | Cross-vendor open standard |
| Ecosystem reuse | One schema per model | Effectively zero | Implement once, reused across Hosts |
| Context resources | Usually just "function calls" | Custom | Three primitives: Tools / Resources / Prompts |
| Reverse capabilities | Not supported | Generally not supported | Sampling / Elicitation / Roots |
| Transport & auth | Embedded API | Custom | stdio / Streamable HTTP + OAuth |
| Best for | Narrow scope within a single app | Heavily customized private use | Tools/data reused across multiple endpoints; standardization required |
A rough decision rule:
- Capability serves a single AI app and is essentially never reused → Function Calling is enough.
- Capability will be consumed by multiple AI apps (multiple internal products + third-party tools like Claude Desktop / Cursor) → prefer MCP.
- You need to expose resources and workflow templates to users/models, not just functions → MCP's Resources / Prompts are what Function Calling doesn't have.
11. When Not to Use MCP
Every recommendation has a counter-example. In these situations, forcing MCP only adds complexity:
- Pure prompt engineering: no interaction with external systems, purely prompt tuning — you don't need MCP.
- Latency-critical, same-process calls: even with stdio, MCP incurs JSON-RPC serialization overhead; for latency-sensitive scenarios a direct function call is cheaper.
- Capability tightly coupled to the application and never reused: rolling your own function calling is lighter.
- General-purpose cross-language, cross-process RPC: that's not MCP's goal — use gRPC / OpenAPI.
12. A Few Tips for Production
When actually deploying MCP, here are a few lessons from stepping on the rakes:
1. Namespace your tool names
Don't call it search; call it github_search_issues or jira_search_tickets. Only then can the model route accurately when several Servers are connected at once.
2. description is written for the model, not for humans
Tool descriptions go directly into the model's context, and wording materially affects invocation accuracy. Be explicit: what it does, when to use it, when not to, and the input constraints.
3. Dangerous operations must use Elicitation
Writing to a DB, sending messages, charging a card — operations with real side effects should proactively call elicitation/create to get user confirmation, rather than trusting that "the model won't go off the rails."
4. Pair Streamable HTTP with OAuth
For remote Servers, stop hardcoding Bearer Tokens in config. OAuth is the auth method recommended by the spec; pair it with roots to scope the Server's working area.
5. Make good use of listChanged notifications
When the tool set changes dynamically with business needs (e.g. a new data source is hooked up), emit notifications/tools/list_changed — the Client refreshes immediately, no restart required.
6. Use the MCP Inspector during development
uv run mcp dev server.pyIt spins up a web UI to browse tools/resources/prompts, invoke them manually, and inspect JSON-RPC frames — ten times faster than print-debugging.
13. References
- Official documentation homepage: https://modelcontextprotocol.io/docs/
- Architecture overview: https://modelcontextprotocol.io/docs/learn/architecture
- Server concepts (Tools / Resources / Prompts): https://modelcontextprotocol.io/docs/learn/server-concepts
- Client concepts (Sampling / Elicitation / Roots): https://modelcontextprotocol.io/docs/learn/client-concepts
- Latest specification: https://modelcontextprotocol.io/specification/latest
- Python SDK: https://github.com/modelcontextprotocol/python-sdk
- Official reference Server repository: https://github.com/modelcontextprotocol/servers
- MCP Inspector (the debugging weapon): https://github.com/modelcontextprotocol/inspector
Closing Thoughts
MCP appeared at the end of 2024, and today it's already broadly supported by mainstream AI applications — Claude, ChatGPT, VS Code, Cursor — becoming the de facto standard of the AI tool ecosystem.
Its design philosophy is simple:
Protocol stays protocol; model stays model. MCP doesn't dictate how you use an LLM or manage context. It defines just one thing — "how AI applications communicate with external systems" — and makes it general enough and clean enough.
For engineers, the payoff of understanding MCP is high: write a Server once and every Host in the ecosystem can reuse it; write a Client once and any community Server plugs in. In an era of accelerating AI-application fragmentation, MCP is one of the few protocol layers where engineering value accrues across vendors and across products.
