1. Project Overview & Quickstart (modelcontextprotocol/inspector)
MCP Inspector
A developer tool for inspecting Model Context Protocol (MCP) servers. It ships as a single package, @modelcontextprotocol/inspector, that provides three ways to inspect a server:
- Web โ a Vite + React + Mantine single-page app with a Node backend.
- CLI โ a scriptable command-line client for automation, CI, and fast agent feedback loops.
- TUI โ an interactive terminal UI built with Ink.
All three run through one global mcp-inspector binary:
npx @modelcontextprotocol/inspector # web UI (default)
npx @modelcontextprotocol/inspector --cli # CLI
npx @modelcontextprotocol/inspector --tui # TUIUpgrading from v1? Read the v1 โ v2 migration guide โ CLI flags, the new
--configvs.--catalogsplit, the Node engine bump, and what no longer ships.
Repo status. This is the v2 line of the Inspector. Active development happens on
v2/main(the develop branch โ all v2 PRs target it), which is merged intomainat milestone releases;mainis the default branch and holds the latest released v2, published to the npmlatesttag. The legacy v1 line lives onv1/mainโ security fixes only, published straight from that branch to the npmv1-latesttag (npx @modelcontextprotocol/inspector@v1-latest). SeeAGENTS.mdfor branch/board conventions.
Project layout
v2 is not an npm workspace. Each client under clients/* keeps its own package.json and node_modules; shared code lives in core/ and is consumed via a @inspector/core build-time alias (no package.json of its own). A single npm install at the root cascades installs into every client (see Setup).
/* Detailed source-code truncated for AI context efficiency. */Each client has its own README with client-specific detail:
web ยท cli ยท tui ยท launcher.
Task-oriented guides live under docs/:
- Migrating from v1 to v2 โ the v1 โ v2 map: CLI flag mapping,
--configvs.--catalogsemantics with before/after examples, the Node engine bump (>=22.7.5โ>=22.19.0), env-var renames, and the sub-packages that no longer ship. - MCP server configuration โ which server(s) the Inspector connects to:
--catalogvs.--config, ad-hoc targets, the--separator, the file format and its Inspector-specific per-server fields. Shared by all three clients; the cli and tui READMEs delegate their server-options sections to it. - Reviewing an MCP App โ the CLI-first โ one-shot-web recipe for automated App-tool review:
--app-infoprobe โ deep-link navigate โ rendered widget, plus OAuth handoff and proxy support. - Launcher and config consolidation โ why the launcher runs a client in-process rather than spawning it, and how the shared config processor fits in.
Setup
Requires Node >=22.19.0.
npm install # root install; postinstall cascades into every client- Fresh clone: run
npm installat the repo root. - After a pull that changes a client's dependencies: re-run
npm installat the root to re-sync every client.
The cascade (scripts/install-clients.mjs) is dev-only โ it exits early when the package is installed as a dependency, and the published tarball ships only each client's build/, so end users are unaffected. Set INSPECTOR_SKIP_CLIENT_INSTALL=1 to skip it.
Where a dependency is declared. The MCP SDK packages (@modelcontextprotocol/client, core, server, server-legacy, ext-apps) live in the root package.json only โ never in a client's. Node resolution walks up, so the root install is on every client's chain, and the root manifest is already what the published tarball resolves against. Declaring them per client installs a second copy that can drift from the root's, which is how two versions of ext-apps (and of the transitive v1 @modelcontextprotocol/sdk) ended up in the tree before #1970 โ and a second copy of client/core is the failure vitest.shared.mts carries a dedupe workaround for. The same root-only placement holds for anything reached solely through root-owned code with no manifest of its own (test-servers/src, core/), and vitest.shared.mts aliases those to the repo root โ express and yaml, both reached through test-servers/src, are the two today. Whether such a package is a dependency or a devDependency follows from who consumes it at runtime, not from where it is declared: anything core/ imports at runtime must be a root dependency, because the client builds externalize npm packages and a published install resolves them from the root manifest, where devDependencies are absent. express is test-only and is a devDependency; yaml currently sits in dependencies. vite and @vitejs/plugin-react are root dependencies for the same reason, not by mistake โ they look like build tooling, but clients/web/server/start-vite-dev-server.ts imports them at runtime for mcp-inspector --web --dev, and clients/web/tsup.runner.config.ts lists both as external, so a published install resolves them from the root manifest. Moving them to devDependencies would break --web --dev for consumers (and the on-demand vite build in ensure-web-build.ts) while passing every local check. It does mean they show up under npm audit --omit=dev, which is a feature: they really are in the production tree.
Running during development
For day-to-day web iteration, run Vite directly from the web client (fast HMR, no launcher build needed):
cd clients/web && npm run devThe launcher-driven scripts below run the built launcher, so build first (npm run build):
npm run web # prod web launcher against clients/web/dist
npm run web:dev # web launcher in --dev mode (Vite)The `@inspector/core` shared package
core/ holds the logic shared by all three clients so that web, CLI, and TUI behave identically. Its entry point is the InspectorClient class (core/mcp/), which owns the connection to an MCP server, the request/response lifecycle, and a set of state stores; core/react/ exposes React hooks over those stores that both the web and TUI (Ink) React trees consume. OAuth (core/auth/) is factored into isomorphic logic plus browser/node/remote backends so the same flows work in the browser, in Node, and against a remote backend.
core/ intentionally has no package.json โ it is not published on its own. Each client bundles it in via a @inspector/core alias:
- CLI / TUI:
esbuildOptions.aliasin theirtsup.config.tsmaps@inspector/coreโ the repocore/directory, andnoExternal: [/^@inspector\/core/]inlines it into the bundle. - Web: the same alias in
clients/web/vite.config.tsfor the browser app and the Node backend runner.
Publishing core/ as its own package (e.g. for third parties to build on) is deliberately deferred โ see issue #1636.
Web client: "dumb components" + Storybook
The v2 web client is built from presentational ("dumb") components โ they accept data and callbacks as props and contain only display logic, with no direct data fetching or client state. State comes from the @inspector/core hooks, wired in near the top of the tree. This keeps components isolated, testable, and documentable.
That approach is what makes Storybook first-class here: every screen and element component has a *.stories.tsx file (96+ stories) that renders it against fixture props. Storybook play functions double as interaction tests, run headless in CI (npm run ci:storybook, Chromium via Playwright).
Styling follows a strict Mantine-first convention (theme variants and component props over CSS classes, --inspector-* CSS custom properties over raw color literals). The full rules live in AGENTS.md under React instructions โ read them before touching web UI. Element components live in clients/web/src/components/elements/; theme variants in clients/web/src/theme/.
Test servers
test-servers/ provides composable MCP servers used by the integration and smoke suites, so tests exercise a real server over a real transport instead of mocks. A server is assembled from presets (fixture factories in test-servers/src/preset-registry.ts โ tools, resources, prompts, tasks, elicitation, sampling, OAuth, โฆ) and can be driven two ways:
- In-process โ import the factories (
createTestServerHttp,createEchoTool, โฆ) and run the server inside the test's event loop (used by the HTTP integration paths). - As a subprocess โ
test-servers/build/test-server-stdio.jsis spawned as a real stdio child (used by the CLI smoke and stdio integration tests).
Configure a server declaratively with a JSON config (see test-servers/configs/*.json) selecting presets, then load it via --config. Because the servers are spawned as real subprocesses, the build output must exist first:
npm run test-servers:build # (from clients/web) โ tsc -p test-servers, emits test-servers/build/The Vite alias @modelcontextprotocol/inspector-test-server (in clients/web/vite.config.ts) points at test-servers/build/index.js so getTestMcpServerPath() resolves to a real .js path.
Serving the modern protocol era
A streamable-HTTP server can also serve the modern (2026-07-28) protocol era via the SDK's createMcpHandler:
- Set
transport.modernin the JSON config โtruefor dual-era stateless serving, or{ "legacy": "reject" }for modern-only strict. - Or pass
modernon theServerConfigfor an in-processcreateTestServerHttp.
This is what lets an Inspector connection negotiating protocolEra: "auto" | "modern" reach the modern leg (populated server/discover, sessionless). See test-servers/configs/modern-http.json.
Showcase configs
Each config below is a ready-made server for exercising one feature by hand. Load one with --config, and unless noted, connect with Protocol Era = Modern.
| Config | Demonstrates | Issue |
|---|---|---|
mcp-app-http.json (legacy era) |
An MCP App (UI resource + app tool) in the Apps tab | #1859 |
modern-mrtr-http.json |
A single MRTR round-trip | โ |
mrtr-showcase-http.json |
Every MRTR preset in one server | #1860 |
modern-network-http.json |
Network tab: Mcp-* headers + error taxonomy |
#1628 |
xmcpheader-modern-http.json |
Tools tab: x-mcp-header mirroring and exclusions |
#1632 |
pagination-http.json |
Page-by-page list fetching | #1721 |
structured-output-http.json |
Tools tab: a result's structuredContent section |
#1908 |
duplicate-tool-names-http.json |
A tools/list that repeats a tool name |
#1957 |
nullable-fields-http.json |
Tools tab: nullable (anyOf + null) arguments |
#1928 |
rfc6570-templates-http.json |
Resources tab: RFC 6570 resource-template expansion | #1919 |
advertised-extensions-http.json |
Tool registration gated on advertised extensions | #1739 |
logging-{legacy,modern}-http.json |
Logging, both eras | #1629 |
subscriptions-{legacy,modern}-http.json |
Resource subscriptions, both eras | #1630 |
tasks-{legacy,modern}-http.json |
Tasks, both eras | #1631 |
MCP Apps
mcp-app-http.json serves the mcp_app_demo tool (_meta.ui.resourceUri) alongside its mcp_app_demo_widget UI resource, so the Apps tab has a real App to render. It is a plain streamable-HTTP server โ connect with the default (legacy) protocol era, not Modern.
Open the Apps tab, select mcp_app_demo, give it a title and click Open App: the widget renders inside the sandbox iframe and exercises the host-side UI protocol surface โ host-context render, size-changed, ui/message, and a log line into the App logs panel. Because the widget is served through the sandbox proxy page, this config is also what reproduces #1859 (a missing clients/web/static/sandbox_proxy.html surfaces here as a "Sandbox not loaded" message in place of the widget) โ a failure that only ever appeared in an installed package, never in the repo.
For the scripted version of the same flow (--app-info probe โ deep link โ rendered widget), see Reviewing an MCP App.
MRTR
modern-mrtr-http.json serves the mrtr_confirm tool (preset mrtr_confirm, createMrtrTool) over the modern leg. Its handler returns inputRequired(...) embedding a form elicitation, so invoking it produces a real round-trip: input_required โ the client fulfils the embedded elicitation and retries with a new id โ complete.
The Inspector drives MRTR manually (inputRequired: { autoFulfill: false }), so the embedded elicitation pauses at the pending-request modal (tagged "input_required") for you to answer, then the retry completes. Useful for eyeballing both that pending-request UX and the Protocol view's MRTR conversation grouping.
mrtr-showcase-http.json bundles every MRTR preset in one server:
| Preset | Behavior |
|---|---|
mrtr_confirm |
Single round |
mrtr_two_step |
Two elicitation rounds via requestState |
mrtr_sample |
Embedded sampling โ the Sampling panel |
mrtr_roots |
Embedded roots/list, auto-answered silently from configured roots (no modal) |
mrtr_edge |
An inputRequests-only round, then a requestState-only round |
mrtr_empty |
Completes with an empty result โ no content, no structuredContent |
mrtr_loop |
Never completes โ trips the MRTR_MAX_ROUNDS bound |
Run mrtr_empty and answer its single elicitation: the Protocol tab groups the
exchange as an MRTR conversation ending COMPLETE, and the Results panel says
"Empty result โ The tool call completed successfully and returned no content."
On the broken build that same result rendered as "No results yet", the panel's
pre-run placeholder (#1860) โ
so a call the user had just watched succeed read as a call that never ran. An
empty content array with no structuredContent is a legal CallToolResult,
and the panel only ever mounts once a result exists, so the placeholder wording
could not be true there. (The neighbouring half of the same gap โ a result whose
payload lives only in structuredContent โ was closed by
#1908.)
The legacy
collect_elicitationpreset callsserver.elicitInput, which errors on the 2026-07-28 leg โ serverโclient requests aren't allowed there. MRTR is the modern replacement.
Network tab โ standardized headers and error taxonomy
modern-network-http.json covers SEP-2243 / SEP-2575. It serves a get_weather tool whose city argument carries an x-mcp-header: "City" annotation, so a modern client mirrors it to Mcp-Param-City.
It also serves four trigger_* tools that the modern leg's spec-error injector (transport.modern.injectSpecErrors: true) answers with a real HTTP status plus JSON-RPC error body:
| Tool | Response |
|---|---|
trigger_header_mismatch |
400 / -32020 |
trigger_missing_capability |
400 / -32021 |
trigger_unsupported_version |
400 / -32022 (with data.supported) |
trigger_method_not_found |
404 / -32601 |
Open the Network tab to see the mirrored Mcp-* headers highlighted, sentinel values decoded, and each error rendered distinctly.
Mcp-Param-*mirroring is built by the Inspector, not the SDK. The SDK only mirrors insideclient.callTool(), and skips it in the browser (detectProbeEnvironment() !== "browser"). The Inspector routestools/callthroughclient.request()to drive MRTR manually, so it builds the mirrored headers itself (#1846) โ on every client, web included, since the web client's upstream request is issued by the Node backend rather than the browser. Soget_weatheris callable from web, CLI, and TUI alike, in both the plain and "Run as task" forms.
`x-mcp-header` in the Tools tab
xmcpheader-modern-http.json serves:
echoโ plain tool.get_weatherโ a validx-mcp-header: "City"annotation on itscityargument.invalid_header_toolโ an annotation using the header name"Bad Header". The space makes it an invalid RFC 9110 token, so the whole tool definition is invalid.trigger_invalid_paramsโ answered with a real-32602 Invalid paramserror whose message is not about a missing tool.
Open the Tools tab: get_weather's detail panel shows a "Mirrored request headers (SEP-2243)" section (city โ Mcp-Param-City), and invalid_header_tool appears struck-through under an "Excluded (SEP-2243)" divider with the reason on hover. A conforming Streamable HTTP client MUST drop it from tools/list; the Inspector surfaces why.
Under SDK v2 a tools/call rejecting with -32602 renders as a distinct error panel rather than an isError result โ headed "Unknown Tool" when the message names a missing tool, or "Invalid Parameters" otherwise (run trigger_invalid_params).
Page-by-page fetching
pagination-http.json serves 12 tools, 12 resources, and 12 prompts (presets numbered_tools / numbered_resources / numbered_prompts, count: 12) with a maxPageSize of 4 each, so every list paginates into three pages.
Turn on "Fetch Lists One Page at a Time" (Server Settings โ the paginatedLists setting, or the Paginated switch in a list sidebar) and the lists load page 1 only (4 items) with a Load next page control and an N pages loaded status. Each click fetches the next 4 and appends them; Refresh resets to page 1. With the switch off (the default), the same lists auto-aggregate all three pages on connect.
Structured output
structured-output-http.json serves list_items (nested structuredContent โ objects inside arrays inside an object, the shape from #1908), get_temp (a flat three-key payload), and echo (no outputSchema at all). It is a plain streamable-HTTP server โ connect with the default (legacy) protocol era.
Run list_items from the Tools tab: the result panel shows the content[] text summary ("Found 2 items.") and a collapsible Structured Output section rendering the schema-validated payload as pretty-printed, copyable JSON. That section is what v2 was dropping โ a tool declaring an outputSchema returns its real data there, and the text block usually only summarizes it. Run echo to confirm the section is absent when a result carries no structuredContent.
Duplicate tool names
duplicate-tool-names-http.json serves get_weather, get_temp, echo, and add, then repeats get_weather and echo at the end of tools/list with the same name and a (duplicate) title (duplicateToolNames). No preset can produce this shape โ the SDK's registerTool rejects a repeated name โ but a real server can and does, and the Inspector has to render it faithfully.
Connect (default legacy era), open the Tools tab, and type get into Search tools: the list must narrow to exactly the three get_* rows. On the broken build it kept a stale echo row, because the sidebar keyed rows by tool.name alone and the colliding keys orphaned a child during reconciliation (#1957).
The duplicated copies are appended rather than placed beside their twin on purpose. React matches a leading run of same-key children first, so a head-adjacent duplicate happens to line up and the defect hides; separating the pair is what makes it observable โ and it is also the realistic shape, two tool sources concatenated.
Nullable arguments
nullable-fields-http.json serves record_shipment, whose four arguments are each declared with Zod's .nullish() โ "optional and explicitly nullable". That compiles to anyOf: [, { "type": "null" }], so the real type (and, for the enum, its enum list) sits on a branch rather than at the top level. get_temp sits alongside it with a plain, non-nullable units enum for comparison. Plain streamable-HTTP โ connect with the default (legacy) protocol era.
Open the Tools tab and select record_shipment: direction must render as a Select (envio / recebimento) with a clear button that sets it back to null, reference as a text input, quantity as a number input, and express as a checkbox. On the broken build every one of them fell through to the raw-JSON textarea, which re-escaped its own contents on each keystroke until the value was unusable (#1928). The tool echoes the arguments it received, so the result panel shows exactly what was sent.
The TUI had the same gap and is worth checking against the same server (--tui, then test record_shipment): direction is a select, quantity an integer field, express a boolean. Both clients now share one collapse step โ normalizeNullableUnion in core/json/nullableUnion.ts โ precisely so they cannot drift on which schemas they can render.
RFC 6570 resource templates
rfc6570-templates-http.json serves two resource templates straight out of #1919 โ events_by_topic (foobar://events/{topic}) and events_by_query (foobar://events{?topic}) โ each echoing the URI it was matched against, plus a plain foobar://events resource (see below). Plain streamable-HTTP; connect with the default (legacy) protocol era.
Open the Resources tab and pick events_by_topic, then enter foo/bar. The request must go out as foobar://events/foo%2Fbar, and the result echoes back the URI the server matched. On the broken build the value was spliced in raw, so the slash created a second path segment and the SDK's matcher answered -32602 Resource not found: foobar://events/foo/bar โ the exact failure in the issue. The same holds for ?, #, %, spaces, and non-ASCII text.
events_by_query is the half that was invisible: the old /\{(\w+)\}/g scan could not see an expression carrying an operator, so no topic input was rendered at all. It now appears, marked Optional โ RFC 6570 drops the whole expression when the variable is undefined, so reading with the field blank requests foobar://events, and filling it in requests foobar://events?topic=foo%2Fbar. The URI preview beside the title shows the partially-expanded form as you type, leaving unfilled expressions standing as written.
The plain
foobar://eventsresource is registered deliberately, not as filler. The SDK'sUriTemplate.match()compiles{?topic}to a required\?topic=([^&]+), so a template alone cannot serve the blank read โmatch("foobar://events")returnsnull. A real server exposes the unfiltered collection as its own resource; the showcase does the same so that step actually resolves.
The web client and the TUI expand through one shared helper, core/mcp/uriTemplate.ts โ the web Resources form directly, the TUI via InspectorClient.readResourceFromTemplate โ and both derive their form fields from its parser too, which is the half that makes the sharing real: a form submits values under the names it rendered, so a parser that mangles a name silently drops the value at expansion time. (The CLI is not a consumer: it has no template form, and its resources/read passes the already-expanded --uri straight through.)
The SDK's UriTemplate is still used, but only to validate a template (constructing it is what rejects an unclosed expression). Its expander is not, because it is incomplete in five ways โ each measured against the pinned SDK, not inferred:
| Shape | SDK behavior |
|---|---|
{a,b} |
raw-joins the values โ no encoding, operator prefix dropped |
{;id} |
; is missing from its operator list, so the variable parses as ;id |
{id:3} |
the prefix modifier is folded into the name, giving id:3 |
{+v} / {#v} |
encodeURI mangles reserved [/] ([::1] โ %5B::1%5D) and double-encodes pct-triplets (%2F โ %252F) |
{v} |
encodeURIComponent leaves the sub-delims !'()* bare, which RFC 6570 requires encoded |
The ; and :3 rows are the ones a user sees directly: on the SDK's parse the form renders fields literally labelled ;id and id:3. The +/# row is silent corruption rather than over-escaping โ an IPv6 literal or an already-encoded path arrives at the server altered.
A template that cannot be expanded at all โ an out-of-grammar modifier ({id:abc}), or an expression declaring no variable ({}, {a,}, {?}) โ withholds the read rather than sending something. Pick events_malformed (foobar://events/{topic:abc}) to see it: Read Resource is disabled, the reason is printed under the form, and the preview shows the template as the server declared it. The alternative is worse than it looks: x://{} would otherwise expand to x:// with no inputs rendered, so the form's "everything required is filled" check passes vacuously and it reads a URI that is not the template the server published.
Literals are pct-encoded on expansion too (RFC 6570 ยง3.1): cafรฉ/{var} sends caf%C3%A9/value, not raw UTF-8 in the path โ something the SDK's expander does not do either. And the names a template may use are RFC 6570's varchar plus a labelled tolerance for - and ~: the conformance suite rejects {default-graph-uri}, but real servers publish such names and the SDK's matcher round-trips them, so the Inspector expands them and marks the variable conforming: false rather than refusing a resource that demonstrably works.
An undefined variable is what omits its expression โ a variable defined as the empty string expands (x{?q} gives x?q=, x{;q} gives x;q, per RFC 6570 ยง3.2.7). The expander honors that distinction, so a caller such as readResourceFromTemplate can request either URI. Collapsing the two is a form concern, not a template one: both clients seed every declared variable with "" and a text input cannot express "defined but empty", so each form drops its blanks (definedValues) on the way in.
Requiredness is a property of the expression, not the variable: RFC 6570 drops undefined names from a multi-name expression, so {a,b} with only a filled is expandable and a form must not block it. requiredGroups returns one entry per non-omittable expression and hasRequiredValues asks that each be satisfied by any one of its names โ which no per-variable flag can express once a name recurs across expressions ({a,b}{a,c} is satisfied by filling b and c).
Advertised extensions
advertised-extensions-http.json serves echo (always) and a get_weather tool gated on the io.modelcontextprotocol/tasks extension (extensionGatedTools): the tool is registered but starts disabled, and the server enables it on notifications/initialized only when the client declared that extension in its capabilities.extensions.
- Connect โ the Inspector advertises the Tasks extension by default, so the Tools list shows both
echoandget_weather. - Open Server Settings โ Advertised Extensions, uncheck Tasks (io.modelcontextprotocol/tasks), and reconnect.
- The client now advertises no extensions, the server never enables
get_weather, and the Tools list shows onlyecho.
This is the debugging knob for a server legitimately changing tool registration based on what the client advertises. Legacy stateful leg only โ the modern per-request leg has no persistent oninitialized.
Logging, both eras
logging-legacy-http.json and logging-modern-http.json both serve logging: true plus a send_notification tool that emits a notifications/message at a chosen level. The legacy one is a plain streamable-HTTP server; the modern one sets transport.modern: true.
- Legacy โ the Logs tab gives a session-scoped Set Active Level selector + Set button. Calling
send_notificationstreams the log into the panel. - Modern โ the same tab instead shows Log Level per Request. Pick a level to opt in and the client stamps
_meta["io.modelcontextprotocol/logLevel"]on every subsequent request (verify in the Network tab's request body). Callingsend_notificationstreams the log over the request's SSE response. Set it back to Off and the same call is silently gated โ the request omits thelogLevelkey, so the log never arrives.
That gating is faithful to the spec ("a server MUST NOT emit notifications/message for a request that didn't opt in") because send_notification emits through the SDK's request-scoped, threshold-aware extra.log (ctx.mcpReq.log). On the modern leg it reads the per-request logLevel opt-in from the request envelope and drops the message when the client didn't opt in or the level is below the requested severity; on legacy it honors the session level from logging/setLevel. Because it emits through the request's notify, the modern response upgrades to SSE and the log rides the originating request's stream.
Resource subscriptions, both eras
subscriptions-legacy-http.json and subscriptions-modern-http.json both serve three numbered_resources with subscriptions: true. The legacy one also serves an update_resource tool; the modern one sets transport.modern: true.
- Legacy โ open a resource in the Resources tab and click Subscribe. The client sends
resources/subscribeand the Subscriptions section lists the URI with no stream chrome. Callupdate_resourcewith that URI and the server updates the content and emitsnotifications/resources/updated, stamping the subscribed tile's last-updated time. - Modern โ the same Subscribe instead sends
subscriptions/listen(its filter carriesresourceSubscriptionsplus theresourcesListChangedopt-in) and resolves onnotifications/subscriptions/acknowledged. The Subscriptions section then shows a stream-status badge (ConnectingโฆโListening) in its header, and reconnects by re-listing if the long-lived stream drops.
The modern config deliberately omits update_resource. The SDK's modern leg is stateless/per-request (createMcpHandler(() => createMcpServer(config))), so the tool would run against a throwaway server instance โ the content change wouldn't persist for the next resources/read, and its resources/updated wouldn't reach the separate listen stream. More confusing than useful.
So the live update-notification round-trip is demonstrated on the legacy (stateful-session) server, and the modern server is for the subscribe/listen/badge behavior. The Inspector's receive path is era-transparent, so a real stateful modern server that routes resources/updated onto the listen stream drives the subscribed tile the same way.
Tasks, both eras
Legacy (tasks-legacy-http.json) advertises capabilities.tasks (tasks: { list, cancel }) with the simple_task / progress_task / elicitation_task presets. Run one of those tools with Run as task on, and the Tasks tab lists it (populated via tasks/list), polls tasks/get, fetches the payload with the blocking tasks/result, and cancels with tasks/cancel.
Modern (tasks-modern-http.json) sets transport.modern: true and tasksExtension: true, advertising the io.modelcontextprotocol/tasks extension (SEP-2663) and serving modern_task / modern_input_task. The Tasks tab is gated on the negotiated extension, not capabilities.tasks.
- Run
modern_taskas a task โ thetools/callreturns aCreateTaskResult(resultType: "task", visible in the Protocol/Network tabs), the client pollstasks/get(notasks/list), and the completed task inlines its result (no blockingtasks/result). - Run
modern_input_taskโ the task moves toinput_required, surfacing an embedded elicitation through the pending-request modal. Answering it sendstasks/updatewith theinputResponses, and the next poll completes.
SDK v2 removed all tasks support and era-gates the tasks/* spec methods out of the modern era on both sides. So the Inspector drives the extension itself โ the resultType: "task" frame is rewritten at the transport into a CallToolResult carrying the handle, and tasks/get / update / cancel ride a raw-wire request channel with the full modern envelope. The test server serves tasks/* from an Express interceptor ahead of the SDK handler, since the SDK's modern leg would answer them -32601.
The Tasks tab's Refresh re-polls the handles already known to the client โ modern has no server-side task list.
Building
npm run build # builds all clients: web โ cli โ tui โ launcherIndividual clients: build:web, build:cli, build:tui, build:launcher. The web build produces both the browser SPA (clients/web/dist, Vite) and the Node prod-server runner (clients/web/build, tsup).
Testing & the quality gate
Each client self-validates from its own folder; the root scripts chain them. There is no aggregate root test script โ use validate (fast) or coverage (the gate).
| Script | What it does |
|---|---|
npm run validate |
Runs the three durable guards first โ verify:format-coverage (every tracked source file is format-gated), verify:typecheck-coverage (every one lands in a tsconfig project), verify:dep-lockstep (no dependency reaching one tsc program from two installs skews across them) โ then test:scripts (the guards' own parser unit tests), then validate:core (the shared core/ format:check + lint gate), then per client: format:check + lint + typecheck (cli/tui/launcher; web typechecks via tsc -b inside its build) + build + fast unit tests. The quick inner-loop check. |
npm run coverage |
The per-file โฅ90% gate (lines/statements/functions/branches) under v8 instrumentation, per client. CI-enforced. For web this also runs the integration project and covers the shared core/ runtime (including core/json and core/client). |
npm run smoke |
End-to-end smokes through the built launcher (--help dispatch + prod cli/tui/web), plus two headless-Chromium smokes: a boot smoke that runs the prod web bundle and asserts a clean first render (no uncaught error โ sync exception or unhandled rejection, how a Node built-in reaching the browser bundle manifests), and an MCP Apps smoke (smoke:web:app) that drives connect โ open app โ data-app-status="ready" against a composable App server, covering the sandbox proxy and UI-protocol bridge. |
npm run verify:build-gate |
Runs a real vite build with a Node built-in forced into the browser graph and asserts the build fails via the #1769 gate (which turns Vite's browser-externalization warning into a hard error). Guards against the warning phrasing drifting in a Vite bump and silently disabling the gate. Part of npm run ci. |
npm run verify:format-coverage |
Parses the format:check globs out of every package.json (only those reachable from validate), enumerates all tracked source files, and fails listing any not covered by a glob โ the durable guard for the "every first-party source file is format-gated" invariant (#1792). Runs first in validate. |
npm run test:scripts |
Table-driven unit tests (node --test) for the guard's own pure parsers (scripts/lib/npm-scripts.mjs, scripts/lib/tsc-program.mjs + the exported helpers of verify-typecheck-coverage.mjs and verify-dep-lockstep.mjs), one case per rule they encode, plus scripts/lib/resolve-node-bin.test.mjs โ the cross-platform bin resolver (#1939), pinned against the real bin/exports shapes of the packages the scripts actually spawn. Runs in validate โ and verify:typecheck-coverage guards this gate in turn (reachable from validate, non-empty test set, every test file matched by the test:scripts glob), since node --test silently skips a file its glob misses and still exits 0. |
npm run verify:typecheck-coverage |
The typecheck-coverage analog of the above (#1791): for each Node client (auto-discovered from disk โ enrolled via its typecheck script's projects, or for a tsc -b client like clients/web via its tsconfig.json references) it runs those projects with tsc --listFilesOnly, unions them, and fails listing any tracked .ts/.tsx/.mts/.cts under the client that lands in no project (so a new top-level config/helper can't silently go untypechecked). It also requires, deny-by-default, the first-party TS no client owns (test-servers/src, the root vitest.shared.mts, all of core/, and any new top-level location) to land in some client project's tsc pass โ so a core *.tsx web's projects don't reach is caught too. Also asserts the gate is wired (each client's typecheck pass โ its typecheck script, or web's tsc -b โ is reachable from its validate, and the root chain runs each client's validate). Runs in validate. |
npm run verify:dep-lockstep |
Guards the "one version per install-crossing dependency" invariant (#1896). v2 is not a workspace, so a client's test project compiles the shared first-party TypeScript โ core/, test-servers/src, and the root-owned vitest.shared.mts, all of which resolve their dependencies from the root install โ alongside the client's own sources, putting the same package in one tsc program twice. At the same version that's harmless; skewed, TypeScript must relate two structurally-distinct copies of every type, which for a recursive-generic surface is exponential (zod 4.3.6 vs 4.4.3 exhausted the 4GB tsc heap in clients/web). Derives its candidate set from what actually enters each program (#1965) โ every client tsconfig project listed with tsc --listFilesOnly via the shared scripts/lib/tsc-program.mjs, each resolved node_modules file mapped to its owning install, keeping the packages that reach one program from two installs (a package whose declarations arrive only through another package's .d.ts, as @modelcontextprotocol/sdk's do, is invisible to a scan of first-party imports). Prices each copy from the lockfile entry for the exact install path the program resolved, compares only the installs that met in one program, and fails deny-by-default on any disagreement not in the annotated TOLERATED_SKEW allowlist โ empty today โ with an allowlisted package tolerated only within a major version. Runs in validate. |
npm run ci |
Mandatory pre-push command. validate โ coverage โ verify:build-gate โ smoke โ Storybook. A true superset of GitHub CI. |
npm run pack:verify |
Publish smoke โ see Publishing. |
Per-client scripts exist too (validate:web, coverage:cli, smoke:tui, โฆ), plus root validate:core / format:core for the shared core/ package, format:scripts for the root scripts/ tooling, and format:shared / lint:shared for the root "shared" surface (test-servers/src/**, vitest.shared.mts, the root eslint.config.js). Run npm run format before committing โ the root format fixes core/, the root scripts/, the shared surface, and every client; validate runs the non-fixing format:check and fails CI on any unformatted file.
Linting is type-aware. All five ESLint scopes (clients/{web,cli,tui,launcher} plus the root core/ + shared gate) enable @typescript-eslint/no-floating-promises at error, so a promise that is neither awaited, returned, .catch(โฆ)-terminated, nor explicitly discarded with void fails lint โ and therefore validate (#1959). The rule needs type information, so each scope's config names a parser project; the root scope's is tsconfig.lint.json, a lint-only project covering core/**, test-servers/src/**, and vitest.shared.mts, which have no tsconfig of their own. It emits nothing and changes no typecheck โ but a new first-party TS location added to the root lint scope must be added to its include. See TypeScript instructions in AGENTS.md for when void is acceptable.
For the full testing rules โ the โฅ90% per-file gate, where test files live, the unit vs. integration vs. storybook projects, and the v8 ignore policy โ see AGENTS.md.
Publishing
The root @modelcontextprotocol/inspector package ships as one tarball with a single version number โ no separate -web / -cli / -tui / -core packages. npm run build builds every client, then prepack runs before npm publish. Runtime dependencies are declared on the root package.json; client builds bundle @inspector/core and externalize npm packages resolved from the root install.
What ships, and the packaging invariants
The root package.json "files" allowlist is the source of truth for the tarball. A few non-obvious entries exist because they are read at runtime or were silently dropped by npm's packlist โ do not remove them without re-running npm run pack:verify:
- No source maps. The client bundlers set
sourcemap: false(clients/{cli,tui}/tsup.config.ts,clients/web/tsup.runner.config.ts); Vite and the launcher'stscalready emit none. Maps are ~half the unpacked size and aren't needed at runtime โ debug vianpm run devon the source. clients/web/buildships viaclients/web/.npmignore.clients/web/.gitignorelistsbuild/, and npm's packlist honors that nested.gitignoreover the root"files"allowlist โ so the prod web-server runner was silently missing from the tarball whileclients/web/distslipped through (its.gitignoreonly listsdist-ssr).clients/web/.npmignoreoverrides the.gitignorefor publishing so bothbuild/(runner) anddist/(SPA) ship. The other clients don't need this โ none ship a nested.gitignore.clients/web/staticships the MCP Apps sandbox proxy.clients/web/static/sandbox_proxy.htmlis a committed source file (not a build artifact), read from disk at runtime byclients/web/server/sandbox-controller.tsas<runner dir>/../static/sandbox_proxy.html. It was missing from the root"files"allowlist entirely, so every published build failed the Apps tab with "Sandbox not loaded" (#1859) while working fine in the repo. Because the path is resolved relative toclients/web/build, the directory must ship at that exact location โpack:verifyasserts both the tarball entry and the installed-on-disk path.- A dependency that renders React is bundled, not externalized. An externalized package resolves its own
reactfrom wherever npm placed it in the consumer's tree, which is not necessarily where the bundle resolves ours โ npm places a package beside a React satisfying its peer range, and those ranges are looser than ours.ink-formandink-scroll-viewdeclare">=18", so a project holding React 18 satisfies them and gets them hoisted while the Inspector's React 19 nests underneath: two React copies, and the TUI dies withTypeError: Cannot read properties of null (reading 'useState')the moment a tool test form or a scroll view mounts (#1952). Both are therefore inlined byclients/tui/tsup.config.tsand are not root dependencies: the tarball ships their code insideclients/tui/build/index.jsrather than having consumers install them. Bundling also pins their transitive deps to what this repo's install resolved (notablyink-select-input@6viaoverrides, which npm ignores for a package installed as a dependency).inkis the one exception, on cost: bundling it works but adds ~1.4 MB (react-reconcilerandyoga-layoutcome along, plus acreateRequirebanner for the inlined CJS), so it stays external โ not because its">=19"peer makes it safe, which it does not. What keeps that tolerable is the rootreactrange:"^19.0.0"is deliberately open to the whole major so npm can dedupe our React with whatever React 19 a consumer pins, leaving an externalinkon the same copy the bundle uses. Narrowing that range reopens the bug for the renderer itself โclients/tui/__tests__/tsupConfig.test.tspins it toink's peer floor, and guards the rest of the split; see the TUI README. - A single version number, read from the root
package.json. The Inspector ships as one package with one version, so only the rootpackage.jsoncarries aversionโ the fourclients/*/package.jsons deliberately have none. Every Node client (CLI, TUI, and the web backend) resolves the version through the sharedreadInspectorVersion()reader incore/node/version.ts, which walks up to the root manifest (always present in the tarball). No clientpackage.jsonis read at runtime, so none needs to ship. The web browser can't read the filesystem; it gets its version from the backend viaGET /api/config(see #1639).
`npm run pack:verify` โ publish smoke against the real tarball
The smoke:* scripts run against the in-repo build tree, which is not the published package. npm run pack:verify (scripts/pack-and-verify.mjs) closes that gap: it builds, npm packs the publishable tarball (asserting no source maps ship and that the runtime-required files are present), installs the tarball into a clean throwaway consumer โ a fresh temp directory where it runs a real npm install <tgz> (pulls runtime deps, runs postinstall), exactly as npx @modelcontextprotocol/inspector would โ and drives the installed mcp-inspector bin end to end: --help dispatch, a real --cli tools/list over stdio, and a prod --web boot that must serve / from the shipped dist. It catches "works in --dev, breaks under npx โฆ" path/packaging failures. It requires network access (the install pulls deps), so it is a local / release check, not part of the fast validate/ci loop.
Cutting a release
Publishing is automated by two release-gated jobs in .github/workflows/main.yml (github.event_name == 'release', both needs: build):
publishโ the npm package. Runsnpm run pack:verifyas the pre-publish gate, asserts the release tag matches the rootpackage.jsonversion, thennpm publish --access public --provenanceโ a singlenpm publish(v2 is not an npm workspace, so there is no v1-stylepublish-all/--workspaces), with a signed provenance attestation via GitHub OIDC (id-token: write,environment: release,NPM_TOKEN).publish-github-container-registryโ the container image (see Docker).
A v2 release is cut from main, after the milestone's work has been merged there from v2/main โ not from v2/main itself. (The v1 line releases independently from v1/main to the v1-latest tag and never touches main; see Repo status.)
Because there is one version number (only the root package.json has one โ the clients carry none, so there is nothing to keep in sync and no check-version step), the release flow is three steps.
1. Bump on v2/main, before the milestone merge. The bump is part of the milestone's work, so it belongs on the develop branch and flows into main with everything else:
Substitute the real issue number and release below โ the commands are written to be copy-pasteable as-is (a 2.2.0 โ 2.3.0 minor bump):
git checkout -b v2/chore/2010-bump-2-3-0 v2/main
npm version minor --no-git-tag-version # or major / patch; bump only, no tag
# PR โ v2/mainโ ๏ธ --no-git-tag-version is load-bearing. A bare npm version also tags, and the tag would land on a v2/main commit โ but the release must be cut from main, so the tag has to point at the merge commit there (step 3). Tagging here creates a tag on a commit that is never released.
2. Merge v2/main โ main through the usual milestone-merge branch. It now carries the bump, so the release lands on main with the version already correct.
Between steps 1 and 2 the two branches do differ, and that is expected, not drift: v2/main reads the version being built while main still reads the one currently released. What this ordering removes is post-release drift โ once the milestone merge lands they agree again, and v2/main is never left behind main. If you see v2/main ahead of main, a release is in flight; if you see it behind, something went wrong.
3. Tag the main commit and draft the Release:
git fetch origin main
git tag 2.3.0 origin/main && git push origin 2.3.0
# then draft & publish a GitHub Release for that tag โ triggers `publish`โ ๏ธ Tag origin/main, not your local HEAD. git checkout main && git pull resolves through whatever merge-or-rebase strategy you have configured, so a divergent local main can quietly produce or replay local commits. Tagging HEAD there tags a commit that is not on origin/main, and git push origin <tag> pushes only the tag โ leaving a release whose commit was never published. Naming origin/main explicitly makes the tagged commit exactly what the remote branch points at, regardless of local state.
โ ๏ธ No v prefix. This repo's release tags are bare x.y.z โ 2.2.0, 2.1.0, 2.0.0 โ so tag 2.3.0, not v2.3.0. Note npm's own tag-version-prefix defaults to v and the repo sets no .npmrc, so a bare npm version would have produced a v-prefixed tag that does not match the convention. Tagging by hand (step 3) is what keeps it right. The workflow's assert step strips a leading v before comparing, so a v-prefixed tag would still publish โ it would just be inconsistent with every previous release.
The release's target commit selects which workflow runs, so this only publishes when a release is cut from a commit carrying this (v2) workflow.
Why the bump goes on v2/main first (#2010). It used to happen on the milestone-merge branch, which is cut from main โ so the bump existed only downstream of v2/main and nothing carried it back. v2/main sat at 2.0.0 through both the 2.1.0 and 2.2.0 releases. That is not cosmetic: a branch cut from a milestone-merge branch silently carries the bump into an unrelated PR (this happened on #2009, where a container bugfix arrived with a 2.0.0 โ 2.2.0 diff), and anything reading the version in development โ readInspectorVersion(), --version, GET /api/config โ reported a version two releases old.
Do not "fix" a future drift by merging main back into v2/main. main carries the entire pre-v2 v1 history (retained through ec5d8e13 chore: replace main's tree with v2 โ ~230 commits v2/main does not have), so a back-merge grafts all of it into the develop branch's log permanently in order to deliver a two-file change. Bumping first means there is nothing to back-merge.
Docker
A container image is published to GHCR (ghcr.io/modelcontextprotocol/inspector, linux/amd64 + linux/arm64) by the release workflow. The Dockerfile is a two-stage build: the first stage installs and npm packs the publishable tarball; the second stage npm install -gs that tarball, so the image ships the exact same artifact as npm, with a clean mcp-inspector bin.
# run the web UI (reads the auth token from the container logs)
docker run --rm -p 127.0.0.1:6274:6274 ghcr.io/modelcontextprotocol/inspector
# or build the image locally
docker build -t mcp-inspector .
docker run --rm -p 127.0.0.1:6274:6274 mcp-inspectorUsing the Apps tab? Publish 6275 too. The MCP Apps sandbox is a second listener the browser reaches directly, on MCP_SANDBOX_PORT (default 6275). Nothing else needs it, so the single-port commands above are fine for ordinary inspection โ but the Apps tab renders a blank widget without it:
docker run --rm -p 127.0.0.1:6274:6274 -p 127.0.0.1:6275:6275 \
ghcr.io/modelcontextprotocol/inspectorPublish it on the same port number inside and out. The sandbox URL is handed to the browser via /api/config as http://localhost:<container port>/sandbox, so remapping it (-p 9000:6275) advertises a port the browser can't reach; use -e MCP_SANDBOX_PORT=9000 -p 127.0.0.1:9000:9000 instead.
Keep the 127.0.0.1: prefix on the published port. A bare -p 6274:6274 publishes on every host interface, putting the Inspector on your local network. The container's HOST=0.0.0.0 is a separate concern โ it governs the container's interfaces, not the host's โ so the DANGEROUSLY_BIND_ALL_INTERFACES opt-in that guards a wildcard bind outside a container does not cover this. It matters more here than for an ordinary web app: the backend spawns processes on request, GET / embeds the API token into the served HTML, and a request arriving with no Origin header skips the origin allow-list entirely โ so for any non-browser client the API token is the only guard. Publishing wider needs a real access-control boundary in front of the Inspector โ a reverse proxy that authenticates, an SSH tunnel, a private network. Setting your own MCP_INSPECTOR_API_TOKEN does not substitute: GET / discloses whatever token is in use, so a custom one is harvested exactly as easily as a generated one.
Keeping the servers you add. The Inspector saves your server list to $HOME/.mcp-inspector/mcp.json, which in the image is /home/node/.mcp-inspector/mcp.json โ inside the container's writable layer, so --rm discards it and every run starts with an empty list. Mount a volume there to keep it:
docker run --rm -p 127.0.0.1:6274:6274 \
-v mcp-inspector-data:/home/node/.mcp-inspector \
ghcr.io/modelcontextprotocol/inspectorThe same volume also persists OAuth tokens and stored state, so an authorized server stays authorized across runs. Use -e MCP_CATALOG_PATH=/some/other/path.json to put the catalog somewhere else โ mount a volume covering whatever directory you point it at. If you bind-mount a host directory instead of a named volume (-v "$PWD/inspector-data:/home/node/.mcp-inspector"), the directory keeps its host ownership, so on Linux add --user "$(id -u):$(id -g)" or chown it to uid 1000 โ otherwise the non-root node user can't write and adding a server fails with EACCES.
Upgrading from an image before this fix? Earlier images did not create /home/node/.mcp-inspector, so Docker created the volume's mount point as root and the non-root node user couldn't write to it. An empty volume repairs itself on the first run of a current image (Docker applies the image directory's ownership to an empty volume), but one that already has files in it keeps its old root ownership and still fails with EACCES. Fix it once:
docker run --rm -u 0 --entrypoint chown \
-v mcp-inspector-data:/data ghcr.io/modelcontextprotocol/inspector \
-R node:node /dataThe image defaults to --web bound to 0.0.0.0:6274 with browser auto-open disabled; override the args to run another mode (docker run --rm ghcr.io/modelcontextprotocol/inspector --cli โฆ). Pass -e MCP_INSPECTOR_API_TOKEN=โฆ to set a known token (otherwise one is generated and printed in the logs), or -e DANGEROUSLY_OMIT_AUTH=true to disable auth. Binding 0.0.0.0 (all network interfaces) is refused by default outside a container โ it exposes the process-spawning backend to the local network โ so the image opts in explicitly with DANGEROUSLY_BIND_ALL_INTERFACES=true (already set in the Dockerfile); a bare HOST=0.0.0.0 without that flag exits with an error. If you remap the published port (-p 127.0.0.1:8080:6274), the browser's origin (http://localhost:8080) no longer matches the in-container port, so set -e ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080 (or run -e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080) or connects will 403. ALLOWED_ORIGINS replaces the default list rather than merging, so list every loopback form you'll browse from (see the web README). The image runs as the non-root node user and has a HEALTHCHECK that probes the web UI โ it assumes the default --web mode, so add --no-healthcheck when running --cli/--tui (which have no web server).
Contributing โ `AGENTS.md` and `CLAUDE.md`
AGENTS.md is the contract for changing this codebase, and it applies to humans and AI agents alike. It is not agent-only boilerplate โ it holds the project's real conventions: the issue-and-board workflow, branch/label rules, the TypeScript and Mantine/React standards, the testing and coverage requirements, and the mandatory pre-push gate. Read it before making changes, and keep it up to date when you change structure, tooling, or rules.
CLAUDE.md is the entry point the Claude Code agent loads automatically; it simply includes AGENTS.md and this README, so both agents and humans work from the same source of truth. If you use a different agent that reads AGENTS.md, you get the same rules.
A key rule worth surfacing here: all work is issue-driven. Before starting, find or create a tracking issue on the v2 project board; open PRs against v2/main with Closes #. The exact recipes (labels, board IDs, statuses) are in AGENTS.md.
License
MIT.
2. Official Technical Reference & Guides (modelcontextprotocol/docs)
File: README.md
Model Context Protocol documentation
This documentation has moved to modelcontextprotocol/modelcontextprotocol.
Please file issues and pull requests for documentation changes there.
File: docs/tools/debugging.mdx
title: Debugging
Effective debugging is essential when developing MCP servers or integrating them with applications. This guide covers the debugging tools and approaches available in the MCP ecosystem.
This guide is for macOS. Guides for other platforms are coming soon.
Debugging tools overview
MCP provides several tools for debugging at different levels:
MCP Inspector
- Interactive debugging interface
- Direct server testing
- See the Inspector guide for details
Claude Desktop Developer Tools
- Integration testing
- Log collection
- Chrome DevTools integration
Server Logging
- Custom logging implementations
- Error tracking
- Performance monitoring
Debugging in Claude Desktop
Checking server status
The Claude.app interface provides basic server status information:
Click the icon to view:
- Connected servers
- Available prompts and resources
Click the icon to view:
- Tools made available to the model
Viewing logs
Review detailed MCP logs from Claude Desktop:
# Follow logs in real-time
tail -n 20 -F ~/Library/Logs/Claude/mcp*.logThe logs capture:
- Server connection events
- Configuration issues
- Runtime errors
- Message exchanges
Using Chrome DevTools
Access Chrome's developer tools inside Claude Desktop to investigate client-side errors:
- Create a
developer_settings.jsonfile withallowDevToolsset to true:
echo '{"allowDevTools": true}' > ~/Library/Application\ Support/Claude/developer_settings.json- Open DevTools:
Command-Option-Shift-i
Note: You'll see two DevTools windows:
- Main content window
- App title bar window
Use the Console panel to inspect client-side errors.
Use the Network panel to inspect:
- Message payloads
- Connection timing
Common issues
Working directory
When using MCP servers with Claude Desktop:
- The working directory for servers launched via
claude_desktop_config.jsonmay be undefined (like/on macOS) since Claude Desktop could be started from anywhere - Always use absolute paths in your configuration and
.envfiles to ensure reliable operation - For testing servers directly via command line, the working directory will be where you run the command
For example in claude_desktop_config.json, use:
{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username/data"]
}Instead of relative paths like ./data
Environment variables
MCP servers inherit only a subset of environment variables automatically, like USER, HOME, and PATH.
To override the default variables or provide your own, you can specify an env key in claude_desktop_config.json:
{
"myserver": {
"command": "mcp-server-myapp",
"env": {
"MYAPP_API_KEY": "some_key",
}
}
}Server initialization
Common initialization problems:
Path Issues
- Incorrect server executable path
- Missing required files
- Permission problems
- Try using an absolute path for
command
Configuration Errors
- Invalid JSON syntax
- Missing required fields
- Type mismatches
Environment Problems
- Missing environment variables
- Incorrect variable values
- Permission restrictions
Connection problems
When servers fail to connect:
- Check Claude Desktop logs
- Verify server process is running
- Test standalone with Inspector
- Verify protocol compatibility
Implementing logging
Server-side logging
When building a server that uses the local stdio transport, all messages logged to stderr (standard error) will be captured by the host application (e.g., Claude Desktop) automatically.
For all transports, you can also provide logging to the client by sending a log message notification:
Important events to log:
- Initialization steps
- Resource access
- Tool execution
- Error conditions
- Performance metrics
Client-side logging
In client applications:
- Enable debug logging
- Monitor network traffic
- Track message exchanges
- Record error states
Debugging workflow
Development cycle
Initial Development
- Use Inspector for basic testing
- Implement core functionality
- Add logging points
Integration Testing
- Test in Claude Desktop
- Monitor logs
- Check error handling
Testing changes
To test changes efficiently:
- Configuration changes: Restart Claude Desktop
- Server code changes: Use Command-R to reload
- Quick iteration: Use Inspector during development
Best practices
Logging strategy
Structured Logging
- Use consistent formats
- Include context
- Add timestamps
- Track request IDs
Error Handling
- Log stack traces
- Include error context
- Track error patterns
- Monitor recovery
Performance Tracking
- Log operation timing
- Monitor resource usage
- Track message sizes
- Measure latency
Security considerations
When debugging:
Sensitive Data
- Sanitize logs
- Protect credentials
- Mask personal information
Access Control
- Verify permissions
- Check authentication
- Monitor access patterns
Getting help
When encountering issues:
First Steps
- Check server logs
- Test with Inspector
- Review configuration
- Verify environment
Support Channels
- GitHub issues
- GitHub discussions
Providing Information
- Log excerpts
- Configuration files
- Steps to reproduce
- Environment details
Next steps
File: docs/tools/inspector.mdx
title: Inspector
The MCP Inspector is an interactive developer tool for testing and debugging MCP servers. While the Debugging Guide covers the Inspector as part of the overall debugging toolkit, this document provides a detailed exploration of the Inspector's features and capabilities.
Getting started
Installation and basic usage
The Inspector runs directly through npx without requiring installation:
npx @modelcontextprotocol/inspector <command>npx @modelcontextprotocol/inspector <command> <arg1> <arg2>Inspecting servers from NPM or PyPi
A common way to start server packages from NPM or PyPi.
Inspecting locally developed servers
To inspect servers locally developed or downloaded as a repository, the most common
way is:
Please carefully read any attached README for the most accurate instructions.
Feature overview
The Inspector provides several features for interacting with your MCP server:
Server connection pane
- Allows selecting the transport for connecting to the server
- For local servers, supports customizing the command-line arguments and environment
Resources tab
- Lists all available resources
- Shows resource metadata (MIME types, descriptions)
- Allows resource content inspection
- Supports subscription testing
Prompts tab
- Displays available prompt templates
- Shows prompt arguments and descriptions
- Enables prompt testing with custom arguments
- Previews generated messages
Tools tab
- Lists available tools
- Shows tool schemas and descriptions
- Enables tool testing with custom inputs
- Displays tool execution results
Notifications pane
- Presents all logs recorded from the server
- Shows notifications received from the server
Best practices
Development workflow
Start Development
- Launch Inspector with your server
- Verify basic connectivity
- Check capability negotiation
Iterative testing
- Make server changes
- Rebuild the server
- Reconnect the Inspector
- Test affected features
- Monitor messages
Test edge cases
- Invalid inputs
- Missing prompt arguments
- Concurrent operations
- Verify error handling and error responses
Next steps
<Card
title="Debugging Guide"
icon="bug"
href="/docs/tools/debugging"
>
Learn about broader debugging strategies
</Card>File: docs/concepts/architecture.mdx
title: "Core architecture"
The Model Context Protocol (MCP) is built on a flexible, extensible architecture that enables seamless communication between LLM applications and integrations. This document covers the core architectural components and concepts.
Overview
MCP follows a client-server architecture where:
- Hosts are LLM applications (like Claude Desktop or IDEs) that initiate connections
- Clients maintain 1:1 connections with servers, inside the host application
- Servers provide context, tools, and prompts to clients
flowchart LR
subgraph "Host"
client1[MCP Client]
client2[MCP Client]
end
subgraph "Server Process"
server1[MCP Server]
end
subgraph "Server Process"
server2[MCP Server]
end
client1 <-->|Transport Layer| server1
client2 <-->|Transport Layer| server2Core components
Protocol layer
The protocol layer handles message framing, request/response linking, and high-level communication patterns.
// Handle incoming notifications
setNotificationHandler<T>(schema: T, handler: (notification: T) => Promise<void>): void
// Send requests and await responses
request<T>(request: Request, schema: T, options?: RequestOptions): Promise<T>
// Send one-way notifications
notification(notification: Notification): Promise<void>
}
```async def send_notification(
self,
notification: NotificationT
) -> None:
"""Send one-way notification that doesn't expect response."""
# Notification handling implementation
async def _received_request(
self,
responder: RequestResponder[ReceiveRequestT, ResultT]
) -> None:
"""Handle incoming request from other side."""
# Request handling implementation
async def _received_notification(
self,
notification: ReceiveNotificationT
) -> None:
"""Handle incoming notification from other side."""
# Notification handling implementation
```Key classes include:
ProtocolClientServer
Transport layer
The transport layer handles the actual communication between clients and servers. MCP supports multiple transport mechanisms:
Stdio transport
- Uses standard input/output for communication
- Ideal for local processes
HTTP with SSE transport
- Uses Server-Sent Events for server-to-client messages
- HTTP POST for client-to-server messages
All transports use JSON-RPC 2.0 to exchange messages. See the specification for detailed information about the Model Context Protocol message format.
Message types
MCP has these main types of messages:
Requests expect a response from the other side:
typescriptinterface Request { method: string; params?: { ... }; }Results are successful responses to requests:
typescriptinterface Result { [key: string]: unknown; }Errors indicate that a request failed:
typescriptinterface Error { code: number; message: string; data?: unknown; }Notifications are one-way messages that don't expect a response:
typescriptinterface Notification { method: string; params?: { ... }; }
Connection lifecycle
1. Initialization
sequenceDiagram
participant Client
participant Server
Client->>Server: initialize request
Server->>Client: initialize response
Client->>Server: initialized notification
Note over Client,Server: Connection ready for use- Client sends
initializerequest with protocol version and capabilities - Server responds with its protocol version and capabilities
- Client sends
initializednotification as acknowledgment - Normal message exchange begins
2. Message exchange
After initialization, the following patterns are supported:
- Request-Response: Client or server sends requests, the other responds
- Notifications: Either party sends one-way messages
3. Termination
Either party can terminate the connection:
- Clean shutdown via
close() - Transport disconnection
- Error conditions
Error handling
MCP defines these standard error codes:
enum ErrorCode {
// Standard JSON-RPC error codes
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603
}SDKs and applications can define their own error codes above -32000.
Errors are propagated through:
- Error responses to requests
- Error events on transports
- Protocol-level error handlers
Implementation example
Here's a basic example of implementing an MCP server:
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
resources: {}
}
});
// Handle requests
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "example://resource",
name: "Example Resource"
}
]
};
});
// Connect transport
const transport = new StdioServerTransport();
await server.connect(transport);
```app = Server("example-server")
@app.list_resources()
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri="example://resource",
name="Example Resource"
)
]
async def main():
async with stdio_server() as streams:
await app.run(
streams[0],
streams[1],
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
```Best practices
Transport selection
Local communication
- Use stdio transport for local processes
- Efficient for same-machine communication
- Simple process management
Remote communication
- Use SSE for scenarios requiring HTTP compatibility
- Consider security implications including authentication and authorization
Message handling
Request processing
- Validate inputs thoroughly
- Use type-safe schemas
- Handle errors gracefully
- Implement timeouts
Progress reporting
- Use progress tokens for long operations
- Report progress incrementally
- Include total progress when known
Error management
- Use appropriate error codes
- Include helpful error messages
- Clean up resources on errors
Security considerations
Transport security
- Use TLS for remote connections
- Validate connection origins
- Implement authentication when needed
Message validation
- Validate all incoming messages
- Sanitize inputs
- Check message size limits
- Verify JSON-RPC format
Resource protection
- Implement access controls
- Validate resource paths
- Monitor resource usage
- Rate limit requests
Error handling
- Don't leak sensitive information
- Log security-relevant errors
- Implement proper cleanup
- Handle DoS scenarios
Debugging and monitoring
Logging
- Log protocol events
- Track message flow
- Monitor performance
- Record errors
Diagnostics
- Implement health checks
- Monitor connection state
- Track resource usage
- Profile performance
Testing
- Test different transports
- Verify error handling
- Check edge cases
- Load test servers
File: docs/concepts/prompts.mdx
title: "Prompts"
Prompts enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs. They provide a powerful way to standardize and share common LLM interactions.
Overview
Prompts in MCP are predefined templates that can:
- Accept dynamic arguments
- Include context from resources
- Chain multiple interactions
- Guide specific workflows
- Surface as UI elements (like slash commands)
Prompt structure
Each prompt is defined with:
{
name: string; // Unique identifier for the prompt
description?: string; // Human-readable description
arguments?: [ // Optional list of arguments
{
name: string; // Argument identifier
description?: string; // Argument description
required?: boolean; // Whether argument is required
}
]
}Discovering prompts
Clients can discover available prompts through the prompts/list endpoint:
// Request
{
method: "prompts/list"
}
// Response
{
prompts: [
{
name: "analyze-code",
arguments: [
{
name: "language",
required: true
}
]
}
]
}Using prompts
To use a prompt, clients make a prompts/get request:
// Request
{
method: "prompts/get",
params: {
name: "analyze-code",
arguments: {
language: "python"
}
}
}
// Response
{
messages: [
{
role: "user",
content: {
type: "text",
text: "Please analyze the following Python code for potential improvements:\n\n```python\ndef calculate_sum(numbers):\n total = 0\n for num in numbers:\n total = total + num\n return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n```"
}
}
]
}Dynamic prompts
Prompts can be dynamic and include:
Embedded resource context
{
"name": "analyze-project",
"description": "Analyze project logs and code",
"arguments": [
{
"name": "timeframe",
"description": "Time period to analyze logs",
"required": true
},
{
"name": "fileUri",
"description": "URI of code file to review",
"required": true
}
]
}When handling the prompts/get request:
{
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Analyze these system logs and the code file for any issues:"
}
},
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": "logs://recent?timeframe=1h",
"text": "[2024-03-14 15:32:11] ERROR: Connection timeout in network.py:127\n[2024-03-14 15:32:15] WARN: Retrying connection (attempt 2/3)\n[2024-03-14 15:32:20] ERROR: Max retries exceeded",
"mimeType": "text/plain"
}
}
},
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": "file:///path/to/code.py",
"text": "def connect_to_service(timeout=30):\n retries = 3\n for attempt in range(retries):\n try:\n return establish_connection(timeout)\n except TimeoutError:\n if attempt == retries - 1:\n raise\n time.sleep(5)\n\ndef establish_connection(timeout):\n # Connection implementation\n pass",
"mimeType": "text/x-python"
}
}
}
]
}Multi-step workflows
const debugWorkflow = {
name: "debug-error",
async getMessages(error: string) {
return [
{
role: "user",
content: {
type: "text",
text: `Here's an error I'm seeing: ${error}`
}
},
{
role: "assistant",
content: {
type: "text",
text: "I'll help analyze this error. What have you tried so far?"
}
},
{
role: "user",
content: {
type: "text",
text: "I've tried restarting the service, but the error persists."
}
}
];
}
};Example implementation
Here's a complete example of implementing prompts in an MCP server:
Best practices
When implementing prompts:
- Use clear, descriptive prompt names
- Provide detailed descriptions for prompts and arguments
- Validate all required arguments
- Handle missing arguments gracefully
- Consider versioning for prompt templates
- Cache dynamic content when appropriate
- Implement error handling
- Document expected argument formats
- Consider prompt composability
- Test prompts with various inputs
UI integration
Prompts can be surfaced in client UIs as:
- Slash commands
- Quick actions
- Context menu items
- Command palette entries
- Guided workflows
- Interactive forms
Updates and changes
Servers can notify clients about prompt changes:
- Server capability:
prompts.listChanged - Notification:
notifications/prompts/list_changed - Client re-fetches prompt list
Security considerations
When implementing prompts:
- Validate all arguments
- Sanitize user input
- Consider rate limiting
- Implement access controls
- Audit prompt usage
- Handle sensitive data appropriately
- Validate generated content
- Implement timeouts
- Consider prompt injection risks
- Document security requirements
File: docs/concepts/resources.mdx
title: "Resources"
Resources are a core primitive in the Model Context Protocol (MCP) that allow servers to expose data and content that can be read by clients and used as context for LLM interactions.
Server authors should be prepared to handle any of these interaction patterns when implementing resource support. In order to expose data to models automatically, server authors should use a model-controlled primitive such as Tools.
Overview
Resources represent any kind of data that an MCP server wants to make available to clients. This can include:
- File contents
- Database records
- API responses
- Live system data
- Screenshots and images
- Log files
- And more
Each resource is identified by a unique URI and can contain either text or binary data.
Resource URIs
Resources are identified using URIs that follow this format:
[protocol]://[host]/[path]For example:
file:///home/user/documents/report.pdfpostgres://database/customers/schemascreen://localhost/display1
The protocol and path structure is defined by the MCP server implementation. Servers can define their own custom URI schemes.
Resource types
Resources can contain two types of content:
Text resources
Text resources contain UTF-8 encoded text data. These are suitable for:
- Source code
- Configuration files
- Log files
- JSON/XML data
- Plain text
Binary resources
Binary resources contain raw binary data encoded in base64. These are suitable for:
- Images
- PDFs
- Audio files
- Video files
- Other non-text formats
Resource discovery
Clients can discover available resources through two main methods:
Direct resources
Servers expose a list of concrete resources via the resources/list endpoint. Each resource includes:
{
uri: string; // Unique identifier for the resource
name: string; // Human-readable name
description?: string; // Optional description
mimeType?: string; // Optional MIME type
}Resource templates
For dynamic resources, servers can expose URI templates that clients can use to construct valid resource URIs:
{
uriTemplate: string; // URI template following RFC 6570
name: string; // Human-readable name for this type
description?: string; // Optional description
mimeType?: string; // Optional MIME type for all matching resources
}Reading resources
To read a resource, clients make a resources/read request with the resource URI.
The server responds with a list of resource contents:
{
contents: [
{
uri: string; // The URI of the resource
mimeType?: string; // Optional MIME type
// One of:
text?: string; // For text resources
blob?: string; // For binary resources (base64 encoded)
}
]
}Resource updates
MCP supports real-time updates for resources through two mechanisms:
List changes
Servers can notify clients when their list of available resources changes via the notifications/resources/list_changed notification.
Content changes
Clients can subscribe to updates for specific resources:
- Client sends
resources/subscribewith resource URI - Server sends
notifications/resources/updatedwhen the resource changes - Client can fetch latest content with
resources/read - Client can unsubscribe with
resources/unsubscribe
Example implementation
Here's a simple example of implementing resource support in an MCP server:
// List available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain"
}
]
};
});
// Read resource contents
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
if (uri === "file:///logs/app.log") {
const logContents = await readLogFile();
return {
contents: [
{
uri,
mimeType: "text/plain",
text: logContents
}
]
};
}
throw new Error("Resource not found");
});
```@app.list_resources()
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri="file:///logs/app.log",
name="Application Logs",
mimeType="text/plain"
)
]
@app.read_resource()
async def read_resource(uri: AnyUrl) -> str:
if str(uri) == "file:///logs/app.log":
log_contents = await read_log_file()
return log_contents
raise ValueError("Resource not found")
# Start server
async with stdio_server() as streams:
await app.run(
streams[0],
streams[1],
app.create_initialization_options()
)
```Best practices
When implementing resource support:
- Use clear, descriptive resource names and URIs
- Include helpful descriptions to guide LLM understanding
- Set appropriate MIME types when known
- Implement resource templates for dynamic content
- Use subscriptions for frequently changing resources
- Handle errors gracefully with clear error messages
- Consider pagination for large resource lists
- Cache resource contents when appropriate
- Validate URIs before processing
- Document your custom URI schemes
Security considerations
When exposing resources:
- Validate all resource URIs
- Implement appropriate access controls
- Sanitize file paths to prevent directory traversal
- Be cautious with binary data handling
- Consider rate limiting for resource reads
- Audit resource access
- Encrypt sensitive data in transit
- Validate MIME types
- Implement timeouts for long-running reads
- Handle resource cleanup appropriately
File: docs/concepts/roots.mdx
title: "Roots"
Roots are a concept in MCP that define the boundaries where servers can operate. They provide a way for clients to inform servers about relevant resources and their locations.
What are Roots?
A root is a URI that a client suggests a server should focus on. When a client connects to a server, it declares which roots the server should work with. While primarily used for filesystem paths, roots can be any valid URI including HTTP URLs.
For example, roots could be:
file:///home/user/projects/myapp
https://api.example.com/v1Why Use Roots?
Roots serve several important purposes:
- Guidance: They inform servers about relevant resources and locations
- Clarity: Roots make it clear which resources are part of your workspace
- Organization: Multiple roots let you work with different resources simultaneously
How Roots Work
When a client supports roots, it:
- Declares the
rootscapability during connection - Provides a list of suggested roots to the server
- Notifies the server when roots change (if supported)
While roots are informational and not strictly enforcing, servers should:
- Respect the provided roots
- Use root URIs to locate and access resources
- Prioritize operations within root boundaries
Common Use Cases
Roots are commonly used to define:
- Project directories
- Repository locations
- API endpoints
- Configuration locations
- Resource boundaries
Best Practices
When working with roots:
- Only suggest necessary resources
- Use clear, descriptive names for roots
- Monitor root accessibility
- Handle root changes gracefully
Example
Here's how a typical MCP client might expose roots:
{
"roots": [
{
"uri": "file:///home/user/projects/frontend",
"name": "Frontend Repository"
},
{
"uri": "https://api.example.com/v1",
"name": "API Endpoint"
}
]
}This configuration suggests the server focus on both a local repository and an API endpoint while keeping them logically separated.
File: docs/concepts/sampling.mdx
title: "Sampling"
Sampling is a powerful MCP feature that allows servers to request LLM completions through the client, enabling sophisticated agentic behaviors while maintaining security and privacy.
This feature of MCP is not yet supported in the Claude Desktop client.
How sampling works
The sampling flow follows these steps:
- Server sends a
sampling/createMessagerequest to the client - Client reviews the request and can modify it
- Client samples from an LLM
- Client reviews the completion
- Client returns the result to the server
This human-in-the-loop design ensures users maintain control over what the LLM sees and generates.
Message format
Sampling requests use a standardized message format:
{
messages: [
{
role: "user" | "assistant",
content: {
type: "text" | "image",
// For text:
text?: string,
// For images:
data?: string, // base64 encoded
mimeType?: string
}
}
],
modelPreferences?: {
hints?: [{
name?: string // Suggested model name/family
}],
costPriority?: number, // 0-1, importance of minimizing cost
speedPriority?: number, // 0-1, importance of low latency
intelligencePriority?: number // 0-1, importance of capabilities
},
systemPrompt?: string,
includeContext?: "none" | "thisServer" | "allServers",
temperature?: number,
maxTokens: number,
stopSequences?: string[],
metadata?: Record<string, unknown>
}Request parameters
Messages
The messages array contains the conversation history to send to the LLM. Each message has:
role: Either "user" or "assistant"content: The message content, which can be:- Text content with a
textfield - Image content with
data(base64) andmimeTypefields
- Text content with a
Model preferences
The modelPreferences object allows servers to specify their model selection preferences:
hints: Array of model name suggestions that clients can use to select an appropriate model:name: String that can match full or partial model names (e.g. "claude-3", "sonnet")- Clients may map hints to equivalent models from different providers
- Multiple hints are evaluated in preference order
Priority values (0-1 normalized):
costPriority: Importance of minimizing costsspeedPriority: Importance of low latency responseintelligencePriority: Importance of advanced model capabilities
Clients make the final model selection based on these preferences and their available models.
System prompt
An optional systemPrompt field allows servers to request a specific system prompt. The client may modify or ignore this.
Context inclusion
The includeContext parameter specifies what MCP context to include:
"none": No additional context"thisServer": Include context from the requesting server"allServers": Include context from all connected MCP servers
The client controls what context is actually included.
Sampling parameters
Fine-tune the LLM sampling with:
temperature: Controls randomness (0.0 to 1.0)maxTokens: Maximum tokens to generatestopSequences: Array of sequences that stop generationmetadata: Additional provider-specific parameters
Response format
The client returns a completion result:
{
model: string, // Name of the model used
stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string,
role: "user" | "assistant",
content: {
type: "text" | "image",
text?: string,
data?: string,
mimeType?: string
}
}Example request
Here's an example of requesting sampling from a client:
{
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "What files are in the current directory?"
}
}
],
"systemPrompt": "You are a helpful file system assistant.",
"includeContext": "thisServer",
"maxTokens": 100
}
}Best practices
When implementing sampling:
- Always provide clear, well-structured prompts
- Handle both text and image content appropriately
- Set reasonable token limits
- Include relevant context through
includeContext - Validate responses before using them
- Handle errors gracefully
- Consider rate limiting sampling requests
- Document expected sampling behavior
- Test with various model parameters
- Monitor sampling costs
Human in the loop controls
Sampling is designed with human oversight in mind:
For prompts
- Clients should show users the proposed prompt
- Users should be able to modify or reject prompts
- System prompts can be filtered or modified
- Context inclusion is controlled by the client
For completions
- Clients should show users the completion
- Users should be able to modify or reject completions
- Clients can filter or modify completions
- Users control which model is used
Security considerations
When implementing sampling:
- Validate all message content
- Sanitize sensitive information
- Implement appropriate rate limits
- Monitor sampling usage
- Encrypt data in transit
- Handle user data privacy
- Audit sampling requests
- Control cost exposure
- Implement timeouts
- Handle model errors gracefully
Common patterns
Agentic workflows
Sampling enables agentic patterns like:
- Reading and analyzing resources
- Making decisions based on context
- Generating structured data
- Handling multi-step tasks
- Providing interactive assistance
Context management
Best practices for context:
- Request minimal necessary context
- Structure context clearly
- Handle context size limits
- Update context as needed
- Clean up stale context
Error handling
Robust error handling should:
- Catch sampling failures
- Handle timeout errors
- Manage rate limits
- Validate responses
- Provide fallback behaviors
- Log errors appropriately
Limitations
Be aware of these limitations:
- Sampling depends on client capabilities
- Users control sampling behavior
- Context size has limits
- Rate limits may apply
- Costs should be considered
- Model availability varies
- Response times vary
- Not all content types supported
File: docs/concepts/tools.mdx
title: "Tools"
Tools are a powerful primitive in the Model Context Protocol (MCP) that enable servers to expose executable functionality to clients. Through tools, LLMs can interact with external systems, perform computations, and take actions in the real world.
Overview
Tools in MCP allow servers to expose executable functions that can be invoked by clients and used by LLMs to perform actions. Key aspects of tools include:
- Discovery: Clients can list available tools through the
tools/listendpoint - Invocation: Tools are called using the
tools/callendpoint, where servers perform the requested operation and return results - Flexibility: Tools can range from simple calculations to complex API interactions
Like resources, tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems.
Tool definition structure
Each tool is defined with the following structure:
{
name: string; // Unique identifier for the tool
description?: string; // Human-readable description
inputSchema: { // JSON Schema for the tool's parameters
type: "object",
properties: { ... } // Tool-specific parameters
}
}Implementing tools
Here's an example of implementing a basic tool in an MCP server:
// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
}
}]
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "calculate_sum") {
const { a, b } = request.params.arguments;
return {
content: [
{
type: "text",
text: String(a + b)
}
]
};
}
throw new Error("Tool not found");
});
```@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="calculate_sum",
description="Add two numbers together",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["a", "b"]
}
)
]
@app.call_tool()
async def call_tool(
name: str,
arguments: dict
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
if name == "calculate_sum":
a = arguments["a"]
b = arguments["b"]
result = a + b
return [types.TextContent(type="text", text=str(result))]
raise ValueError(f"Tool not found: {name}")
```Example tool patterns
Here are some examples of types of tools that a server could provide:
System operations
Tools that interact with the local system:
{
name: "execute_command",
inputSchema: {
type: "object",
properties: {
command: { type: "string" },
args: { type: "array", items: { type: "string" } }
}
}
}API integrations
Tools that wrap external APIs:
{
name: "github_create_issue",
inputSchema: {
type: "object",
properties: {
title: { type: "string" },
body: { type: "string" },
labels: { type: "array", items: { type: "string" } }
}
}
}Data processing
Tools that transform or analyze data:
{
name: "analyze_csv",
inputSchema: {
type: "object",
properties: {
filepath: { type: "string" },
operations: {
type: "array",
items: {
enum: ["sum", "average", "count"]
}
}
}
}
}Best practices
When implementing tools:
- Provide clear, descriptive names and descriptions
- Use detailed JSON Schema definitions for parameters
- Include examples in tool descriptions to demonstrate how the model should use them
- Implement proper error handling and validation
- Use progress reporting for long operations
- Keep tool operations focused and atomic
- Document expected return value structures
- Implement proper timeouts
- Consider rate limiting for resource-intensive operations
- Log tool usage for debugging and monitoring
Security considerations
When exposing tools:
Input validation
- Validate all parameters against the schema
- Sanitize file paths and system commands
- Validate URLs and external identifiers
- Check parameter sizes and ranges
- Prevent command injection
Access control
- Implement authentication where needed
- Use appropriate authorization checks
- Audit tool usage
- Rate limit requests
- Monitor for abuse
Error handling
- Don't expose internal errors to clients
- Log security-relevant errors
- Handle timeouts appropriately
- Clean up resources after errors
- Validate return values
Tool discovery and updates
MCP supports dynamic tool discovery:
- Clients can list available tools at any time
- Servers can notify clients when tools change using
notifications/tools/list_changed - Tools can be added or removed during runtime
- Tool definitions can be updated (though this should be done carefully)
Error handling
Tool errors should be reported within the result object, not as MCP protocol-level errors. This allows the LLM to see and potentially handle the error. When a tool encounters an error:
- Set
isErrortotruein the result - Include error details in the
contentarray
Here's an example of proper error handling for tools:
This approach allows the LLM to see that an error occurred and potentially take corrective action or request human intervention.
Testing tools
A comprehensive testing strategy for MCP tools should cover:
- Functional testing: Verify tools execute correctly with valid inputs and handle invalid inputs appropriately
- Integration testing: Test tool interaction with external systems using both real and mocked dependencies
- Security testing: Validate authentication, authorization, input sanitization, and rate limiting
- Performance testing: Check behavior under load, timeout handling, and resource cleanup
- Error handling: Ensure tools properly report errors through the MCP protocol and clean up resources
File: docs/concepts/transports.mdx
title: "Transports"
Transports in the Model Context Protocol (MCP) provide the foundation for communication between clients and servers. A transport handles the underlying mechanics of how messages are sent and received.
Message Format
MCP uses JSON-RPC 2.0 as its wire format. The transport layer is responsible for converting MCP protocol messages into JSON-RPC format for transmission and converting received JSON-RPC messages back into MCP protocol messages.
There are three types of JSON-RPC messages used:
Requests
{
jsonrpc: "2.0",
id: number | string,
method: string,
params?: object
}Responses
{
jsonrpc: "2.0",
id: number | string,
result?: object,
error?: {
code: number,
message: string,
data?: unknown
}
}Notifications
{
jsonrpc: "2.0",
method: string,
params?: object
}Built-in Transport Types
MCP includes two standard transport implementations:
Standard Input/Output (stdio)
The stdio transport enables communication through standard input and output streams. This is particularly useful for local integrations and command-line tools.
Use stdio when:
- Building command-line tools
- Implementing local integrations
- Needing simple process communication
- Working with shell scripts
const transport = new StdioServerTransport();
await server.connect(transport);
```const transport = new StdioClientTransport({
command: "./server",
args: ["--option", "value"]
});
await client.connect(transport);
```async with stdio_server() as streams:
await app.run(
streams[0],
streams[1],
app.create_initialization_options()
)
```async with stdio_client(params) as streams:
async with ClientSession(streams[0], streams[1]) as session:
await session.initialize()
```Server-Sent Events (SSE)
SSE transport enables server-to-client streaming with HTTP POST requests for client-to-server communication.
Use SSE when:
- Only server-to-client streaming is needed
- Working with restricted networks
- Implementing simple updates
const app = express();
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {}
});
let transport: SSEServerTransport | null = null;
app.get("/sse", (req, res) => {
transport = new SSEServerTransport("/messages", res);
server.connect(transport);
});
app.post("/messages", (req, res) => {
if (transport) {
transport.handlePostMessage(req, res);
}
});
app.listen(3000);
```const transport = new SSEClientTransport(
new URL("http://localhost:3000/sse")
);
await client.connect(transport);
```app = Server("example-server")
sse = SseServerTransport("/messages")
async def handle_sse(scope, receive, send):
async with sse.connect_sse(scope, receive, send) as streams:
await app.run(streams[0], streams[1], app.create_initialization_options())
async def handle_messages(scope, receive, send):
await sse.handle_post_message(scope, receive, send)
starlette_app = Starlette(
routes=[
Route("/sse", endpoint=handle_sse),
Route("/messages", endpoint=handle_messages, methods=["POST"]),
]
)
```Custom Transports
MCP makes it easy to implement custom transports for specific needs. Any transport implementation just needs to conform to the Transport interface:
You can implement custom transports for:
- Custom network protocols
- Specialized communication channels
- Integration with existing systems
- Performance optimization
// Send a JSON-RPC message
send(message: JSONRPCMessage): Promise<void>;
// Close the connection
close(): Promise<void>;
// Callbacks
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
}
```Args:
read_stream: Stream to read incoming messages from
write_stream: Stream to write outgoing messages to
"""
async with anyio.create_task_group() as tg:
try:
# Start processing messages
tg.start_soon(lambda: process_messages(read_stream))
# Send messages
async with write_stream:
yield write_stream
except Exception as exc:
# Handle errors
raise exc
finally:
# Clean up
tg.cancel_scope.cancel()
await write_stream.aclose()
await read_stream.aclose()
```Error Handling
Transport implementations should handle various error scenarios:
- Connection errors
- Message parsing errors
- Protocol errors
- Network timeouts
- Resource cleanup
Example error handling:
async send(message: JSONRPCMessage) {
try {
// Sending logic
} catch (error) {
this.onerror?.(new Error(`Failed to send message: ${error}`));
throw error;
}
}
}
```async def message_handler():
try:
async with read_stream_writer:
# Message handling logic
pass
except Exception as exc:
logger.error(f"Failed to handle message: {exc}")
raise exc
async with anyio.create_task_group() as tg:
tg.start_soon(message_handler)
try:
# Yield streams for communication
yield read_stream, write_stream
except Exception as exc:
logger.error(f"Transport error: {exc}")
raise exc
finally:
tg.cancel_scope.cancel()
await write_stream.aclose()
await read_stream.aclose()
except Exception as exc:
logger.error(f"Failed to initialize transport: {exc}")
raise exc
```Best Practices
When implementing or using MCP transport:
- Handle connection lifecycle properly
- Implement proper error handling
- Clean up resources on connection close
- Use appropriate timeouts
- Validate messages before sending
- Log transport events for debugging
- Implement reconnection logic when appropriate
- Handle backpressure in message queues
- Monitor connection health
- Implement proper security measures
Security Considerations
When implementing transport:
Authentication and Authorization
- Implement proper authentication mechanisms
- Validate client credentials
- Use secure token handling
- Implement authorization checks
Data Security
- Use TLS for network transport
- Encrypt sensitive data
- Validate message integrity
- Implement message size limits
- Sanitize input data
Network Security
- Implement rate limiting
- Use appropriate timeouts
- Handle denial of service scenarios
- Monitor for unusual patterns
- Implement proper firewall rules
Debugging Transport
Tips for debugging transport issues:
- Enable debug logging
- Monitor message flow
- Check connection states
- Validate message formats
- Test error scenarios
- Use network analysis tools
- Implement health checks
- Monitor resource usage
- Test edge cases
- Use proper error tracking