Skip to content

Commit 4ff932c

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/fetch-readability-python-fallback
Re-express the readability fallback over main's SSRF-guard rework: allow_private_ips and use_readability_js now thread side by side through serve/fetch_url, the CLI gains --no-readability-js next to --allow-private-ips, and the characterization tests moved onto the in-process harness (MockTransport + FakeWeb) with the forwarding pinned through serve(use_readability_js=False).
2 parents 5c3d999 + bbb701e commit 4ff932c

295 files changed

Lines changed: 63915 additions & 9384 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/README.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Changesets
2+
3+
The four TypeScript servers (`everything`, `filesystem`, `memory`,
4+
`sequentialthinking`) are versioned with
5+
[changesets](https://github.com/changesets/changesets). This directory holds the
6+
pending ones: one small Markdown file per change, each naming the packages it
7+
bumps, the bump type, and the line that becomes the CHANGELOG entry. The whole
8+
release flow is in [`RELEASING.md`](../RELEASING.md).
9+
10+
A PR that changes what a TypeScript server publishes adds one:
11+
12+
```sh
13+
npm run changeset
14+
```
15+
16+
Pick the affected package or packages, choose the bump type, and write a
17+
one-line summary for a user of the server. Commit the generated file with the
18+
PR.
19+
20+
| Bump | When |
21+
| --- | --- |
22+
| **patch** | A bug fix |
23+
| **minor** | A new tool, prompt, resource or option |
24+
| **major** | A breaking change: a tool removed or renamed, a schema change that breaks clients, a protocol or Node floor bump |
25+
26+
A change that does not alter a published TypeScript package needs none: docs,
27+
skills, workflows, the gate's scripts, tests, and anything in a Python server.
28+
The Python servers (`fetch`, `git`, `time`) use CalVer and are stamped by the
29+
**Prepare Python Release** workflow instead.
30+
31+
Merged changesets accumulate on `v2/main`. The **Version Packages** workflow
32+
keeps one rolling PR open that consumes them: merging it applies the version
33+
bumps, writes each package's `CHANGELOG.md`, and deletes the consumed files.
34+
Nothing is published until a maintainer publishes a GitHub Release.

‎.changeset/config.json‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
3+
"changelog": [
4+
"@changesets/changelog-github",
5+
{ "repo": "modelcontextprotocol/servers" }
6+
],
7+
"commit": false,
8+
"fixed": [],
9+
"linked": [],
10+
"access": "public",
11+
"baseBranch": "v2/main",
12+
"updateInternalDependencies": "patch",
13+
"ignore": []
14+
}

‎.claude/skills/board-ops/SKILL.md‎

Lines changed: 461 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[
2+
{
3+
"prompt": "Move the board card for issue 4810 to In Review.",
4+
"expect": "board-ops"
5+
},
6+
{
7+
"prompt": "How do I set the Priority field on an issue's card on the Servers V2 board?",
8+
"expect": "board-ops"
9+
},
10+
{
11+
"prompt": "The project board rejected my Status option ID. How do I get the current one?",
12+
"expect": "board-ops"
13+
},
14+
{
15+
"prompt": "I'm about to add an option to one of the board's single-select fields. Should I do anything first?",
16+
"expect": "board-ops"
17+
},
18+
{
19+
"prompt": "Several cards lost their Status value after someone edited the field. How do I put it back?",
20+
"expect": "board-ops"
21+
},
22+
{
23+
"prompt": "Write out every command I need to run to file an issue, start to finish, for: the time server returns the wrong UTC offset for Australia/Lord_Howe during daylight saving. I'll run them myself.",
24+
"chain": ["issue-create", "board-ops"]
25+
},
26+
{
27+
"prompt": "My fix for issue 4810 is committed and the gate is green. Write out every command I need to run from here until the PR is open and tracked, start to finish. I'll run them myself.",
28+
"chain": ["pr-flow", "board-ops"]
29+
},
30+
{
31+
"prompt": "Explain the difference between TCP and UDP.",
32+
"expect": null
33+
},
34+
{
35+
"prompt": "What year was the first Moon landing?",
36+
"expect": null
37+
}
38+
]
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
---
2+
name: client-smoke
3+
description: "Drive a locally built server with a real client: the Inspector CLI (scripted), the Inspector web UI (by hand) and an LLM client. Use when smoke-testing a server change; when calling a tool, reading a resource or getting a prompt from the command line; when choosing a protocol era; or when the Inspector CLI will not connect to, or misreads, a server command."
4+
disable-model-invocation: false
5+
---
6+
7+
# Client smoke
8+
9+
A server change is checked against real clients before its PR opens: the MCP
10+
Inspector and an LLM client. This skill is how to run each one against a server
11+
built from this checkout, and what the output means. What the PR must then
12+
record is in `/pr-flow` (client evidence); building and starting the server is
13+
`/local-dev`; automated tests are `/testing`.
14+
15+
Three clients, in the order to reach for them:
16+
17+
| Client | Driven by | Use it for |
18+
| --- | --- | --- |
19+
| Inspector **CLI** | A command. This is the **scripted path**, and the one an agent runs | One request, one JSON result: the evidence that goes in a PR |
20+
| Inspector **web** UI | A person, by hand. Nothing in this repo automates a browser | What only a UI shows: notifications arriving, elicitation and sampling prompts, a long-running tool's progress |
21+
| An **LLM client** | A prompt | Whether a model picks the tool and uses its result, which the PR checklist asks for |
22+
23+
Build first (`npm run build -w src/<server>`). Every command below runs the
24+
last build of a TypeScript server, not your edit.
25+
26+
## The Inspector CLI
27+
28+
```sh
29+
npx -y @modelcontextprotocol/inspector --cli <server command> [-- ] <inspector options>
30+
```
31+
32+
It starts the server command over stdio (or connects to a URL), sends one
33+
request, prints the result and exits. Pin the Inspector version in anything you
34+
keep (`@modelcontextprotocol/inspector@<version>`); the commands here were run
35+
on 2.9.0. `--cli --help` is the reference for the options.
36+
37+
```sh
38+
INSPECT="npx -y @modelcontextprotocol/inspector --cli"
39+
40+
# List, then call, on a TypeScript server
41+
$INSPECT node src/everything/dist/index.js stdio --method tools/list --format json
42+
$INSPECT node src/everything/dist/index.js stdio \
43+
--method tools/call --tool-name echo --tool-arg message=hello --format json
44+
# → {"result":{"content":[{"type":"text","text":"Echo: hello"}]}}
45+
46+
# Arguments that are not strings: pass them as JSON
47+
$INSPECT node src/everything/dist/index.js stdio \
48+
--method tools/call --tool-name get-sum --tool-args-json '{"a":2,"b":3}' --format json
49+
50+
# Resources and prompts
51+
$INSPECT node src/everything/dist/index.js stdio \
52+
--method resources/read --uri demo://resource/dynamic/text/1 --format json
53+
$INSPECT node src/everything/dist/index.js stdio \
54+
--method prompts/get --prompt-name args-prompt --prompt-args city=Paris --format json
55+
56+
# A server that takes a positional argument
57+
$INSPECT node src/filesystem/dist/index.js /abs/dir \
58+
--method tools/call --tool-name list_allowed_directories --format json
59+
60+
# A Python server: note the `--` (see below)
61+
$INSPECT uv --directory src/time run mcp-server-time -- \
62+
--method tools/call --tool-name get_current_time --tool-arg timezone=UTC --format json
63+
$INSPECT uv --directory src/git run mcp-server-git --repository "$PWD" -- \
64+
--method tools/list --format json
65+
```
66+
67+
`--format json` prints a result as one JSON object, `{"result": …}`, on
68+
**stdout**, and a failure as `{"error": …}` on **stderr**. The server's own
69+
stderr (its startup banner) passes through on stderr too. So capture stdout
70+
alone for the result, never `2>&1` into a JSON parser: a tool error produces
71+
both objects, one on each stream (see the table below).
72+
73+
### How the command line is split
74+
75+
The server command comes **first** and the Inspector's options **after** it.
76+
With no `--`, the server command ends at the first word that starts with `-`.
77+
That rule produces the three failures people actually hit:
78+
79+
- **A server command with its own flags needs `--` before the Inspector's
80+
options.** `uv --directory …` and `mcp-server-git --repository …` both have
81+
one. Without the separator the command is cut at `--directory`, and what runs
82+
is a bare `uv`, which prints its help and exits. With it, everything before
83+
`--` is the server command and everything after is the Inspector's.
84+
- **Putting an Inspector option before the server command loses the command.**
85+
`--cli -e KEY=VALUE node …` leaves no server command at all, and the CLI
86+
falls back to your personal catalog (`~/.mcp-inspector/mcp.json`). With
87+
several servers in it that fails with `Multiple servers found in config
88+
file. Please specify one with --server`. Either way it is no longer talking
89+
to your build, so move the option after the command.
90+
- **`Method is required`** means the options ended up on the wrong side of a
91+
`--`.
92+
93+
### Environment variables
94+
95+
The spawned server gets only the SDK's default safelist from your shell
96+
(`PATH`, `HOME`, `USER` and the like), not the variables you set. Setting
97+
`MEMORY_FILE_PATH=… npx …` has no effect on the server; pass it with `-e`:
98+
99+
```sh
100+
$INSPECT node src/memory/dist/index.js \
101+
-e MEMORY_FILE_PATH=/abs/tmp/graph.jsonl \
102+
--method tools/call --tool-name read_graph --format json
103+
```
104+
105+
### Exit codes and errors
106+
107+
The exit code is the verdict, so check it rather than reading the text:
108+
109+
| Outcome | stdout | stderr | Exit |
110+
| --- | --- | --- | --- |
111+
| The request succeeded | `{"result": …}` | | 0 |
112+
| The tool ran and returned `isError: true` | `{"result":{"content": …, "isError": true}}` | `{"error":{"code":"tool_is_error", …}}` | 5 |
113+
| No such tool | | `{"error":{"code":"tool_not_found", …}}` | 5 |
114+
| An HTTP server could not be reached (refused, DNS, timeout) | | `{"error":{"code":"unreachable", …}}` | 4 |
115+
| Bad arguments, a failed version negotiation, or a stdio server that exited | | `{"error":{"code":"error", …}}` | 1 |
116+
117+
These are the codes observed on 2.9.0 for the cases this repo's servers
118+
produce; the CLI defines others (schema and skill checks, authentication)
119+
that its `--help` describes.
120+
121+
A tool error is a **successful protocol exchange**: the result object is still
122+
printed on stdout, and it is what to quote when the change is about an error a
123+
tool returns.
124+
125+
### `everything` over HTTP
126+
127+
Start the transport in one terminal (or in the background), then give the CLI
128+
the URL. The transport is inferred from the path: `/mcp` is Streamable HTTP,
129+
`/sse` is HTTP+SSE.
130+
131+
```sh
132+
(
133+
URL=http://localhost:3917/mcp
134+
# Refuse a port something already answers on: it would be tested instead of this build.
135+
curl -s -m 2 -o /dev/null "$URL" && { echo "port 3917 is already in use" >&2; exit 1; }
136+
PORT=3917 node src/everything/dist/index.js streamableHttp >/dev/null 2>&1 &
137+
SERVER_PID=$!
138+
trap 'kill "$SERVER_PID" 2>/dev/null' EXIT # stops it however the subshell ends
139+
READY=
140+
for _ in $(seq 50); do # at most ten seconds
141+
kill -0 "$SERVER_PID" 2>/dev/null || { echo "server exited before listening" >&2; exit 1; }
142+
curl -s -m 2 -o /dev/null "$URL" && { READY=1; break; }
143+
sleep 0.2
144+
done
145+
[ -n "$READY" ] || { echo "server did not listen within ten seconds" >&2; exit 1; }
146+
$INSPECT "$URL" --method tools/call --tool-name echo --tool-arg message=over-http --format json
147+
)
148+
```
149+
150+
Each part of that is there for a failure it prevents. The first check refuses
151+
a port that already answers, because a stale listener there would be the
152+
server under test. Without the wait, the CLI can reach the port before the
153+
server is listening and exit 4. The wait is bounded and checks that this
154+
server is still alive, so one that fails to bind (the build is missing, the
155+
port was taken in between) ends the run instead of hanging it. The subshell's `EXIT` trap stops the server by pid even when the CLI call
156+
fails; a listener left on the port makes the next run talk to the old build.
157+
By hand in two terminals, the equivalent is waiting for the `listening on
158+
port` line and stopping the server with Ctrl-C.
159+
160+
A change to a tool, resource or prompt of `everything` is checked over stdio
161+
and Streamable HTTP. Check HTTP+SSE as well when the change touches transport
162+
or session code; it is deprecated but must keep working.
163+
164+
## Protocol eras
165+
166+
The Inspector connects in one of three eras, chosen with `--protocol-era` (the
167+
CLI) or **Server Settings → Protocol Era** (the web UI):
168+
169+
| Era | What the Inspector does |
170+
| --- | --- |
171+
| `legacy` (the default) | A plain `initialize`. Negotiates 2025-11-25 or older |
172+
| `modern` | Pins 2026-07-28 through `server/discover`, with no fallback |
173+
| `auto` | Probes `server/discover`, and falls back to `initialize` |
174+
175+
⚠️ **Only the legacy era can be exercised against these servers today.** All
176+
seven are on a v1 SDK (`@modelcontextprotocol/sdk` 1.x, `mcp` 1.x), whose
177+
newest protocol version is 2025-11-25. None implements `server/discover`. So:
178+
179+
- `--protocol-era modern` fails, by design, with `Version negotiation failed:
180+
the server did not offer pinned protocol version 2026-07-28 via
181+
server/discover (no fallback in pin mode)` and exit 1. That is the expected
182+
result for an unmigrated server, not a regression in your change.
183+
- `--protocol-era auto` succeeds, but through the fallback, so it is a legacy
184+
connection and proves nothing about 2026-07-28.
185+
186+
The both-eras check that `/pr-flow` asks for (#4857) therefore has one half
187+
that can pass today. For a server that does not yet serve the modern era,
188+
record the legacy run, and say in the PR that the server does not yet speak
189+
2026-07-28 rather than leaving the modern era unmentioned. Moving a server to
190+
a v2 SDK (#4856 for TypeScript, #4851 for Python) does not change that by
191+
itself: those keep the legacy wire behavior. Modern-era support arrives with
192+
#4852 (TypeScript) and #4853 (Python). Once a server has it, run every command
193+
twice, with `--protocol-era legacy` and `--protocol-era modern`, and expect
194+
both to succeed. Do not use `auto` for evidence in either case: it
195+
does not say which era answered.
196+
197+
## The Inspector web UI
198+
199+
The web client is the hand-driven path. It needs a person at a browser; this
200+
repo configures no browser automation, so an agent prepares the command and
201+
the steps and asks for what was observed.
202+
203+
```sh
204+
npx -y @modelcontextprotocol/inspector node src/everything/dist/index.js stdio
205+
npx -y @modelcontextprotocol/inspector -- uv --directory src/time run mcp-server-time
206+
npx -y @modelcontextprotocol/inspector --transport http --server-url http://localhost:3001/mcp
207+
```
208+
209+
⚠️ **The web launcher reads `--` the other way round from the CLI.** Here the
210+
Inspector's own options come **before** `--` and the server command **after**
211+
it; in `--cli` mode the server command is before `--` and the options after.
212+
A line copied from one mode to the other starts the wrong thing. Pass a
213+
server's environment with `-e KEY=VALUE` before the `--`.
214+
215+
Use the web UI for what a single request cannot show: `toggle-simulated-logging` and
216+
`toggle-subscriber-updates` (notifications over time),
217+
`trigger-elicitation-request` and `trigger-sampling-request` (the client is
218+
asked something), `trigger-long-running-operation` (progress), and a
219+
filesystem server's response to a Roots change.
220+
221+
Evidence from the web UI is what was clicked and what appeared, in words. This
222+
repo does not attach screenshots.
223+
224+
## An LLM client
225+
226+
The PR checklist asks whether the change was tested with an LLM client. Any
227+
client that can launch a stdio server works; each server's `README.md` has the
228+
config blocks for Claude Desktop and VS Code, and `/local-dev` shows how to
229+
point one at the local build.
230+
231+
Claude Code can do it in one headless command, which makes it the scriptable
232+
option. Write a config that names the local build with an **absolute** path:
233+
234+
```json
235+
{
236+
"mcpServers": {
237+
"everything": {
238+
"command": "node",
239+
"args": ["/abs/path/to/servers/src/everything/dist/index.js", "stdio"]
240+
}
241+
}
242+
}
243+
```
244+
245+
```sh
246+
printf '%s' "Call the echo tool of the everything server with the message 'llm-smoke' and reply with exactly the text it returned." \
247+
| claude -p --mcp-config /abs/path/to/config.json --strict-mcp-config \
248+
--allowedTools "mcp__everything__echo" --max-turns 3
249+
# → Echo: llm-smoke
250+
```
251+
252+
`--strict-mcp-config` makes the file the only source of servers, so the answer
253+
cannot have come from a published copy configured elsewhere. A tool is
254+
addressed as `mcp__<server name in the config>__<tool name>`, and only the
255+
tools listed in `--allowedTools` can be called without a prompt. Keep the
256+
config file outside the worktree.
257+
258+
Claude Code, run this way, has no era switch and does not print the protocol
259+
version it negotiated. `/pr-flow` asks for the LLM client in both eras as well;
260+
where the client gives no way to choose, record the client and its version and
261+
say that the era could not be selected, rather than labelling the run with an
262+
era it was not shown to use. Against a v1-SDK server the connection is legacy
263+
whatever the client prefers, for the reason given under Protocol eras.
264+
265+
## What to write down
266+
267+
For each client: the command or the prompt, and the result trimmed to what
268+
shows the change. For a bug fix, run the same CLI command against `v2/main`
269+
and against the branch, and keep both outputs. Where the evidence goes in the
270+
PR, and what a change with no client-visible surface records instead, is
271+
`/pr-flow`.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[
2+
{
3+
"prompt": "Smoke-test my change to the time server with the Inspector CLI.",
4+
"expect": "client-smoke"
5+
},
6+
{
7+
"prompt": "How do I call a tool on my locally built server from the command line and get JSON back?",
8+
"expect": "client-smoke"
9+
},
10+
{
11+
"prompt": "The Inspector CLI says 'Multiple servers found in config file' when I pass it my server command. How do I fix it?",
12+
"expect": "client-smoke"
13+
},
14+
{
15+
"prompt": "How do I check my server change against a 2026-07-28 client?",
16+
"expect": "client-smoke"
17+
},
18+
{
19+
"prompt": "How do I get Claude Code to call a tool on my local build of the everything server in one headless command?",
20+
"expect": "client-smoke"
21+
},
22+
{
23+
"prompt": "How do I open the Inspector web UI against my local build of the git server?",
24+
"expect": "client-smoke"
25+
},
26+
{
27+
"prompt": "Summarize the plot of Hamlet in two sentences.",
28+
"expect": null
29+
},
30+
{
31+
"prompt": "Convert 5 kilometers to miles.",
32+
"expect": null
33+
}
34+
]

0 commit comments

Comments
 (0)