Skip to content

MCP inspector plugin

Use the MCP plugin to test an MCP server you build or operate. A flow connects to the server as one identity, reads its complete tool, resource, prompt and resource-template inventories, calls tools and asserts what came back, including JSON-RPC refusals. Tool inventories keep annotation hints and hashes of each tool’s input schema and description, so two runs show which tools were added, removed or changed.

This page is about the plugin that inspects your MCP server. To connect a coding agent to Reflow’s own MCP server, see MCP reference.

The MCP plugin is bundled with the public CLI. It is a single native executable and needs no Node or other runtime. It runs wherever the CLI runs (currently macOS on Apple silicon). Only OAuth sign-in also needs the Browser plugin.

  • The Reflow CLI installed and signed in to an approved team. See Get started.
  • For OAuth sign-in only: a reflow apply run on your device with the Browser plugin in the same flow.
  • A server that speaks MCP over Streamable HTTP, at an http or https endpoint (https for OAuth sign-in). Other transports are not supported (see limits).
  • The endpoint’s origin must be admitted by the host. The binding cannot widen network access: the plugin may reach only the reflow target URL’s origin and any origins you grant with --allow-origin. With OAuth, also grant the authorization server’s origin when it differs from the endpoint’s origin.
  • A test identity for the server: a bearer token, private headers such as an API key, an OAuth client, or none for a public server.

Select your application collection during onboarding. On an application’s first initialization, this installs all seven bundled plugins (Browser, LocalBash, PostgreSQL, DockerUbuntu, Mailbox, MCP and OpenAPI):

Terminal window
reflow init --collection <collection-id>

An application that already has a plugin selection keeps it, and naming a different set without --upgrade is refused. To add MCP to an existing selection, stop active application executions, then pass the complete desired set with --upgrade:

Terminal window
reflow init Browser LocalBash PostgreSQL DockerUbuntu Mailbox MCP OpenAPI --collection <collection-id> --upgrade

--upgrade also moves every other listed plugin to this CLI’s bundled release, so steps whose plugin changed run again. Check reflow plan before applying.

See one selection per application for how an upgrade applies across devices and branches. The examples on this page also use other plugins: OAuth sign-in needs Browser, and tokens minted during the run uses LocalBash. Keep them in the selection.

Save a private bindings.json. The flow alias api selects the connection with the same name:

{
"api": {
"provider": "MCP",
"configuration": {
"endpoint": "https://mcp.example.com/mcp",
"bearerToken": "replace-with-test-token",
"timeoutMs": 15000
}
}
}

Select the endpoint as the target, so its origin is admitted:

Terminal window
chmod 600 /absolute/path/to/bindings.json
reflow target https://mcp.example.com/mcp --group mcp-tests --collection <collection-id> --bindings /absolute/path/to/bindings.json

Add --allow-origin https://auth.example.com for each other origin the plugin must reach, such as an OAuth issuer on a different host. See auth redirects and navigation origins for how grants are kept and replaced.

FieldRequiredMeaning
endpointyesAbsolute HTTP or HTTPS Streamable HTTP endpoint, without user information or fragment.
bearerTokennoSent as Authorization: Bearer. Cannot be combined with oauth.
headersnoPrivate request headers as a JSON object string, for example an API-key or session header. Authorization must use the Bearer scheme; Cookie and other reserved headers are refused.
oauthnoPrivate OAuth settings as a JSON object string. See OAuth sign-in.
tokenDirectorynoPrivate directory for per-run tokens. Cannot be combined with oauth.
caFilenoAbsolute path to a PEM certificate authority added to system trust (up to 256 KiB).
sessionCleanupnodelete (default) or server-managed. See session cleanup.
timeoutMsnoDeadline in milliseconds for each request, including a streamed tool reply (default 15000, 100 to 600000).
maxResponseBytesnoLargest HTTP response and raw inventory (default and maximum 8 MiB).
maxPages, maxItemsnoInventory paging limits (defaults 50 and 5000).

Without any credential the plugin connects anonymously. Use that for a public server, or to assert that the server refuses an unauthenticated client with anon.connect expect="{\"status\":401}". Give that check its own alias and binding without credentials, such as anon: after a refused connection, later commands on the same alias are refused.

Endpoint URLs, header values, tokens and session IDs are redacted from snapshots, artifacts and text before it is hashed. Keep them in the private bindings file, never in a flow. Binary resource contents (blobs) cannot be redacted: they are hashed as received, so if a blob embeds a private value, its hash can confirm a guess of that value.

Copy this into a private draft outside your checkout, such as /tmp/mcp-tools.md. Replace the tool names and arguments with your server’s, and point it only at a test server: api.call really calls the tool.

---
name: Inspect the support MCP server
providers:
api: MCP
---
### Check what this identity can use
```rfl
api.connect
api.tools expect="{\"present\":[\"search_tickets\"],\"absent\":[\"delete_workspace\"],\"noTrueAnnotations\":[\"destructive\"]}"
api.call tool="search_tickets" arguments="{\"query\":\"refund\"}" expect="{\"isError\":false}"
api.call tool="delete_workspace" arguments="{}" expect="{\"rpcError\":-32602}"
api.disconnect
```

The last call asserts that the server refuses a tool this identity must not use. Use the code your server actually returns: -32602 (invalid params) is common for an unknown tool, but some servers use another code, and many return a tool result with isError: true instead, which you assert with expect="{\"isError\":true}". If the identity might be allowed to call the tool, the call runs it, so assert refusals only with tools that are harmless on your test server.

Create the test in the team’s chosen location, then run it:

Terminal window
reflow flows create --name mcp-tools --file /tmp/mcp-tools.md
reflow plan mcp-tools
reflow apply mcp-tools
reflow dashboard

The Effect column is the command’s declared effect. A mutate command may change the server, so a reply that was lost or ambiguous is recorded as unknown and is never replayed automatically.

CommandEffectWhat it does
api.connectobserveInitialize a session, or assert the HTTP status of a refused initialization with the status expectation.
api.toolsobserveList every tool with annotation hints and schema and description hashes.
api.resourcesobserveList every resource and assert its URI set.
api.resource_templatesobserveList every resource template and assert its URI template set.
api.promptsobserveList every prompt with its argument names.
api.read_resourceobserveRead one resource (uri) and assert its text or a JSON-RPC refusal.
api.get_promptobserveRender one prompt (name, optional arguments) and assert its messages or a refusal.
api.callmutateCall one tool (tool, optional arguments) once and assert its result or a refusal.
api.call_valuemutateCall once and capture a public scalar selected by an RFC 6901 JSON Pointer.
api.serversmutateCall a gateway’s list_servers tool and assert the exact server slug set.
api.describe_servermutateCall a gateway’s describe_server tool and assert one member server’s tool names.
api.describe_toolsmutateCall a gateway’s describe_tools tool and assert the exact tool name set.
api.disconnectmutateEnd the session with at most one DELETE and record the outcome.
api.oauth_beginmutateStart one private OAuth attempt and open the consent page in the host’s Browser.
api.oauth_finishmutateWait for consent, exchange the code once and install the token in this binding.

arguments and expect are JSON object strings, so quotes inside them are escaped. Both may contain ${name} references to string inputs and captures, but only inside a JSON string value, where the value is inserted as escaped text; see references in JSON arguments. Every command except oauth_begin and oauth_finish also accepts path and tokenFile (below); oauth_begin accepts path, and oauth_finish takes no arguments. Use list_plugins or the application’s Plugins page to see the exact argument schema of the release your application selected.

api.servers, api.describe_server and api.describe_tools are for MCP gateways: servers that front several member servers and expose tools named list_servers, describe_server and describe_tools. They call that tool (or the one named by tool) and read the returned names, from structured content or JSON text. Pass the member server for describe_server in arguments, for example {"server":"billing"}. Skip them for an ordinary MCP server.

Calls and the gateway helpers always count as mutations, whatever annotations a tool declares. Annotations describe a tool; they do not restrict what a call does.

expect accepts only the keys below; any other key is refused.

CommandsKeys
tools, resources, resource_templates, promptspresent, absent, only (exact set), count; tools also noTrueAnnotations (readOnly, destructive, idempotent, openWorld)
callrpcError (JSON-RPC error code), isError (tool result flag), textIncludes, textExcludes, redacted, json
call_valuetextIncludes, textExcludes, redacted, json; also requires a non-error result and a valid scalar selection
servers, describe_server, describe_toolspresent, absent, only, plus the call keys
read_resource, get_promptrpcError, textIncludes, textExcludes, redacted, json, count
connectstatus (HTTP status, 100-599)

rpcError matches a JSON-RPC refusal from the server, which is different from a tool result with isError: true. Without expect, call and the gateway helpers succeed only on a tool result with isError: false: a result with isError: true or any JSON-RPC refusal fails the step. textIncludes searches the complete redacted text before truncation. An inventory that could not be read completely never satisfies an expectation.

textExcludes searches every decoded string in the complete result or error, including structured content, metadata and embedded resources, before truncation. It accepts 1–64 exact, case-sensitive strings. Binding private values have already been redacted, so use "redacted": false to assert that no binding secret appeared. Pair absence checks with "isError": false when you need a successful tool result. Operands containing private values or terminal controls are refused before contact.

Gateway present and absent check the complete extracted name set. Unreadable names, protocol errors, tool errors and failed member servers fail gateway_names_unavailable; they cannot satisfy absence by returning no names. Repeated or uncheckable JSON members fail closed for these reply checks.

For exact JSON assertions and capture examples, see MCP JSON captures and assertions.

connect accepts public routing headers as a JSON object string:

api.connect headers="{\"X-Region\":\"eu\",\"X_Tenant\":\"test-tenant\"}"

Put it before other session commands. Headers are sent on every MCP request in this instance, including its GET stream and DELETE, and never to OAuth servers. Reserved protocol or credential headers, private binding header names and private values are refused. At most 32 names and 32,768 characters are accepted; each value is 1–16,384 characters. Go canonicalizes names (x_tenant becomes X_tenant). Evidence records sorted names, without values. Use private binding headers for credentials.

The binding’s timeoutMs defaults to 15,000. For a 75-second call, select a binding with timeoutMs: 120000 and give the flow enough overall time, for example budgets.minutes: 15. Keepalives do not extend the deadline. There is no per-call timeout argument; a lost or ambiguous reply is never retried.

Each step records a snapshot with status succeeded, failed or unknown, and an observation.json artifact with the same state.

  • Inventories are sorted by name, URI or URI template. A tool record keeps its annotation hints (a missing hint is null, never false) and SHA-256 hashes of its input schema and description, not their text.
  • A call records one of: a tool result (kind: "result" with isError, content types, redacted text truncated to 16,384 characters, a hash of structured content and failed expectations), a JSON-RPC refusal (kind: "rpc-error" with the code and redacted message), a confirmed reply that could not be observed safely (kind: "unavailable"), or a lost or ambiguous reply (kind: "unknown").
  • An unknown call may have run, and the plugin never retries it. A lost or ambiguous reply to any request, or an invalid inventory page, closes the connection: later inspection commands in that binding fail, but api.disconnect still sends its one session DELETE. A single reply with invalid content to call, read_resource or get_prompt fails only that step.

Comparing two successful tools snapshots reports added, removed and changed tools, with the changed fields (annotation hints, schema hash, description hash, and whether private values were redacted), and retains a tool-diff.json artifact. See snapshots and review.

When an earlier step creates the credential, such as an API key for a test agent, the token must not travel as a flow value: captures and command arguments are retained evidence. Instead:

  1. Set tokenDirectory in the binding to an absolute directory with mode 0700 or stricter, owned by the user that runs Reflow. Dedicate it to run tokens: a flow may name any file in it. The example below keeps it inside the LocalBash working directory, created once with mkdir -m 700 .reflow-run-tokens and listed in .gitignore.
  2. Have an earlier step write the token to a 0600 file there without printing it.
  3. Pass only the file name as tokenFile.

The bindings give the minting step a LocalBash working directory and the MCP connection its token directory:

{
"shell": {
"provider": "LocalBash",
"configuration": {
"cwd": "/absolute/path/to/your/app"
}
},
"api": {
"provider": "MCP",
"configuration": {
"endpoint": "https://mcp.example.com/mcp",
"tokenDirectory": "/absolute/path/to/your/app/.reflow-run-tokens"
}
}
}
---
name: Inspect with a fresh agent key
providers:
shell: LocalBash
api: MCP
---
### Mint a key and inspect
```rfl
shell.bash command="umask 077 && ./scripts/mint-test-agent-key > .reflow-run-tokens/agent-key"
api.connect tokenFile="agent-key"
api.tools expect="{\"only\":[\"check_health\"]}"
api.disconnect
```

The flow names the file relative to the LocalBash working directory, so no machine-specific path is written into it. Reflow redacts binding values, including both absolute paths, from run evidence, but a flow is stored and shared as written: keep absolute paths in the private bindings file.

The plugin reads the file once, before contacting the server, and never records the token. The token takes precedence over the binding’s bearerToken or Authorization header. The file name must be 1 to 128 letters, digits, dots, underscores or hyphens. The file must be a regular file owned by you with mode 0600 or stricter and a single link, at most 16 KiB, and hold one printable token of at least 8 characters. Name tokenFile on the alias’s first command: that command fixes the file name, so naming one only later, or a different one, is refused. The writing step and the plugin must run on the same device as the same user. If the writing step is reused instead of run, the file is not written again: rerun that step.

For servers created during a run, bind their common endpoint prefix and pass each server’s slug as path on the alias’s first command, for example api.connect path="support". That command fixes the path for the alias; a different path later is refused. path is refused when the bound endpoint has a query string. Evidence does not depend on the path, so per-run slugs compare equal.

api.disconnect sends at most one session DELETE and records its outcome; it sends none when the server never issued a session. Only a successful response confirms cleanup; a lost or refused DELETE is reported as unconfirmed. The DELETE gets at most 1.5 seconds whatever timeoutMs is, so a slow server’s cleanup is reported as unconfirmed. After api.disconnect, later inspection commands on that alias are refused; another api.disconnect sends nothing more and reports the recorded outcome. Some servers refuse client termination with 405 and expire sessions themselves. For those, set sessionCleanup: "server-managed" so a 405 counts as no cleanup needed, and end the flow with api.disconnect to record it.

With an oauth binding, a flow signs in through the authorization server’s consent page in the host’s Browser. The flow never receives the token, the authorization code or the authorization URL.

OAuth sign-in needs:

  • A reflow apply run on your device, which hosts the private consent channel between the MCP plugin and the Browser plugin.
  • A Browser alias in the same flow, using Chromium (required for OAuth consent).
  • An HTTPS MCP endpoint, selected with reflow target, and the authorization server’s origin granted with --allow-origin when it differs from the endpoint’s origin.
  • A flow that runs in full. A flow that would be partly reused and partly rerun is refused; invalidate the whole flow and plan it again.
{
"api": {
"provider": "MCP",
"configuration": {
"endpoint": "https://mcp.example.com/mcp",
"oauth": "{\"issuer\":\"https://auth.example.com\",\"scopes\":[\"tools.read\"]}"
}
},
"site": {
"provider": "Browser",
"configuration": {
"browser": "chromium"
}
}
}

The documented flow shape is oauth_begin, then your Browser steps that complete consent, then oauth_finish. Between the two, only Browser commands written into the flow and oauth_finish may run: prose steps for the agent and scripts are refused, and inspection commands are refused until oauth_finish succeeds. Adapt the Browser steps to your consent page:

providers:
api: MCP
site: Browser
api.oauth_begin
site.click selector="[data-testid=approve]"
api.oauth_finish
api.tools expect="{\"present\":[\"search_tickets\"]}"
  • issuer is the trusted HTTPS issuer that the server’s protected-resource metadata must list, or endpoint for a server that is its own authorization server. Discovery reads protected-resource metadata (RFC 9728), then authorization-server metadata (RFC 8414 or OpenID Connect).
  • Without clientId, the plugin registers a public client dynamically. With clientId, also set the exact registered callbackPath (a path of letters, digits, /, _ and -, not /done). The callback is http://127.0.0.1:<random port><callbackPath>, so register a loopback redirect that accepts any port on 127.0.0.1.
  • The authorization server’s metadata must support PKCE S256 and response type code, allow the authorization_code grant when it lists grant types, and list none in token_endpoint_auth_methods_supported. Consent has consentTimeoutMs (default 120000).
  • oauth_begin accepts the optional path; oauth_finish takes no arguments.
  • Dynamically registered clients remain at the issuer: the plugin does not deregister them. Tokens are not refreshed or revoked.

Reflow’s OAuth tests run against local fixture servers. Treat OAuth with your own authorization server as early support and check its evidence closely.

  • Streamable HTTP only. Sessions start with protocol 2025-11-25 and accept 2025-06-18, 2025-03-26, 2024-11-05 and 2024-10-07. Server-sent events in replies are handled; the plugin answers server ping and refuses other server requests with -32601.
  • Not supported: launching stdio servers, the legacy HTTP+SSE transport, resuming a lost stream, automatic reconnection, resource subscriptions, token refresh or revocation, and OAuth client deregistration.
  • timeoutMs is an absolute deadline per request; keepalive comments do not extend it. OAuth requests use at most 60 seconds.
  • Snapshot state and each JSON artifact are limited to 7 MiB.