Skip to content

fix(runner): bound server scenarios with a per-scenario timeout - #454

Open
aviseth wants to merge 1 commit into
modelcontextprotocol:mainfrom
aviseth:fix/server-runner-scenario-timeout
Open

fix(runner): bound server scenarios with a per-scenario timeout#454
aviseth wants to merge 1 commit into
modelcontextprotocol:mainfrom
aviseth:fix/server-runner-scenario-timeout

Conversation

@aviseth

@aviseth aviseth commented Aug 13, 2026

Copy link
Copy Markdown

Closes #427.

The client runner has bounded each scenario since it was written. The server runner never did, so a server that accepts the TCP connection and then stops answering stalled the whole run instead of failing its own scenario.

--timeout now works in server mode, same 30s default as client mode. When it fires, the scenario gets a scenario-timeout FAILURE naming the bound and the suite carries on.

The one decision I'd like a second opinion on: the timed-out scenario.run promise is left pending rather than cancelled. A read blocked on a socket has nothing to cancel through, so there is nothing to await — I swallow its rejection so a late failure can't land on whatever scenario is running by then. Threading an AbortSignal through RunContext would let scenarios actually tear down, but that touches every scenario and seemed like a separate change. Say the word and I'll do it that way instead.

Tested by pointing server-initialize at a server that completes the handshake and never writes a byte. With the runner change reverted the test hangs until vitest kills it at 30s. Full suite, typecheck and lint pass.

#316's per-call bounds still earn their keep — they fail faster and attribute the failure better. They're just no longer the only thing between a wedged server and an unbounded run.

The client runner has had a 30s per-scenario timeout since it was written,
but `runServerConformanceTest` awaited `scenario.run(ctx)` with no bound. A
server under test that accepts the TCP connection and then never answers an
initialize POST, a tools call, or an SSE read stalled the entire suite run,
not just its own scenario.

modelcontextprotocol#316 bounded the session-termination DELETEs in teardown, but that is a
call-site fix: covering the rest that way needs the same treatment on every
raw fetch in every scenario. Bounding at the runner makes those per-call
bounds defense-in-depth instead of the only protection.

`--timeout` now applies to server mode too, with the same 30s default as
client mode. On expiry the scenario reports a `scenario-timeout` FAILURE
naming the elapsed bound, so a wedged server reads as a failing scenario
rather than a hung process.

The timed-out scenario promise is left pending deliberately — a read blocked
on a socket has no cancellation channel — with its rejection swallowed so a
late failure can't surface against whichever scenario is running by then.

Fixes modelcontextprotocol#427
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server runner has no per-scenario timeout; a wedged server stalls the whole suite

1 participant