inspector Documentation

Visual testing tool for MCP servers

10,500 stars TypeScript TypeScript 1 file ยท ~31,994 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy inspector on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name inspector -p 6274:6274 --restart always modelcontextprotocol/inspector:latest

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:

bash
npx @modelcontextprotocol/inspector          # web UI (default)
npx @modelcontextprotocol/inspector --cli    # CLI
npx @modelcontextprotocol/inspector --tui    # TUI

Upgrading from v1? Read the v1 โ†’ v2 migration guide โ€” CLI flags, the new --config vs. --catalog split, 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 into main at milestone releases; main is the default branch and holds the latest released v2, published to the npm latest tag. The legacy v1 line lives on v1/main โ€” security fixes only, published straight from that branch to the npm v1-latest tag (npx @modelcontextprotocol/inspector@v1-latest). See AGENTS.md for 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).

text
/* 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, --config vs. --catalog semantics 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: --catalog vs. --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-info probe โ†’ 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.

bash
npm install     # root install; postinstall cascades into every client
  • Fresh clone: run npm install at the repo root.
  • After a pull that changes a client's dependencies: re-run npm install at 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):

bash
cd clients/web && npm run dev

The launcher-driven scripts below run the built launcher, so build first (npm run build):

bash
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.alias in their tsup.config.ts maps @inspector/core โ†’ the repo core/ directory, and noExternal: [/^@inspector\/core/] inlines it into the bundle.
  • Web: the same alias in clients/web/vite.config.ts for 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.js is 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:

bash
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.modern in the JSON config โ€” true for dual-era stateless serving, or { "legacy": "reject" } for modern-only strict.
  • Or pass modern on the ServerConfig for an in-process createTestServerHttp.

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_elicitation preset calls server.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 inside client.callTool(), and skips it in the browser (detectProbeEnvironment() !== "browser"). The Inspector routes tools/call through client.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. So get_weather is 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 valid x-mcp-header: "City" annotation on its city argument.
  • 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 params error 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://events resource is registered deliberately, not as filler. The SDK's UriTemplate.match() compiles {?topic} to a required \?topic=([^&]+), so a template alone cannot serve the blank read โ€” match("foobar://events") returns null. 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.

  1. Connect โ€” the Inspector advertises the Tasks extension by default, so the Tools list shows both echo and get_weather.
  2. Open Server Settings โ†’ Advertised Extensions, uncheck Tasks (io.modelcontextprotocol/tasks), and reconnect.
  3. The client now advertises no extensions, the server never enables get_weather, and the Tools list shows only echo.

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_notification streams 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). Calling send_notification streams the log over the request's SSE response. Set it back to Off and the same call is silently gated โ€” the request omits the logLevel key, 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/subscribe and the Subscriptions section lists the URI with no stream chrome. Call update_resource with that URI and the server updates the content and emits notifications/resources/updated, stamping the subscribed tile's last-updated time.
  • Modern โ€” the same Subscribe instead sends subscriptions/listen (its filter carries resourceSubscriptions plus the resourcesListChanged opt-in) and resolves on notifications/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_task as a task โ€” the tools/call returns a CreateTaskResult (resultType: "task", visible in the Protocol/Network tabs), the client polls tasks/get (no tasks/list), and the completed task inlines its result (no blocking tasks/result).
  • Run modern_input_task โ€” the task moves to input_required, surfacing an embedded elicitation through the pending-request modal. Answering it sends tasks/update with the inputResponses, 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

bash
npm run build     # builds all clients: web โ†’ cli โ†’ tui โ†’ launcher

Individual 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's tsc already emit none. Maps are ~half the unpacked size and aren't needed at runtime โ€” debug via npm run dev on the source.
  • clients/web/build ships via clients/web/.npmignore. clients/web/.gitignore lists build/, and npm's packlist honors that nested .gitignore over the root "files" allowlist โ€” so the prod web-server runner was silently missing from the tarball while clients/web/dist slipped through (its .gitignore only lists dist-ssr). clients/web/.npmignore overrides the .gitignore for publishing so both build/ (runner) and dist/ (SPA) ship. The other clients don't need this โ€” none ship a nested .gitignore.
  • clients/web/static ships the MCP Apps sandbox proxy. clients/web/static/sandbox_proxy.html is a committed source file (not a build artifact), read from disk at runtime by clients/web/server/sandbox-controller.ts as <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 to clients/web/build, the directory must ship at that exact location โ€” pack:verify asserts 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 react from 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-form and ink-scroll-view declare ">=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 with TypeError: Cannot read properties of null (reading 'useState') the moment a tool test form or a scroll view mounts (#1952). Both are therefore inlined by clients/tui/tsup.config.ts and are not root dependencies: the tarball ships their code inside clients/tui/build/index.js rather than having consumers install them. Bundling also pins their transitive deps to what this repo's install resolved (notably ink-select-input@6 via overrides, which npm ignores for a package installed as a dependency). ink is the one exception, on cost: bundling it works but adds ~1.4 MB (react-reconciler and yoga-layout come along, plus a createRequire banner 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 root react range: "^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 external ink on the same copy the bundle uses. Narrowing that range reopens the bug for the renderer itself โ€” clients/tui/__tests__/tsupConfig.test.ts pins it to ink'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 root package.json carries a version โ€” the four clients/*/package.jsons deliberately have none. Every Node client (CLI, TUI, and the web backend) resolves the version through the shared readInspectorVersion() reader in core/node/version.ts, which walks up to the root manifest (always present in the tarball). No client package.json is read at runtime, so none needs to ship. The web browser can't read the filesystem; it gets its version from the backend via GET /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. Runs npm run pack:verify as the pre-publish gate, asserts the release tag matches the root package.json version, then npm publish --access public --provenance โ€” a single npm publish (v2 is not an npm workspace, so there is no v1-style publish-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):

bash
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:

bash
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.

bash
# 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-inspector

Using 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:

bash
docker run --rm -p 127.0.0.1:6274:6274 -p 127.0.0.1:6275:6275 \
  ghcr.io/modelcontextprotocol/inspector

Publish 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:

bash
docker run --rm -p 127.0.0.1:6274:6274 \
  -v mcp-inspector-data:/home/node/.mcp-inspector \
  ghcr.io/modelcontextprotocol/inspector

The 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:

bash
docker run --rm -u 0 --entrypoint chown \
  -v mcp-inspector-data:/data ghcr.io/modelcontextprotocol/inspector \
  -R node:node /data

The 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:

  1. MCP Inspector

    • Interactive debugging interface
    • Direct server testing
    • See the Inspector guide for details
  2. Claude Desktop Developer Tools

    • Integration testing
    • Log collection
    • Chrome DevTools integration
  3. 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:

  1. Click the icon to view:

    • Connected servers
    • Available prompts and resources
  2. Click the icon to view:

    • Tools made available to the model

Viewing logs

Review detailed MCP logs from Claude Desktop:

bash
# Follow logs in real-time
tail -n 20 -F ~/Library/Logs/Claude/mcp*.log

The 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:

  1. Create a developer_settings.json file with allowDevTools set to true:
bash
echo '{"allowDevTools": true}' > ~/Library/Application\ Support/Claude/developer_settings.json
  1. 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.json may be undefined (like / on macOS) since Claude Desktop could be started from anywhere
  • Always use absolute paths in your configuration and .env files 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:

json
{
  "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:

json
{
  "myserver": {
    "command": "mcp-server-myapp",
    "env": {
      "MYAPP_API_KEY": "some_key",
    }
  }
}

Server initialization

Common initialization problems:

  1. Path Issues

    • Incorrect server executable path
    • Missing required files
    • Permission problems
    • Try using an absolute path for command
  2. Configuration Errors

    • Invalid JSON syntax
    • Missing required fields
    • Type mismatches
  3. Environment Problems

    • Missing environment variables
    • Incorrect variable values
    • Permission restrictions

Connection problems

When servers fail to connect:

  1. Check Claude Desktop logs
  2. Verify server process is running
  3. Test standalone with Inspector
  4. 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.

Local MCP servers should not log messages to stdout (standard out), as this will interfere with protocol operation.

For all transports, you can also provide logging to the client by sending a log message notification:

```python server.request_context.session.send_log_message( level="info", data="Server started successfully", ) ``` ```typescript server.sendLoggingMessage({ level: "info", data: "Server started successfully", }); ```

Important events to log:

  • Initialization steps
  • Resource access
  • Tool execution
  • Error conditions
  • Performance metrics

Client-side logging

In client applications:

  1. Enable debug logging
  2. Monitor network traffic
  3. Track message exchanges
  4. Record error states

Debugging workflow

Development cycle

  1. Initial Development

    • Use Inspector for basic testing
    • Implement core functionality
    • Add logging points
  2. 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

  1. Structured Logging

    • Use consistent formats
    • Include context
    • Add timestamps
    • Track request IDs
  2. Error Handling

    • Log stack traces
    • Include error context
    • Track error patterns
    • Monitor recovery
  3. Performance Tracking

    • Log operation timing
    • Monitor resource usage
    • Track message sizes
    • Measure latency

Security considerations

When debugging:

  1. Sensitive Data

    • Sanitize logs
    • Protect credentials
    • Mask personal information
  2. Access Control

    • Verify permissions
    • Check authentication
    • Monitor access patterns

Getting help

When encountering issues:

  1. First Steps

    • Check server logs
    • Test with Inspector
    • Review configuration
    • Verify environment
  2. Support Channels

    • GitHub issues
    • GitHub discussions
  3. Providing Information

    • Log excerpts
    • Configuration files
    • Steps to reproduce
    • Environment details

Next steps

Learn to use the MCP Inspector

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:

bash
npx @modelcontextprotocol/inspector <command>
bash
npx @modelcontextprotocol/inspector <command> <arg1> <arg2>

Inspecting servers from NPM or PyPi

A common way to start server packages from NPM or PyPi.

```bash npx -y @modelcontextprotocol/inspector npx # For example npx -y @modelcontextprotocol/inspector npx server-postgres postgres://127.0.0.1/testdb ``` ```bash npx @modelcontextprotocol/inspector uvx # For example npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git ```

Inspecting locally developed servers

To inspect servers locally developed or downloaded as a repository, the most common
way is:

```bash npx @modelcontextprotocol/inspector node path/to/server/index.js args... ``` ```bash npx @modelcontextprotocol/inspector \ uv \ --directory path/to/server \ run \ package-name \ args... ```

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

  1. Start Development

    • Launch Inspector with your server
    • Verify basic connectivity
    • Check capability negotiation
  2. Iterative testing

    • Make server changes
    • Rebuild the server
    • Reconnect the Inspector
    • Test affected features
    • Monitor messages
  3. Test edge cases

    • Invalid inputs
    • Missing prompt arguments
    • Concurrent operations
    • Verify error handling and error responses

Next steps

Check out the MCP Inspector source code
text
<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
mermaid
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| server2

Core components

Protocol layer

The protocol layer handles message framing, request/response linking, and high-level communication patterns.

```typescript class Protocol { // Handle incoming requests setRequestHandler(schema: T, handler: (request: T, extra: RequestHandlerExtra) => Promise): void
text
// 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>
}
```
```python class Session(BaseSession[RequestT, NotificationT, ResultT]): async def send_request( self, request: RequestT, result_type: type[Result] ) -> Result: """ Send request and wait for response. Raises McpError if response contains error. """ # Request handling implementation
text
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:

  • Protocol
  • Client
  • Server

Transport layer

The transport layer handles the actual communication between clients and servers. MCP supports multiple transport mechanisms:

  1. Stdio transport

    • Uses standard input/output for communication
    • Ideal for local processes
  2. 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:

  1. Requests expect a response from the other side:

    typescript
    interface Request {
      method: string;
      params?: { ... };
    }
  2. Results are successful responses to requests:

    typescript
    interface Result {
      [key: string]: unknown;
    }
  3. Errors indicate that a request failed:

    typescript
    interface Error {
      code: number;
      message: string;
      data?: unknown;
    }
  4. Notifications are one-way messages that don't expect a response:

    typescript
    interface Notification {
      method: string;
      params?: { ... };
    }

Connection lifecycle

1. Initialization

mermaid
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
  1. Client sends initialize request with protocol version and capabilities
  2. Server responds with its protocol version and capabilities
  3. Client sends initialized notification as acknowledgment
  4. 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:

typescript
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:

```typescript import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
text
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);
```
```python import asyncio import mcp.types as types from mcp.server import Server from mcp.server.stdio import stdio_server
text
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

  1. Local communication

    • Use stdio transport for local processes
    • Efficient for same-machine communication
    • Simple process management
  2. Remote communication

    • Use SSE for scenarios requiring HTTP compatibility
    • Consider security implications including authentication and authorization

Message handling

  1. Request processing

    • Validate inputs thoroughly
    • Use type-safe schemas
    • Handle errors gracefully
    • Implement timeouts
  2. Progress reporting

    • Use progress tokens for long operations
    • Report progress incrementally
    • Include total progress when known
  3. Error management

    • Use appropriate error codes
    • Include helpful error messages
    • Clean up resources on errors

Security considerations

  1. Transport security

    • Use TLS for remote connections
    • Validate connection origins
    • Implement authentication when needed
  2. Message validation

    • Validate all incoming messages
    • Sanitize inputs
    • Check message size limits
    • Verify JSON-RPC format
  3. Resource protection

    • Implement access controls
    • Validate resource paths
    • Monitor resource usage
    • Rate limit requests
  4. Error handling

    • Don't leak sensitive information
    • Log security-relevant errors
    • Implement proper cleanup
    • Handle DoS scenarios

Debugging and monitoring

  1. Logging

    • Log protocol events
    • Track message flow
    • Monitor performance
    • Record errors
  2. Diagnostics

    • Implement health checks
    • Monitor connection state
    • Track resource usage
    • Profile performance
  3. 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.

Prompts are designed to be **user-controlled**, meaning they are exposed from servers to clients with the intention of the user being able to explicitly select them for use.

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:

typescript
{
  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:

typescript
// 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:

typescript
// 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

json
{
  "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:

json
{
  "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

typescript
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:

``` /* Detailed source-code truncated for AI context efficiency. */ ``` ``` /* Detailed source-code truncated for AI context efficiency. */ ```

Best practices

When implementing prompts:

  1. Use clear, descriptive prompt names
  2. Provide detailed descriptions for prompts and arguments
  3. Validate all required arguments
  4. Handle missing arguments gracefully
  5. Consider versioning for prompt templates
  6. Cache dynamic content when appropriate
  7. Implement error handling
  8. Document expected argument formats
  9. Consider prompt composability
  10. 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:

  1. Server capability: prompts.listChanged
  2. Notification: notifications/prompts/list_changed
  3. 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.

Resources are designed to be **application-controlled**, meaning that the client application can decide how and when they should be used. Different MCP clients may handle resources differently. For example: - Claude Desktop currently requires users to explicitly select resources before they can be used - Other clients might automatically select resources based on heuristics - Some implementations may even allow the AI model itself to determine which resources to use

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:

text
[protocol]://[host]/[path]

For example:

  • file:///home/user/documents/report.pdf
  • postgres://database/customers/schema
  • screen://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:

typescript
{
  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:

typescript
{
  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:

typescript
{
  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)
    }
  ]
}
Servers may return multiple resources in response to one `resources/read` request. This could be used, for example, to return a list of files inside a directory when the directory is read.

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:

  1. Client sends resources/subscribe with resource URI
  2. Server sends notifications/resources/updated when the resource changes
  3. Client can fetch latest content with resources/read
  4. Client can unsubscribe with resources/unsubscribe

Example implementation

Here's a simple example of implementing resource support in an MCP server:

```typescript const server = new Server({ name: "example-server", version: "1.0.0" }, { capabilities: { resources: {} } });
text
// 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");
});
```
```python app = Server("example-server")
text
@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:

  1. Use clear, descriptive resource names and URIs
  2. Include helpful descriptions to guide LLM understanding
  3. Set appropriate MIME types when known
  4. Implement resource templates for dynamic content
  5. Use subscriptions for frequently changing resources
  6. Handle errors gracefully with clear error messages
  7. Consider pagination for large resource lists
  8. Cache resource contents when appropriate
  9. Validate URIs before processing
  10. 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:

text
file:///home/user/projects/myapp
https://api.example.com/v1

Why Use Roots?

Roots serve several important purposes:

  1. Guidance: They inform servers about relevant resources and locations
  2. Clarity: Roots make it clear which resources are part of your workspace
  3. Organization: Multiple roots let you work with different resources simultaneously

How Roots Work

When a client supports roots, it:

  1. Declares the roots capability during connection
  2. Provides a list of suggested roots to the server
  3. Notifies the server when roots change (if supported)

While roots are informational and not strictly enforcing, servers should:

  1. Respect the provided roots
  2. Use root URIs to locate and access resources
  3. 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:

  1. Only suggest necessary resources
  2. Use clear, descriptive names for roots
  3. Monitor root accessibility
  4. Handle root changes gracefully

Example

Here's how a typical MCP client might expose roots:

json
{
  "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:

  1. Server sends a sampling/createMessage request to the client
  2. Client reviews the request and can modify it
  3. Client samples from an LLM
  4. Client reviews the completion
  5. 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:

typescript
{
  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 text field
    • Image content with data (base64) and mimeType fields

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 costs
    • speedPriority: Importance of low latency response
    • intelligencePriority: 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 generate
  • stopSequences: Array of sequences that stop generation
  • metadata: Additional provider-specific parameters

Response format

The client returns a completion result:

typescript
{
  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:

json
{
  "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:

  1. Always provide clear, well-structured prompts
  2. Handle both text and image content appropriately
  3. Set reasonable token limits
  4. Include relevant context through includeContext
  5. Validate responses before using them
  6. Handle errors gracefully
  7. Consider rate limiting sampling requests
  8. Document expected sampling behavior
  9. Test with various model parameters
  10. 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.

Tools are designed to be **model-controlled**, meaning that tools are exposed from servers to clients with the intention of the AI model being able to automatically invoke them (with a human in the loop to grant approval).

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/list endpoint
  • Invocation: Tools are called using the tools/call endpoint, 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:

typescript
{
  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:

```typescript const server = new Server({ name: "example-server", version: "1.0.0" }, { capabilities: { tools: {} } });
text
// 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");
});
```
```python app = Server("example-server")
text
@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:

typescript
{
  name: "execute_command",
  inputSchema: {
    type: "object",
    properties: {
      command: { type: "string" },
      args: { type: "array", items: { type: "string" } }
    }
  }
}

API integrations

Tools that wrap external APIs:

typescript
{
  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:

typescript
{
  name: "analyze_csv",
  inputSchema: {
    type: "object",
    properties: {
      filepath: { type: "string" },
      operations: {
        type: "array",
        items: {
          enum: ["sum", "average", "count"]
        }
      }
    }
  }
}

Best practices

When implementing tools:

  1. Provide clear, descriptive names and descriptions
  2. Use detailed JSON Schema definitions for parameters
  3. Include examples in tool descriptions to demonstrate how the model should use them
  4. Implement proper error handling and validation
  5. Use progress reporting for long operations
  6. Keep tool operations focused and atomic
  7. Document expected return value structures
  8. Implement proper timeouts
  9. Consider rate limiting for resource-intensive operations
  10. 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:

  1. Clients can list available tools at any time
  2. Servers can notify clients when tools change using notifications/tools/list_changed
  3. Tools can be added or removed during runtime
  4. 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:

  1. Set isError to true in the result
  2. Include error details in the content array

Here's an example of proper error handling for tools:

```typescript try { // Tool operation const result = performOperation(); return { content: [ { type: "text", text: `Operation successful: ${result}` } ] }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Error: ${error.message}` } ] }; } ``` ```python try: # Tool operation result = perform_operation() return types.CallToolResult( content=[ types.TextContent( type="text", text=f"Operation successful: {result}" ) ] ) except Exception as error: return types.CallToolResult( isError=True, content=[ types.TextContent( type="text", text=f"Error: {str(error)}" ) ] ) ```

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

typescript
{
  jsonrpc: "2.0",
  id: number | string,
  method: string,
  params?: object
}

Responses

typescript
{
  jsonrpc: "2.0",
  id: number | string,
  result?: object,
  error?: {
    code: number,
    message: string,
    data?: unknown
  }
}

Notifications

typescript
{
  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
```typescript const server = new Server({ name: "example-server", version: "1.0.0" }, { capabilities: {} });
text
const transport = new StdioServerTransport();
await server.connect(transport);
```
```typescript const client = new Client({ name: "example-client", version: "1.0.0" }, { capabilities: {} });
text
const transport = new StdioClientTransport({
  command: "./server",
  args: ["--option", "value"]
});
await client.connect(transport);
```
```python app = Server("example-server")
text
async with stdio_server() as streams:
    await app.run(
        streams[0],
        streams[1],
        app.create_initialization_options()
    )
```
```python params = StdioServerParameters( command="./server", args=["--option", "value"] )
text
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
```typescript import express from "express";
text
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);
```
```typescript const client = new Client({ name: "example-client", version: "1.0.0" }, { capabilities: {} });
text
const transport = new SSEClientTransport(
  new URL("http://localhost:3000/sse")
);
await client.connect(transport);
```
```python from mcp.server.sse import SseServerTransport from starlette.applications import Starlette from starlette.routing import Route
text
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"]),
    ]
)
```
```python async with sse_client("http://localhost:8000/sse") as streams: async with ClientSession(streams[0], streams[1]) as session: await session.initialize() ```

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
```typescript interface Transport { // Start processing messages start(): Promise;
text
// 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;
}
```
Note that while MCP Servers are often implemented with asyncio, we recommend implementing low-level interfaces like transports with `anyio` for wider compatibility. ```python @contextmanager async def create_transport( read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception], write_stream: MemoryObjectSendStream[JSONRPCMessage] ): """ Transport interface for MCP.
text
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:

  1. Connection errors
  2. Message parsing errors
  3. Protocol errors
  4. Network timeouts
  5. Resource cleanup

Example error handling:

```typescript class ExampleTransport implements Transport { async start() { try { // Connection logic } catch (error) { this.onerror?.(new Error(`Failed to connect: ${error}`)); throw error; } }
text
async send(message: JSONRPCMessage) {
    try {
      // Sending logic
    } catch (error) {
      this.onerror?.(new Error(`Failed to send message: ${error}`));
      throw error;
    }
  }
}
```
Note that while MCP Servers are often implemented with asyncio, we recommend implementing low-level interfaces like transports with `anyio` for wider compatibility. ```python @contextmanager async def example_transport(scope: Scope, receive: Receive, send: Send): try: # Create streams for bidirectional communication read_stream_writer, read_stream = anyio.create_memory_object_stream(0) write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
text
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:

  1. Handle connection lifecycle properly
  2. Implement proper error handling
  3. Clean up resources on connection close
  4. Use appropriate timeouts
  5. Validate messages before sending
  6. Log transport events for debugging
  7. Implement reconnection logic when appropriate
  8. Handle backpressure in message queues
  9. Monitor connection health
  10. 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:

  1. Enable debug logging
  2. Monitor message flow
  3. Check connection states
  4. Validate message formats
  5. Test error scenarios
  6. Use network analysis tools
  7. Implement health checks
  8. Monitor resource usage
  9. Test edge cases
  10. Use proper error tracking