Skip to content

Servers that are programs

Most MCP servers are not endpoints. They are programs a host starts, talks to over a pipe, and is responsible for stopping. Point scout at one with --stdio and the command after --:

scout check --stdio -- npx -y @modelcontextprotocol/server-everything stdio
scout check --stdio -- uvx mcp-server-git --repository .
scout check --stdio --stdio-env GITHUB_TOKEN -- docker run -i --rm ghcr.io/example/mcp

scout starts the process, runs the same phases against it, and stops it again — politely first, by closing its stdin, then by force if it ignores that. The process is reaped on every path out, including a cancelled run and a panic in a phase. A diagnostic that leaves a server running has done damage no report undoes.

Everything after -- belongs to the server. That is what lets a server have its own flags without scout guessing whose they are, and it is also why there is no --stdio-command "npx -y thing" form: splitting a command string means quoting rules, and quoting rules mean a shell. There is no shell here. The command is executed as named, with the arguments as given.

check, connect, tools and call all take --stdio. For call, which has two operands, the tool comes before -- and the server after it:

scout call --stdio get-sum --arg a=2 --arg b=3 -- npx -y @modelcontextprotocol/server-everything stdio

What the server is handed

A stdio server is a program nobody has audited, running with your user's privileges, on the machine where your credentials live. So it is given a fixed base environment and nothing else:

PATH HOME TMPDIR TEMP TMP LANG LC_ALL SystemRoot COMSPEC PATHEXT

That is enough for a program to find its interpreter and its libraries. It does not include your cloud credentials, your tokens for three other services, or the CI secrets that happen to be exported in the shell you typed the command into. This is a deliberate departure from how exec works everywhere else, where a child inherits everything.

A server that legitimately needs a credential is given it by name:

Flag Effect
--stdio-env NAME forward NAME from your environment (repeatable)
--stdio-set NAME=value set the child's environment to exactly this (repeatable, replaces the base)
--stdio-dir PATH run the server in this working directory

Every run reports what it passed, as stdio.environment, because a server that behaves differently under scout than in your own shell almost always differs here.

Credentials

There are none. A pipe carries no bearer token, there is no origin to authorize against, and no authorization server to discover. --token, --auth client-credentials and the rest are refused rather than ignored: silently dropping a credential you passed would produce a report that looks like a test of an authenticated server and is not.

What a stdio server needs instead goes in its arguments or in its environment, which is why --stdio-env exists.

Which checks apply

Seven of the nine phases run unchanged: the handshake, the protocol probes that are about JSON-RPC, the catalog, execution, performance. Two have no subject over a pipe and are reported as skipped, with the reason:

Phase Why it is skipped
discovery a child process has no origin and no metadata to discover; the trust decision was made when you chose which program to run
auth there is nothing to authenticate to, so there is also no wrong credential to send and no refusal to check

Four conformance probes inside the protocol phase are about the HTTP binding rather than about MCP, and are likewise skipped by name: protocol.accept_header, protocol.get_stream, protocol.bogus_session and protocol.version_header. So is handshake.session: over a pipe the connection is the session, so there is no session id a correct server would issue.

None of these is left out of the report. A run that silently contained fewer checks would read as a better result than it is.

Which checks only exist here

Five checks have no HTTP equivalent, and they are the ones that catch the failures specific to this transport.

Check What it looks for
stdio.process the program started and is still running before the first request
stdio.environment what scout handed the child, as a statement rather than a judgement
stdio.alive it was still running when the run finished
stdio.stdout_clean nothing but JSON-RPC messages on stdout
stdio.stderr what it logged, kept and reported

stdio.stdout_clean is the one that earns its place. Over stdio, stdout is the wire: every byte on it is parsed as protocol framing. One startup banner, one print left in a handler, one progress bar, and the stream is corrupt — and what a host reports is a hang, or a parse error naming a line nobody wrote. It never names the cause. scout does, and quotes the line.

Logging to stderr, by contrast, is correct behaviour. scout keeps it, reports it as stdio.stderr, and puts it in the evidence of any finding about a server that died, because a stdio server that fails says why there and nowhere else.

Detecting the protocol generation

Over HTTP, scout tells the two generations of the protocol apart using a status code: the 2026-07-28 revision answers an unimplemented RPC with 404 carrying -32601, while a handshake-era server answers the same -32601 at 200. A pipe has no status code, so that distinction is not available.

What scout does instead is narrower and it says so: a server that answers server/discover speaks the stateless revision; anything else is treated as handshake-era and the handshake that follows settles it. The one case this cannot name is a stateless server that omits the optional server/discover, and the finding's reason says exactly that rather than guessing.

Timeouts do not kill the server

A call that times out is a finding about that call. Over a pipe it would be easy to make it fatal — the read is blocked on a descriptor only the process can release — and the first version of the transport did exactly that. It meant one slow tool ended the whole run, while the same timeout over HTTP costs a single finding. It does not any more: the connection has one reader that matches replies to requests by id, so an abandoned call is abandoned and the next one is unaffected. The late reply is discarded rather than handed to whoever asked next.

That reader is also what lets the performance phase mean anything here: the parallel burst runs in parallel, instead of queueing behind one lock and measuring scout.

The browser will not do this

scout serve refuses a run that names a program, and says so, unless it was started with --allow-stdio. The engine can do it and the CLI does, but "diagnose the URL in this field" and "run this command on the machine scout is running on" are not the same permission — and the token in the page's URL is not a credential anybody should be able to trade for the second. In --public mode it is refused whatever the flags say.