Protocol · tools

Model Context Protocol

MCP is not a framework. It is an open standard so one tool server can be used by Claude, Cursor, VS Code, and an agent you write yourself. You describe the tool once. Every host lists it and calls it.

IntermediatePython 3.10+ · MCP SDK

Host, client, server

AnalogyMCP is a universal remote. The host is the person holding it. Each client is one button wired to one device. The server is the device. The host does not grow a custom plug for every device.
Architecture
Host — Cursor, Claude, or your app
Client — one connection per server
Server — tools, resources, and prompts
Transport — stdio locally, or Streamable HTTP remotely

The protocol is JSON-RPC. The spec splits a data layer (tools, resources, prompts, discovery) from a transport layer (stdio or Streamable HTTP). A host such as VS Code creates a new client object for each server: filesystem, database, a remote service. Local stdio servers usually serve one client. Remote HTTP servers serve many. The server never calls a model. You can test it with no API key.

Starter

print("Host: Cursor, Claude, or your own app")
print("Client: one connection")
print("Server: your Python program, no model inside")

Example

“Weather in New York” cannot be answered from training. The client fetches the tool list, the model picks get_weather, and the server runs your function.

Anthropic introduced MCP in November 2024. The messages are JSON-RPC. Three roles:

RoleWho it isExample
HostThe AI app the person usesClaude Desktop, Cursor, your own chatbot
ClientThe connection the host opens to one serverOne client per server
ServerThe process that offers tools and dataA small Python program for your shop’s orders

Ask “what’s the weather in New York?”. The model cannot know today’s weather from training. The client fetches the server’s tool list, the model picks get_weather, and the call goes to the server. The server does not call a model. It runs your function and returns text. You can build and test the server with no API key.

A server for one shop

AnalogyA server is a menu. The docstring is the description a diner reads. A blank description means the diner guesses.
Data flow
Function + types→SDK builds schema→Host lists tools→Model calls one→You return text

FastMCP turns that function into a tool. The name, description, and JSON Schema come from the function and the type hints. Write the server once and attach it to Claude, Cursor, and your own agent. Return a short string. A 2 MB blob blows the model’s context. Log the rest on the server.

Starter

ORDERS = {"A17": "shipped on 3 March"}

def lookup_order(order_id: str) -> str:
    """Look up shipping status by order id, such as A17."""
    return ORDERS.get(order_id, "unknown order")

print(lookup_order("A17"))

Example

The same order server can sit in a support desktop app and in an engineer’s editor. The second app does not get a custom plugin.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("shop")
ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}

@mcp.tool()
def lookup_order(order_id: str) -> str:
    """Look up an order's shipping status by its id, such as A17."""
    status = ORDERS.get(order_id)
    return f"{order_id} {status}." if status else f"{order_id} is not an order we have."

if __name__ == "__main__":
    mcp.run()  # speaks MCP on stdin/stdout

The SDK builds the tool name, description, and JSON Schema from the function and its type hints. A docstring is not optional polish. It is what the model reads when it decides whether to call the tool.

Write once, connect everywhere

  • The same order server in Claude Desktop for support, and in Cursor for an engineer reproducing a bug.
  • A Postgres server the data team already runs, attached to a new internal agent without a custom plugin.
  • A GitHub or filesystem server so a coding agent reads issues through MCP instead of a one-off shell wrapper.

Tools, resources, and prompts

AnalogyTools are buttons the model presses. Resources are documents the app can attach. Prompts are saved scripts a person can run, like slash commands.
Data flow
tools: do something→resources: read something→prompts: start a task

Memory is not the server’s job. Calls are stateless. The host keeps the conversation. Do not hide a customer’s history in a global on the server and expect the next host to see it. Dangerous tools should require a person to approve them inside the host.

Starter

tools = ["lookup_order"]
resources = ["policy://returns"]
prompts = ["refund_reply"]
print({"tools": tools, "resources": resources, "prompts": prompts})

Example

A returns policy is a resource. “Draft a refund reply” is a prompt. lookup_order is a tool. create_refund is a tool that should ask first.

  • Tools are functions the model calls. lookup_order, create_refund.
  • Resources are data the app can read, like a file or a policy page, addressed by a URI. The model does not have to “call” them as actions; the host attaches them.
  • Prompts are templates a user can pick, the way slash commands work. “Draft a refund reply” with holes for the order id.
@mcp.resource("policy://returns")
def returns_policy() -> str:
    return "30 days, unused, with receipt."

@mcp.prompt()
def refund_reply(order_id: str) -> str:
    return f"Write a short refund update for order {order_id}. Use only tool results."
Memory is not the server’s job. MCP calls are stateless. The host keeps the conversation. Do not hide a customer’s history in a global variable on the server and expect the next host to see it.

stdio locally, HTTP when it is remote

Analogystdio is two people passing notes under a door on the same computer. HTTP is a phone line, so you need to know who is calling.
Data flow
Local: host starts a process→stdin/stdout JSON-RPC→Remote: Streamable HTTP→Auth

That config is what a desktop host uses for a local server. A remote server must authenticate, or a refund tool on the internet is an open refund button. Keep API keys in the server environment, not in the chat.

Starter

config = {
    "mcpServers": {
        "shop": {"command": "python", "args": ["server.py"]}
    }
}
print(config["mcpServers"]["shop"]["command"])

Example

An internal REST “get order” endpoint becomes one MCP tool. Every agent sees a name and a schema. The key stays on the server.

A local server is a process the host starts. Messages go over standard input and output. That is what mcp.run() does, and it is what a Claude or Cursor config points at:

{
  "mcpServers": {
    "shop": {
      "command": "python",
      "args": ["server.py"]
    }
  }
}

A remote server speaks Streamable HTTP so many people can use one deployment. Put auth on that endpoint. A tool that refunds an order, exposed on the internet with no identity check, is an open refund button.

Use case: an internal API

You already have REST for “get order”. Wrap it in one MCP tool instead of pasting the URL into every agent prompt. The model sees a name and a schema. The API key stays in the server’s environment, not in the chat.

Your own agent as the host

AnalogyYour own agent can be the host. It lists tools, asks a model which one to call, runs it, and stops when the model answers in text.
Data flow
List tools→Model picks→Call server→Tool result→Final answer

Test with an in-memory client first. You exercise the real protocol without a port and without a model. When A17 returns the shipped sentence, then attach a model. LangChain and the OpenAI Agents SDK both have adapters so you do not hand-roll JSON-RPC. Approval still belongs on refunds, deletes, and email.

Starter

tools = {"lookup_order": lambda order_id: "A17 shipped" if order_id == "A17" else "unknown"}
print(tools["lookup_order"]("A17"))

Example

A green test that lookup_order("A17") returns the ledger sentence is the whole server. The model is a later guest.

After the server works, a host can list tools and loop: send the question and the tool list to a model, run the tool the model picked, send the result back, stop when the model answers in text. LangChain and the OpenAI Agents SDK both have MCP adapters so you do not hand-roll JSON-RPC.

Test the server with an in-memory client first. You exercise the real protocol without a port and without a model. When lookup of A17 returns the shipped sentence, then attach a model.

Approval belongs on dangerous tools. Listing a tool is not the same as letting the model call it. Hosts can ask a person before create_refund. Do that for anything that spends money, deletes data, or sends mail.

When plain Python is enough

AnalogyIf only your own process will ever call the function, MCP is a second plug for a lamp that already has one. Use a normal function.
Data flow
One app→Plain function→Second app or team→MCP server

Adopt MCP when Claude, Cursor, and a bot you wrote all need the same tool. Keep results small. Put auth on HTTP. Do not store conversation memory on the server.

Starter

def lookup_order(order_id: str) -> str:
    return "shipped"

print("call lookup_order directly until a second host needs it")

Example

A helper used by one script stays a function. The order lookup your whole company will attach to editors becomes a server.

If only your own process will ever call the function, an MCP server is extra moving parts. Use a normal function. Adopt MCP when a second app, or a second team, needs the same tool.

Keep tool results small. Returning a 2 MB JSON blob blows the model’s context. Return the one order, or a short error. Log the rest on the server.

Practice

  • Add list_recent_orders() with a docstring that says it returns at most 5 ids.
  • Add a resource for the shipping policy and a prompt named where_is_my_order.
  • Decide which tool needs a human approval, and write the one-line reason.
  • Explain why the server file has no API key for the LLM.