Skip to main content

client-smoke

Drive a locally built server with a real client: the Inspector CLI (scripted), the Inspector web UI (through Playwright) 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.

Source facts

Repository
modelcontextprotocol/servers
Last source activity
October 27, 2026 at 03:31
Detected SKILL.md language
English
Stars
91,652
Forks
11,880

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
client-smoke
description
Drive a locally built server with a real client: the Inspector CLI (scripted), the Inspector web UI (through Playwright) 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.
disable-model-invocation
false
# Client smoke A server change is checked against real clients before its PR opens: the MCP Inspector and an LLM client. This skill is how to run each one against a server built from this checkout, and what the output means. What the PR must then record is in `/pr-flow` (client evidence); building and starting the server is `/local-dev`; automated tests are `/testing`. Three clients, in the order to reach for them: | Client | Driven by | Use it for | | -------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | 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 | | Inspector **web** UI | A Playwright script in headless Chromium, which an agent writes and runs | What the CLI cannot do: elicitation and sampling round-trips, URL elicitation, tasks, notifications arriving, a long-running tool's progress | | An **LLM client** | A prompt | Whether a model picks the tool and uses its result, which the PR checklist asks for | Build first (`npm run build -w src/<server>`). Every command below runs the last build of a TypeScript server, not your edit. ## The Inspector CLI ```sh npx -y @modelcontextprotocol/inspector --cli <server command> [-- ] <inspector options> ``` It starts the server command over stdio (or connects to a URL), sends one request, prints the result and exits. Pin the Inspector version in anything you keep (`@modelcontextprotocol/inspector@<version>`); the commands here were run on 2.9.0. `--cli --help` is the reference for the options. ```sh INSPECT="npx -y @modelcontextprotocol/inspector --cli" # List, then call, on a TypeScript server $INSPECT node src/everything/dist/index.js stdio --method tools/list --format json $INSPECT node src/everything/dist/index.js stdio \ --method tools/call --tool-name echo --tool-arg message=hello --format json # → {"result":{"content":[{"type":"text","text":"Echo: hello"}]}} # Arguments that are not strings: pass them as JSON $INSPECT node src/everything/dist/index.js stdio \ --method tools/call --tool-name get-sum --tool-args-json '{"a":2,"b":3}' --format json # Resources and prompts $INSPECT node src/everything/dist/index.js stdio \ --method resources/read --uri demo://resource/dynamic/text/1 --format json $INSPECT node src/everything/dist/index.js stdio \ --method prompts/get --prompt-name args-prompt --prompt-args city=Paris --format json # A server that takes a positional argument $INSPECT node src/filesystem/dist/index.js /abs/dir \ --method tools/call --tool-name list_allowed_directories --format json # A Python server: note the `--` (see below) $INSPECT uv --directory src/time run mcp-server-time -- \ --method tools/call --tool-name get_current_time --tool-arg timezone=UTC --format json $INSPECT uv --directory src/git run mcp-server-git --repository "$PWD" -- \ --method tools/list --format json ``` `--format json` prints a result as one JSON object, `{"result": …}`, on **stdout**, and a failure as `{"error": …}` on **stderr**. The server's own stderr (its startup banner) passes through on stderr too. So capture stdout alone for the result, never `2>&1` into a JSON parser: a tool error produces both objects, one on each stream (see the table below). ### How the command line is split The server command comes **first** and the Inspector's options **after** it. With no `--`, the server command ends at the first word that starts with `-`. That rule produces the three failures people actually hit: - **A server command with its own flags needs `--` before the Inspector's options.** `uv --directory …` and `mcp-server-git --repository …` both have one. Without the separator the command is cut at `--directory`, and what runs is a bare `uv`, which prints its help and exits. With it, everything before `--` is the server command and everything after is the Inspector's. - **Putting an Inspector option before the server command loses the command.** `--cli -e KEY=VALUE node …` leaves no server command at all, and the CLI falls back to your personal catalog (`~/.mcp-inspector/mcp.json`). With several servers in it that fails with `Multiple servers found in config file. Please specify one with --server`. Either way it is no longer talking to your build, so move the option after the command. - **`Method is required`** means the options ended up on the wrong side of a `--`. ### Environment variables The spawned server gets only the SDK's default safelist from your shell (`PATH`, `HOME`, `USER` and the like), not the variables you set. Setting `MEMORY_FILE_PATH=… npx …` has no effect on the server; pass it with `-e`: ```sh $INSPECT node src/memory/dist/index.js \ -e MEMORY_FILE_PATH=/abs/tmp/graph.jsonl \ --method tools/call --tool-name read_graph --format json ``` ### Exit codes and errors The exit code is the verdict, so check it rather than reading the text: | Outcome | stdout | stderr | Exit | | -------------------------------------------------------------------------- | -------------------------------------------- | ---------------------------------------- | ---- | | The request succeeded | `{"result": …}` | | 0 | | The tool ran and returned `isError: true` | `{"result":{"content": …, "isError": true}}` | `{"error":{"code":"tool_is_error", …}}` | 5 | | No such tool | | `{"error":{"code":"tool_not_found", …}}` | 5 | | An HTTP server could not be reached (refused, DNS, timeout) | | `{"error":{"code":"unreachable", …}}` | 4 | | Bad arguments, a failed version negotiation, or a stdio server that exited | | `{"error":{"code":"error", …}}` | 1 | These are the codes observed on 2.9.0 for the cases this repo's servers produce; the CLI defines others (schema and skill checks, authentication) that its `--help` describes. A tool error is a **successful protocol exchange**: the result object is still printed on stdout, and it is what to quote when the change is about an error a tool returns. ### `everything` over HTTP Start the transport in one terminal (or in the background), then give the CLI the URL. The transport is inferred from the path: `/mcp` is Streamable HTTP, `/sse` is HTTP+SSE. ```sh ( URL=http://localhost:3917/mcp # Refuse a port something already answers on: it would be tested instead of this build. curl -s -m 2 -o /dev/null "$URL" && { echo "port 3917 is already in use" >&2; exit 1; } PORT=3917 node src/everything/dist/index.js streamableHttp >/dev/null 2>&1 & SERVER_PID=$! trap 'kill "$SERVER_PID" 2>/dev/null' EXIT # stops it however the subshell ends READY= for _ in $(seq 50); do # at most ten seconds kill -0 "$SERVER_PID" 2>/dev/null || { echo "server exited before listening" >&2; exit 1; } curl -s -m 2 -o /dev/null "$URL" && { READY=1; break; } sleep 0.2 done [ -n "$READY" ] || { echo "server did not listen within ten seconds" >&2; exit 1; } $INSPECT "$URL" --method tools/call --tool-name echo --tool-arg message=over-http --format json ) ``` Each part of that is there for a failure it prevents. The first check refuses a port that already answers, because a stale listener there would be the server under test. Without the wait, the CLI can reach the port before the server is listening and exit 4. The wait is bounded and checks that this server is still alive, so one that fails to bind (the build is missing, the 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 fails; a listener left on the port makes the next run talk to the old build. By hand in two terminals, the equivalent is waiting for the `listening on port` line and stopping the server with Ctrl-C. A change to a tool, resource or prompt of `everything` is checked over stdio and Streamable HTTP. Check HTTP+SSE as well when the change touches transport or session code; it is deprecated but must keep working. ## Protocol eras The Inspector connects in one of three eras, chosen with `--protocol-era` (the CLI) or **Server Settings → Protocol Era** (the web UI): | Era | What the Inspector does | | ---------------------- | ----------------------------------------------------------- | | `legacy` (the default) | A plain `initialize`. Negotiates 2025-11-25 or older | | `modern` | Pins 2026-07-28 through `server/discover`, with no fallback | | `auto` | Probes `server/discover`, and falls back to `initialize` | **Every server serves both eras**: the Python servers since #4853, the TypeScript servers since #4852 (stdio through the SDK's `serveStdio`, and `everything`'s Streamable HTTP endpoint through `createMcpHandler` for 2026-07-28 requests). Run every command twice, with `--protocol-era legacy` and `--protocol-era modern`, and expect both to succeed. A 2026-07-28 result carries `io.modelcontextprotocol/serverInfo` in its `_meta`, which is the quick way to see which era answered. Do not use `auto` for evidence: it does not say which era answered. `everything` lists some tools on one era only (the task tools on 2026-07-28, the two `toggle-*` tools on the 2025 era), so a `tools/list` that differs between the two runs is expected. The CLI declares no client capabilities, so a tool that asks the client for input (elicitation, sampling, roots) cannot run from it: on the 2025 era `everything` does not list it (`tool_not_found`, exit 5), and on 2026-07-28 the SDK refuses the call because the request did not declare the capability (`Cannot request input 'form' (elicitation/create): the request's client capabilities do not declare the required capability`, exit 1). Drive those from the web UI. On 2026-07-28 the CLI does follow a task handle: a call to `simulate-research-query` polls the task and prints its final result. ## The Inspector web UI The web client is driven with **Playwright** in headless Chromium: an agent writes the script, runs it, and records what came back. It is not a step to hand to a maintainer. Everything below was run on **Inspector 2.12.0** and **Playwright 1.64.1**, against `everything` in both eras; pin both in any script you keep, since the selectors are the UI's, not an API. ```sh npx -y @modelcontextprotocol/inspector@2.12.0 --web --protocol-era modern -- \ node src/everything/dist/index.js stdio npx -y @modelcontextprotocol/inspector@2.12.0 --web --protocol-era legacy -- \ uv --directory src/time run mcp-server-time npx -y @modelcontextprotocol/inspector@2.12.0 --web \ --transport http --server-url http://localhost:3001/mcp ``` ⚠️ **The web launcher reads `--` the other way round from the CLI.** Here the Inspector's own options come **before** `--` and the server command **after** it; in `--cli` mode the server command is before `--` and the options after. A line copied from one mode to the other starts the wrong thing. Pass a server's environment with `-e KEY=VALUE` before the `--`. Install Playwright in a scratch directory **outside the worktree**, never as a dependency of this repo (`npm i playwright@1.64.1`, then `npx playwright install chromium`), and run the script from there. The launcher inherits the script's working directory, so spawn it with `cwd` set to the worktree root: the server commands above use paths relative to it, and from the scratch directory no server would start. ### The recipe - **Launch.** Spawn the launcher above with `MCP_AUTO_OPEN_ENABLED=false` in its environment, or it opens a tab in your own browser as well. It prints a tokenized URL (`http://127.0.0.1:6274?MCP_INSPECTOR_API_TOKEN=…`) on stdout; read it from there and navigate to it. Start the launcher in its own process group (`detached: true`) and kill the group when the script ends, so the server it spawned goes with it. - **Connect.** The connect control is a Mantine switch whose input is hidden: `getByRole("switch", { name: /Connect or disconnect/ }).check({ force: true })`. - **Pick a screen.** The Servers, Tools, Prompts and Resources tabs are radios
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub