Model Context Protocol (MCP) is a JSON-RPC interface between an AI app and a side program. The side program offers tools the model may call, files or records the app may attach, and prompt templates the user may run. This lab defines the pieces, wires a minimal Python server into Claude Code and Cursor, and lists the security limits the spec actually states.
What MCP is for
Before MCP, each host invented its own plugin shape. A Kubernetes helper written for one desktop app did not load in another. MCP splits the work the way the Language Server Protocol split language smarts from the editor: the host stays responsible for the model and the UI; the server stays responsible for the data and the side effects.
The spec’s base is JSON-RPC, UTF-8, with the same message meanings on every transport. A transport only frames bytes. It does not change what a tool is.
Three roles, from the protocol and from the official Python SDK:
| Role | What it is | Example |
|---|---|---|
| Host | The LLM application the person talks to | Claude Code, Cursor, Claude Desktop |
| Client | The MCP speaker inside the host, one per server | The connector that launches python server.py |
| Server | The process that lists tools, resources, and prompts | The short program below |
The server never talks to the model. The host decides when to put a tool definition into the prompt and when to show a prompt template in the UI.
Tools, resources, and prompts
The spec groups server features by who decides to use them:
| Primitive | Who is in control | What it is | DevOps example |
|---|---|---|---|
| Tool | The model | A function the model may call, including writes and API calls | Look up a service name |
| Resource | The application | Data the client attaches as context | A checked-in runbook page |
| Prompt | The user | A template the user invokes, often as a slash command or menu | “Summarize this incident note” |
Clients can also expose elicitation: the server asks the user for more information. That is optional. A server declares capabilities when a client connects. The Python MCPServer declares tools, resources, and prompts for you, and a careful client only requests families the server advertised (tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get).
Cursor’s MCP page says it supports tools, prompts, resources, roots, elicitation, and the MCP Apps UI extension. If a host cannot draw an app view, the tool result still works as ordinary MCP content.
Transports
The 2026-07-28 transport overview names two standard bindings:
- stdio. The client starts the server as a subprocess. Messages are newline-delimited JSON-RPC on stdin and stdout. Cancellation is a
notifications/cancellednotification. Do not print logs to stdout while the server is serving; the Python SDK moves ordinary flushed stdout to stderr so aprintdoes not corrupt the stream. Uselogging, which writes to stderr. - Streamable HTTP. One HTTP endpoint accepts POST. Each JSON-RPC message is its own POST. The response is either one JSON object or a Server-Sent Events stream scoped to that request. This transport replaced the older HTTP+SSE transport from the 2024-11-05 revision, starting in 2025-03-26. The 2026-07-28 revision also removed the GET stream endpoint and protocol-level sessions. Clients have to handle older servers on purpose.
The Python SDK’s run() accepts stdio (the default), streamable-http, and sse. The run guide says SSE still works for clients that have not moved, and tells you not to build a new server on it. Claude Code still has --transport sse and a type of sse for servers that already exist, plus http for Streamable HTTP. In JSON config, streamable-http is an alias of http. An entry with a url and no type is an error: Claude Code treats a missing type as stdio, skips the server, and asks you to set http, sse, or ws. Cursor documents three transports in its table: stdio, SSE, and Streamable HTTP.
Custom transports are allowed if they keep JSON-RPC and the message patterns. A raw TCP or Unix socket should reuse the stdio framing rather than inventing a new one.
A minimal Python server
The official SDK line documented at py.sdk.modelcontextprotocol.io is v2. It needs Python 3.11 or newer. FastMCP was renamed to MCPServer; new code should not import mcp.server.fastmcp.
pip install "mcp[cli]"
uv add "mcp[cli]" is the same install. The [cli] extra provides the mcp command. mcp dev needs npx on PATH because the Inspector is a Node app.
Save this as server.py. It is a closed catalog of exit-code notes, not a copy of the SDK’s arithmetic demo. The decorator rules match the SDK: the function name is the tool name, the docstring is the description, and the type hints are the input schema.
from mcp.server import MCPServer
mcp = MCPServer("exit-notes")
NOTES = {
0: "The process finished without a reported error.",
1: "A generic failure. Read the last log lines before you retry.",
137: "128 plus signal 9. Often an external kill, including an OOM killer.",
143: "128 plus signal 15. The process was asked to terminate.",
}
@mcp.tool()
def explain_exit_code(code: int) -> str:
"""Explain a process exit code from a fixed note table."""
return NOTES.get(code, f"No note for exit code {code}.")
@mcp.resource("note://{code}")
def note(code: str) -> str:
"""Return the same note as a resource the host can attach."""
try:
key = int(code)
except ValueError:
return "Code must be an integer."
return NOTES.get(key, f"No note for exit code {key}.")
@mcp.prompt()
def incident_blurb(code: int) -> str:
"""Build a user message that asks for a one-line incident blurb."""
return (
"Write one sentence an on-call engineer can paste. "
f"Exit code: {code}. Use only the tool result. "
"If the note says you lack evidence, say so."
)
if __name__ == "__main__":
mcp.run()
mcp.run() with no argument serves stdio and blocks. The if __name__ guard matters because mcp dev and mcp run import the file. Check it without a host:
uv run mcp dev server.py
Open the URL the Inspector prints. Call explain_exit_code with 137. You should get the signal-9 note. A non-integer is rejected by the SDK from the type hint, before the function runs. note://137 is a resource template (the {code} is a parameter), so it shows under templates, not as a single static resource. incident_blurb shows under prompts.
For a deployed server, the documented command is:
uv run mcp run server.py --transport streamable-http
The SDK’s first-steps page then connects a client to http://localhost:8000/mcp. Do not point that at the public internet without the authorization section below.
Add it to Claude Code
Run this in a terminal, not inside an existing claude session. The -- is required. Everything after it is the server command, not a Claude Code flag.
claude mcp add --transport stdio exit-notes -- python server.py
That writes local scope: only you, only this project, under the project entry in ~/.claude.json. On Windows that file is %USERPROFILE%\.claude.json. Local scope is the default.
| Scope | Flag | File | Who sees it |
|---|---|---|---|
| Local | --scope local (default) |
~/.claude.json, under this project |
You, this project |
| Project | --scope project |
.mcp.json in the project root |
Anyone who clones the repo, after approval |
| User | --scope user |
~/.claude.json, top-level mcpServers |
You, all projects |
Project scope:
claude mcp add --scope project --transport stdio exit-notes -- python server.py
The resulting .mcp.json shape, matching the documented HTTP example and the stdio fields used for claude mcp serve, is:
{
"mcpServers": {
"exit-notes": {
"type": "stdio",
"command": "python",
"args": ["server.py"]
}
}
}
Use a full path to python or to server.py if the session’s PATH is thinner than your shell. Claude Code sets CLAUDE_PROJECT_DIR in the server process to the project root. In a project .mcp.json, expand it as ${CLAUDE_PROJECT_DIR:-.} if you reference it in command or args.
Inside a session, /mcp lists servers. A project server stays pending until you approve it in an interactive claude session. Reset those choices with claude mcp reset-project-choices. Check one server with claude mcp get exit-notes.
A remote Streamable HTTP server is the other documented form:
claude mcp add --transport http exit-notes https://example.com/mcp
{
"mcpServers": {
"exit-notes": {
"type": "http",
"url": "https://example.com/mcp"
}
}
}
${VAR} and ${VAR:-default} expand in command, args, env, url, and headers. Do not commit a live token. In a remote server’s url and headers, Claude Code substitutes a documented set of credential variable names with an empty value, so a project file cannot forward those secrets.
When the same name exists in more than one scope, the whole entry from the highest precedence wins. Fields are not merged. Local beats project, which beats user, then plugins, then claude.ai connectors. An organization managedMcpServers entry outranks those. The day-to-day Claude Code commands are on the cheat sheet. A hook can still block a tool after MCP has exposed it; see the hooks guide.
Add it to Cursor
Cursor reads MCP config from .cursor/mcp.json (this project) or ~/.cursor/mcp.json (all projects). You can also install from Customize, or from a team marketplace an admin published.
The STDIO field table marks type as required. command is required. args, env, and envFile are optional. envFile works for stdio only. Remote entries use url and optional headers. Values in command, args, env, url, and headers can use ${env:NAME}, ${userHome}, ${workspaceFolder}, ${workspaceFolderBasename}, and ${pathSeparator}.
{
"mcpServers": {
"exit-notes": {
"type": "stdio",
"command": "python",
"args": ["${workspaceFolder}/server.py"]
}
}
}
A remote server:
{
"mcpServers": {
"exit-notes": {
"url": "https://example.com/mcp",
"headers": {
"Authorization": "Bearer ${env:EXIT_NOTES_TOKEN}"
}
}
}
}
Enable the server under Customize. Cursor asks before it runs an MCP tool unless a Run Mode allowlists it. If the process fails, open the Output panel, choose MCP Logs, and read the startup error. One crashed server does not stop the others. The CLI side of the same file is agent mcp list and agent mcp list-tools <name>.
OAuth for remote servers uses the authorization-code flow with PKCE. You can supply a fixed client in an auth object (CLIENT_ID, optional CLIENT_SECRET, optional scopes) when the provider does not support dynamic client registration. Register both redirect URLs the docs list if people sign in from the web and from the desktop app: https://www.cursor.com/agents/mcp/oauth/callback and http://localhost:8787/callback.
Security
Stdio’s trust model is the one to remember. The host launches the server. Both can run with the same OS privileges. The SDK does not defend either side against the other. A consent dialog helps only if you read the command. Practical consequences, from the local-server security notes:
- Decide before launch: who published the server, and whether you will isolate it. The transport will not do that for you.
- Every server in the session shares the model context. A bad server can steer how the agent uses the other tools.
- Pass secrets on that server’s
env(or Cursor’senv/${env:NAME}), not from a shell profile that every process inherits. - Prefer a narrowly scoped token. Keep long-lived secrets out of the JSON if the client can read them from the OS secret store.
- A container or VM is your isolation boundary if the server is third-party.
HTTP authorization is optional in the spec. When you use it, follow the HTTP authorization spec. Stdio implementations should not use that OAuth flow; they should take credentials from the environment. On any HTTP server: HTTPS except for localhost in development, short-lived access tokens, least-privilege scopes, and no tokens in logs.
Claude Code’s project .mcp.json is shared on purpose. Treat a committed args array like a committed script. Cursor’s own list matches this: install from a source you trust, read what the server can reach, restrict API keys, and review code before you connect it to production systems.
A custom GPT Action is a different connector: one OpenAPI document inside ChatGPT, not an MCP server. n8n can attach a registry MCP server as a tool on an AI Agent node; build the workflow first, then add the agent, as in the first n8n workflow.
FAQ
What problem does MCP solve?
MCP is a standard way for an AI application to reach tools and context. You write a server once. Claude Code, Cursor, and other hosts speak the same JSON-RPC protocol instead of a private plugin format for each product.
What is the difference between a host, a client, and a server?
The host is the app the person uses, such as Claude Code or Cursor. The client is the piece inside the host that speaks MCP, one client per server. The server is the program you run. It exposes tools, resources, and prompts. It does not call the model itself.
Which MCP transports should I use?
The current spec defines stdio and Streamable HTTP. Stdio is for a server the host launches on your machine. Streamable HTTP is a single POST endpoint for a server you deploy. SSE is the older HTTP transport. The Python SDK still runs it, and tells you not to build anything new on it.
How do I add an MCP server to Claude Code and to Cursor?
Claude Code: claude mcp add --transport stdio <name> -- <command> [args], or a .mcp.json file with an mcpServers entry. Cursor: .cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project, with command, args, and env for a local process, or url for HTTP or SSE.
Is a local MCP server sandboxed?
No. Stdio means the host starts your program as a child process. The official security notes say the client and that process share one trust domain. The SDK does not sandbox the server. Isolation, if you want it, is a container, a VM, or the host’s own approval prompts.
How this guide was verified
Written by hand from the official documentation, every command re-checked, and reviewed a second time before publishing. Last reviewed on .
Official sources read for this article
- modelcontextprotocol.io/specification/2026-07-28
- modelcontextprotocol.io/specification/2026-07-28/server
- modelcontextprotocol.io/specification/2026-07-28/basic/transports
- modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
- modelcontextprotocol.io/specification/2026-07-28/basic/authorization
- modelcontextprotocol.io/docs/2026-07-28/tutorials/security/local-server-security
- py.sdk.modelcontextprotocol.io/
- py.sdk.modelcontextprotocol.io/get-started/first-steps/
- py.sdk.modelcontextprotocol.io/run/
- code.claude.com/docs/en/mcp
- cursor.com/docs/mcp

