MCP (Model Context Protocol)
Follow messages one by one with a small and a written by hand, without an , then connect it to and see where the setting is saved.
What MCP is
is an for connecting apps to outside and . Before MCP, every AI app needed its own custom connection for every tool. With MCP you build a once, and any app that speaks MCP — , , or your own — can use it.
MCP was created by , the company that makes . Official documentation: https://claude.com/docs/connectors/building/mcp
MCP client and MCP server
MCP has two sides.
- MCP = the : the side that picks and uses tools. AI agents such as Claude Code and Codex act as MCP clients and connect to servers.
- = the side that offers its own features: a program that makes what it can do available to agents through MCP.
- Host: the name the uses for the AI app the agent lives in. Inside that app, each connected server gets its own MCP client connection.
Example: WebMCP
WebMCP is a way for a website to offer itself the way an MCP server does. It is an provided by the web that lets a website offer its own features as tools that AI agents can use. When the site's developer adds registerTool() to the site's and registers features such as "search products" or "add to cart", the agent calls those tools directly instead of looking at the screen and clicking buttons one by one.
- Agent (MCP client): the AI asked to "find an umbrella in this shop and put it in the cart".
- Website (in the MCP server role): offers its features as tools, runs them when the agent calls, and returns the results. The web browser is the counter that connects the two.
In short, becoming an MCP server means "making myself usable by agents". The Using MCP tab shows how this differs from other browser MCPs.
The three things an MCP server offers
A server can offer three kinds of things:
- Tools: functions the AI may call, such as "search the notes" or "add a row".
- : data the app can read, such as a file or a record, each at an address ().
- : ready-made message a user can pick.
Understand it the easy way
- Think of it like this
- MCP is like the USB of AI apps. Before USB, every printer and keyboard needed its own plug; now any device with a USB plug fits any computer. An MCP server is the device, the AI app is the computer, and the shared plug means you build a server once and every MCP app can use it.
- Example
- The strange-add server offers one tool: it adds two numbers plus a random number from 1 to 100.
- You connect it to Claude Code with one .
- You say "Use strange-add to add 2 and 3": the AI reads the tool description, asks for your approval, and the client calls strange-add on the server. The answer is not 5 — proof that the tool really ran.
- Common mistake
- Installing any MCP server you find. A server runs code on your computer with your . Read it or choose a trusted one, and give it only the folder or key it needs.
Connecting an MCP server to your agent
An is only useful once it is connected to an . The connect commands ( · codex add), scopes and checks differ by , so each tool page has them once in its MCP section.
WebMCP vs Chrome DevTools MCP vs Playwright MCP
All three let an agent work in a web . What differs is who sets it up and how the agent works. WebMCP is a browser the website uses to declare "these features can be used on my site"; the other two are MCP servers you install on the agent side that drive the browser directly on any site.
| Item | WebMCP | Chrome DevTools MCP | Playwright MCP |
|---|---|---|---|
| What it is | A browser API used by websites — a web standard still at the proposal stage (a Web Community Group draft) | An MCP server installed on the agent side (Google team) | An MCP server installed on the agent side (Microsoft) |
| Who sets it up | The website developer — adds registerTool() to the site's to register tools | The person using the agent — adds the to their agent | The person using the agent — adds the server to their agent |
| How the agent works | Calls the tools the site registered, by name. No need to read the screen and guess. | Drives and inspects a real Chrome through the — clicks, typing, . | Reads the page's accessibility tree (its structure) and clicks and types — it looks at structure, not . |
| Good at | Uses that site's features accurately and fast, in my own signed-in | — performance traces, requests, | and — several browsers: , , WebKit |
| Limits | Works only where the site has registered tools. As a new standard, few browsers and sites support it yet. | Built for Chrome (and Chrome for Testing). When the screen changes, it has to find things again. | When the page structure changes, it has to find things again. It works button by button, not by features the site defined. |
Code to start with
// WebMCP: the website registers its own feature as a tool
// (first explainer / Chrome preview: navigator.modelContext,
// spec draft 2026-10-27: document.modelContext)
const modelContext = document.modelContext ?? navigator.modelContext;
modelContext.registerTool({
name: "search-products",
description: "Search this shop's products by keyword",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
async execute({ query }) {
const response = await fetch("/api/products?q=" + encodeURIComponent(query));
return await response.json(); // the agent gets this result
},
});
# Chrome DevTools MCP: add the server to your agent (Claude Code)
claude mcp add chrome-devtools -- npx -y chrome-devtools-mcp@latest
# Playwright MCP: add the server to your agent (Claude Code)
claude mcp add playwright -- npx @playwright/mcp@latest
WebMCP goes in the site's ; the other two go in your computer's . WebMCP's first explainer and the Chrome preview use navigator.modelContext; the 2026-10-27 draft uses document.modelContext — the spec is still changing.
As an analogy — a shop that hangs out a menu, and an errand robot
WebMCP = the order counter and menu a shop hangs out
- The shop (the website) hangs a menu by the door that says "Please order at this counter".
- The menu lists what you can order (tool names) and how to fill in the order form (the input ).
- The robot (the agent) just reads the menu and hands an order form to the counter — it does not walk around the shop.
- But it can order this way only at shops that have built a counter.
Chrome DevTools MCP · Playwright MCP = an errand robot sent by the customer
- The customer (the person using the agent) sends an errand robot into the shop. The shop does not have to prepare anything.
- The robot walks around the shop itself, reads the shelves, picks up items and presses the checkout buttons.
- It can enter any shop, but when the shelves are rearranged it has to search again, so it can be slow and get lost.
- The Chrome DevTools MCP robot is an who also checks the shop's wiring and meters (performance, network, console).
- The Playwright MCP robot is a shopping robot that repeats a set shopping route exactly the same in several shops (browsers).
In short: at a shop with a counter, ordering at the counter is fast and accurate. At a shop without one, the robot has to look around itself. So the three do not replace each other — they are used together.
MCP safety rules
Safety: a tool is code that runs with your , and tool descriptions are untrusted unless the server is trusted. The specification asks hosts to get the user's consent before calling a tool. Install only servers you have read or trust, give a server the smallest access it needs, keep keys in environment variables rather than in code, and watch for — a web page or file that a tool reads may contain instructions aimed at the .
Messages and transports
The messages are 2.0 — small objects such as ("what can you do?") and ("do this, with these arguments"). They travel over one of two standard : stdio (the starts the as a on the same computer and they talk through standard ) or (the server runs at a web address, so many users can reach it).
A hand-made server and client
The server (mcp-server.py) uses only the , so you can see the itself. It reads one JSON-RPC message per line from standard input and writes one reply per line to standard output:
- : the client says hello with a . The server answers with the same , says that it has and gives its name.
- tools/list: the server returns its one tool, strange-add, with a description and an input (a and b are numbers).
- tools/call: the server runs strange-add and returns the result as text.
- A message without an id is a (such as notifications/initialized), so the server sends no reply. Any other request (such as ping) gets an empty result.
strange-add adds a random number from 1 to 100 to a + b. The cannot work that out by itself, so an answer that is not 5 for 2 + 3 proves that the tool really ran. stdout carries the protocol messages only — one stray () breaks the conversation, so write to stderr. print(..., =True) sends each reply at once.
The client (mcp-client.sh) is a script that plays the host's part without any . It starts the server, sends the whole (initialize, the initialized notification, tools/list, tools/call, ping) and checks each reply, printing PASS or FAIL. Exit 0 means that everything passed. Test a server like this before you connect it to an . In a real client the model joins the loop:
- send the user's question and the tool list to the model;
- when the model asks for a tool, the client calls it on the server with tools/call;
- send the result back, and the model writes the final answer.
This hand-made server speaks the style that starts with initialize (protocol version 2025-06-18), which clients such as still accept. The newest (2026-07-28) carries the version in every request instead of an initialize . For real services, use the official (Python MCPServer, ), which follow the specification for you.
"""
mcp-server.py: the smallest MCP server.
One tool, plain Python (standard library only), no SDK.
Who is who
MCP server This program. It offers tools (functions the AI
may use).
Host The app the user talks to (Claude Code, Codex).
It runs one MCP client for each connected server.
MCP client The part of the host that talks to this server
with JSON-RPC messages (here over stdio: one JSON
message per line).
Model The LLM (Claude Opus, GPT, ...). It never talks to
the server directly. It only talks to the host.
Flow
1. Discover tools
The client asks for the tools (tools/list). The server replies
with each tool's name, description and input schema.
2. Offer tools
The host sends the user's message to the model, together
with the tool list.
3. Model decides
The model answers directly, or asks for a tool call
(tool name + arguments). It never runs the tool itself.
4. Call the tool
The client sends the request to the server (tools/call).
5. Run the tool
The server runs the function and returns the result.
6. Model continues
The host gives the result back to the model. The model may
call more tools (3-6 again) or write the final answer.
Model Host (MCP client) MCP server
| | 1. tools/list ------> |
| | <------ tool list |
| <-- 2. message + tool list |
| 3. tool call --> | |
| | 4. tools/call ------> |
| | <------ 5. result |
| <-- 6. result | |
Rule for stdio servers: stdout carries protocol messages only.
Never print() anything else there. Write logs to stderr.
"""
import json
import random
import sys
SERVER_INFO = {"name": "strange-add-mcp", "version": "1.0.0"}
# The tool. The model reads the description to decide when to use it.
STRANGE_ADD = {
"name": "strange-add",
"description": "Add two numbers with a strange number.",
"inputSchema": {
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
},
"required": ["a", "b"],
},
}
def strange_add(a, b):
"""Add a + b and a random number from 1 to 100 (always > a + b).
The model cannot guess the answer, so a right answer proves
that the tool was really called.
"""
return a + b + random.randint(1, 100)
def handle(method, params):
"""Return the result for one request. Others (like ping) get {}."""
if method == "initialize":
# Use the client's protocol version and say "I have tools".
return {
"protocolVersion": params["protocolVersion"],
"capabilities": {"tools": {}},
"serverInfo": SERVER_INFO,
}
if method == "tools/list":
return {"tools": [STRANGE_ADD]}
if method == "tools/call":
args = params["arguments"]
total = strange_add(args["a"], args["b"])
return {"content": [{"type": "text", "text": str(total)}]}
return {}
def main():
# One JSON-RPC message per line comes in, one reply per line goes out.
for line in sys.stdin:
request = json.loads(line)
# No id means a notification: send no reply.
if "id" not in request:
continue
result = handle(request["method"], request.get("params", {}))
reply = {"jsonrpc": "2.0", "id": request["id"], "result": result}
# flush=True sends the reply at once.
print(json.dumps(reply), flush=True)
if __name__ == "__main__":
main()
#!/bin/sh
# mcp-client.sh: the simplest MCP client, a shell script.
# It starts mcp-server.py, talks to it over stdio (one JSON-RPC
# message per line) and checks every reply.
# Exit code 0 means every check passed.
SERVER="python3 $(dirname "$0")/mcp-server.py"
# 1. Send the whole session at once and keep the replies.
# id 1 initialize agree on a version
# (no id) notifications/initialized "ready" (no reply)
# id 2 tools/list which tools are there?
# id 3 tools/call run strange-add(2, 3)
# id 4 ping is the server alive?
replies=$(
$SERVER <<'EOF'
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "shell-mcp-client", "version": "1.0.0"}}}
{"jsonrpc": "2.0", "method": "notifications/initialized"}
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "strange-add", "arguments": {"a": 2, "b": 3}}}
{"jsonrpc": "2.0", "id": 4, "method": "ping"}
EOF
)
echo "--- Server replies ---"
echo "$replies"
echo "--- Checks ---"
failed=0
# 2. check <label> <reply id> <text the reply must contain>
check() {
reply=$(echo "$replies" | grep -F "\"id\": $2,")
if echo "$reply" | grep -qF "$3"; then
echo "PASS $1"
else
echo "FAIL $1 (expected: $3)"
failed=1
fi
}
check "initialize: protocol version" 1 '"protocolVersion": "2025-06-18"'
check "initialize: server info" 1 '"serverInfo": {'
check "initialize: has tools" 1 '"capabilities": {"tools": {}}'
check "tools/list: strange-add" 2 '"name": "strange-add"'
check "ping: empty result" 4 '"result": {}'
# 3. strange-add adds a random 0 to 100,
# so the answer only has to be >= 2 + 3.
total=$(echo "$replies" | grep -F '"id": 3,' |
sed 's/.*"text": "\([0-9]*\)".*/\1/')
if [ "$total" -ge 5 ] 2>/dev/null; then
echo "PASS tools/call: strange-add(2, 3) >= 5 (got $total)"
else
echo "FAIL tools/call: strange-add(2, 3) >= 5 (got $total)"
failed=1
fi
exit $failed
# put the two files above into one folder
mkdir -p ~/tmp/mcp && cd ~/tmp/mcp
# mcp-server.py mcp-client.sh
# 1) test the server without any AI
sh mcp-client.sh # -> 6 x PASS, exit code 0
# 2) connect it to Claude Code (no --scope: the "local" scope)
claude mcp add strange-add -- python3 ~/tmp/mcp/mcp-server.py
claude mcp list # strange-add: ... - Connected
claude mcp get strange-add # Scope: Local config
# 3) where it was saved: NOT in this folder (no new file here)
# ~/.claude.json -> projects -> "<this folder>" -> mcpServers
jq '.projects["'"$PWD"'"].mcpServers' ~/.claude.json
# 4) use it: start Claude Code in THIS folder
claude # "Use strange-add to add 2 and 3"
# -> not 5 (e.g. 61): the tool ran
claude mcp remove strange-add -s local
These run in a on your own computer or server, not on this page. Read each line before you run it.