What Is MCP? A Practical Model Context Protocol Guide

,
AI Tooling logo for AI agent and LLM development guides

Model Context Protocol is often introduced as “USB-C for AI applications.” The analogy is useful, but it can make MCP sound like a universal plugin system that automatically turns an LLM into an agent. It does not.

MCP standardizes how an AI application discovers and uses capabilities exposed by another process or service. Those capabilities may include tools that perform actions, resources that provide context, and reusable prompts. The host application still decides which servers to connect to, what the model can see, whether a tool call needs approval, and how results enter the conversation.

The practical value appears when integrations start multiplying. Without a protocol, every AI client needs custom code for every database, repository, local tool, or internal API. MCP provides a common session and message model, while each server remains responsible for its own domain.

What problem does MCP solve?

Imagine an engineering assistant that needs to search documentation, read build metadata, query a test database, and create an issue. The model can describe what it wants to do, but it should not receive unrestricted filesystem access, database credentials, or a collection of undocumented Python callbacks.

MCP inserts a structured boundary:

user
|
v
MCP host application
|
+-- MCP client session --> documentation server
|
+-- MCP client session --> build tools server
|
+-- MCP client session --> issue tracker server

The official architecture uses a host-client-server model. A host can manage multiple MCP clients, and each client maintains a one-to-one session with a particular server. During initialization, the client and server negotiate protocol versions and capabilities.

MCP is built on JSON-RPC, but JSON-RPC alone is not the main benefit. The protocol defines lifecycle, discovery, capability negotiation, standard primitives, and transport behavior. That lets a host connect to independently developed servers without inventing a new integration contract each time.

Tools, resources, and prompts are not interchangeable

Tools perform an operation

A tool is a callable capability with a name, description, and input schema. Examples include running a query, creating a ticket, calculating a result, or triggering a deployment check. Tool calls can have side effects, so hosts should expose approval and audit controls instead of treating every discovered tool as safe.

Resources provide context

A resource exposes data identified by a URI, such as a file, schema, document, or application record. Resources are generally application-driven: the host determines how they are browsed, selected, searched, or included in model context.

Prompts package reusable interaction patterns

A prompt supplies a reusable message template or workflow entry point. It can make a server’s domain knowledge easier to invoke, but it is not a replacement for the host’s system instructions or security policy.

A frequent design mistake is exposing everything as a tool. Reading a stable document may fit a resource better. A dangerous operation should not be disguised as a harmless context lookup. Choose the primitive based on ownership and side effects, not on which decorator is easiest to write.

Install the official Python SDK

The example below targets Linux and the current official Python SDK. It uses uv because the SDK documentation uses it for isolated, reproducible projects.

mkdir mcp-inventory-demo
cd mcp-inventory-demo
uv init
uv venv
source .venv/bin/activate
uv add "mcp[cli]"

The CLI extra provides commands for running and inspecting servers. The exact SDK surface can change across major versions, so pin the resolved version in a real project and keep examples aligned with that version’s documentation.

Hands-on: build a small inventory MCP server

This server exposes one tool and one resource. The tool checks inventory for a part. The resource returns a short catalog entry. The data is local and read-only so the protocol mechanics remain visible.

# server.py
from mcp.server import MCPServer
mcp = MCPServer("Inventory Demo")
INVENTORY = {
"camera-front": {"quantity": 12, "location": "A-14"},
"lidar-roof": {"quantity": 4, "location": "B-03"},
"radar-corner": {"quantity": 28, "location": "C-09"},
}
@mcp.tool()
def check_inventory(part_id: str) -> dict[str, object]:
"""Return quantity and storage location for a known part."""
item = INVENTORY.get(part_id)
if item is None:
return {
"found": False,
"part_id": part_id,
"message": "Unknown part ID",
}
return {
"found": True,
"part_id": part_id,
**item,
}
@mcp.resource("inventory://parts/{part_id}")
def part_resource(part_id: str) -> str:
"""Return a readable inventory record for one part."""
item = INVENTORY.get(part_id)
if item is None:
return f"Part {part_id} was not found."
return (
f"Part: {part_id}\n"
f"Quantity: {item['quantity']}\n"
f"Location: {item['location']}"
)

Type hints become the tool’s input schema. The docstring becomes part of the description clients can show to the model or user. That convenience makes naming important: a vague function name and an optimistic docstring can cause incorrect tool selection even when the Python code is valid.

Inspect the server before connecting an LLM

uv run mcp dev server.py

The development command launches the MCP Inspector, which can initialize a session, list capabilities, call tools, and read resources. The Inspector is a better first test than connecting a full AI host because it separates protocol behavior from model behavior.

Call check_inventory with:

{
"part_id": "camera-front"
}

The structured result should contain values shaped like:

{
"found": true,
"part_id": "camera-front",
"quantity": 12,
"location": "A-14"
}

Then read inventory://parts/lidar-roof from the Resources panel. If both operations work in the Inspector, the server’s registration and schemas are probably correct. It still does not prove that a particular desktop client or agent configuration launches the same Python environment.

Call the MCP server from Python

An in-process client is useful for a deterministic smoke test because it removes subprocess paths and network transports from the equation.

# client.py
import asyncio
from mcp import Client
from server import mcp
async def main() -> None:
async with Client(mcp) as client:
tools = await client.list_tools()
print("tools:", [tool.name for tool in tools.tools])
result = await client.call_tool(
"check_inventory",
{"part_id": "radar-corner"},
)
print("result:", result.structured_content)
resource = await client.read_resource(
"inventory://parts/camera-front"
)
print("resource:", resource.contents[0].text)
asyncio.run(main())
uv run python client.py

The exact formatting of result objects belongs to the SDK version, but the important sequence is stable: establish a session, initialize capabilities, discover what the server exposes, and then call or read by name.

For a remote deployment, run the server with Streamable HTTP and connect the client to the MCP endpoint:

uv run mcp run server.py --transport streamable-http
async with Client("http://127.0.0.1:8000/mcp") as client:
result = await client.call_tool(
"check_inventory",
{"part_id": "camera-front"},
)

Use 127.0.0.1 for local development unless remote access is intentional. A server bound to every interface without authentication is not merely a convenient demo; it is a network-accessible action surface.

Where the LLM fits

The MCP client code above does not call an LLM. It proves the integration boundary first. A host application typically lists tools, translates their schemas into the model provider’s tool format, sends the user’s request to the model, receives a requested tool call, asks the MCP server to execute it, and sends the result back to the model.

user request
|
v
LLM chooses check_inventory
|
v
host requests approval if required
|
v
MCP client calls server tool
|
v
result returns to host and LLM

This separation matters during debugging. If the Inspector cannot call the tool, fix the server or protocol connection. If the tool works but the model never selects it, inspect the tool name, description, schema, and host prompt. If the model selects it incorrectly, that is not evidence that MCP transport is broken.

Common MCP issues and fixes

The Inspector does not open

The Inspector is a Node.js application. Confirm node and npx are available on PATH. If the Python server imports correctly but mcp dev fails before a connection appears, test the Node tooling separately.

node --version
npx --version
uv run python -c "import mcp; print('mcp import ok')"

A stdio server disconnects immediately

Stdio transport uses standard input and output for protocol messages. Debug prints written to stdout can corrupt the JSON-RPC stream. Send diagnostic logs to stderr and make sure the host launches the correct interpreter and absolute script path.

import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO)

Also reproduce the exact command configured in the host. A server that works under an activated shell may fail when a desktop application launches the system Python without the project’s environment.

The tool does not appear in the client

Confirm the decorator executes during module import and that the host completed initialization before listing tools. Restart or reconnect after changing registrations because clients may cache discovery results. If the tool exists in the Inspector but not in one host, compare protocol and SDK versions and inspect that host’s MCP logs.

The model sees the tool but never uses it

Improve the semantic contract rather than adding transport retries. Use a precise action-oriented name, explain when the tool should and should not be used, provide narrow argument types, and return structured data. Overlapping tools with vague descriptions force the model to guess.

Streamable HTTP returns 404, 405, or a connection error

Connect to the MCP endpoint, commonly /mcp, rather than the server root. Confirm the client is using Streamable HTTP instead of an older SSE-specific configuration. Check reverse proxies for HTTP method restrictions, buffering, and dropped headers before changing server logic.

Remote requests receive HTTP 403

Streamable HTTP servers are required to validate the Origin header to reduce DNS rebinding risk. A rejected origin should not be bypassed by allowing every origin in production. Configure the expected host and origin, bind locally during development, and add authentication for remote access.

A tool call hangs or takes too long

Add timeouts around downstream APIs and design cancellation behavior. The protocol connection being alive does not make the database or external service reliable. Return a clear tool error with enough context for the host to decide whether retrying is safe.

The server works, but it is unsafe

Successful discovery is not a security review. Restrict filesystem roots, validate paths after resolving symlinks, use least-privilege credentials, separate read and write tools, and require confirmation for consequential actions. Treat tool descriptions and resource contents from untrusted servers as data, not as instructions that override the host’s policy.

Pros, limitations, and alternatives

Where MCP helps

  • One server can expose capabilities to multiple compatible hosts.
  • Discovery and schemas reduce hand-written integration glue.
  • Local stdio servers keep simple tools off the network.
  • Resources, tools, and prompts provide clearer semantic boundaries.
  • The host can centralize consent, logging, and model interaction.

Where MCP adds cost

  • Protocol and SDK versions must remain compatible.
  • Tool quality still depends on descriptions, schemas, and error design.
  • Remote servers require real authentication and network security.
  • An MCP wrapper does not repair a poorly designed underlying API.
  • Debugging spans the host, client session, transport, server, and downstream system.

A direct SDK or REST call remains reasonable when one application owns one stable integration. OpenAPI tool generation can work well for existing HTTP services. Function calling inside a single codebase may be simpler when portability is not a requirement. Use MCP when the reusable client-server boundary is valuable, not merely because the protocol is popular.

FAQ

Is MCP an AI agent framework?

No. MCP standardizes context and capability exchange. Planning loops, memory, model selection, approval policy, and agent orchestration belong to the host or another framework.

Does an MCP server contain an LLM?

Not necessarily. Many servers are ordinary adapters around files, databases, APIs, and developer tools. The host typically owns the primary model interaction.

Should I use stdio or Streamable HTTP?

Use stdio for a local server launched and supervised by the host. Use Streamable HTTP when the server must run as a separately deployed service. Remote transport increases the authentication, authorization, and operational work required.

Can MCP tools run arbitrary code?

A server can expose a tool that runs code, but that does not make it safe. Avoid general shell tools unless the threat model, sandbox, allowlists, audit trail, and approval flow are explicit.

Conclusion

MCP is most useful as an integration boundary. It gives AI hosts a consistent way to discover tools, read resources, retrieve prompts, and maintain sessions with specialized servers. It does not remove the need for careful API design or security decisions.

Start with a small read-only server, inspect it without an LLM, add an in-process test, and only then connect a real host. That order makes it possible to tell whether a failure belongs to Python, transport, protocol discovery, tool design, or model behavior.

Use the official MCP architecture specification, Python SDK documentation, and MCP Inspector as the primary references. For a related knowledge workflow built around persistent Markdown rather than live tool connections, see LLM Wiki Explained.

Leave a Reply

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