Skip to content

Commit 0657c3b

Browse files
fix(test-conformance): stop leaking the conformance server and hanging on stale ports (#2276)
1 parent 542d5c9 commit 0657c3b

7 files changed

Lines changed: 52 additions & 10 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/test-conformance': patch
3+
---
4+
5+
Fix the server conformance script leaking the test server process: the cleanup trap killed the npx wrapper while the actual server kept listening on port 3000, making later runs silently test stale code or hang forever in the readiness loop. The script now spawns the server directly with `node --import tsx`, refuses to start while the port is taken, and bounds each readiness probe; both test servers report `EADDRINUSE` with an actionable message, and the plain `test:conformance:client` script works again (`--suite core`, required since conformance 0.2.0-alpha.1).

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/conformance/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ npx @modelcontextprotocol/conformance server \
6060
--scenario server-initialize
6161
```
6262

63+
Note: `pnpm run test:conformance:server` always starts (and tests) its own server, and refuses to run while anything is still listening on its port. Stop the manually started server first, or keep using the direct `--url` invocation above against it.
64+
6365
## Files
6466

6567
- `src/everythingClient.ts` - Client that handles all client conformance scenarios

test/conformance/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@
2828
"start": "npm run server",
2929
"server": "tsx watch --clear-screen=false scripts/cli.ts server",
3030
"client": "tsx scripts/cli.ts client",
31-
"test:conformance:client": "conformance client --command 'npx tsx ./src/everythingClient.ts' --expected-failures ./expected-failures.yaml",
32-
"test:conformance:client:all": "conformance client --command 'npx tsx ./src/everythingClient.ts' --suite all --expected-failures ./expected-failures.yaml",
33-
"test:conformance:client:run": "npx tsx ./src/everythingClient.ts",
31+
"test:conformance:client": "conformance client --command 'node --import tsx ./src/everythingClient.ts' --suite core --expected-failures ./expected-failures.yaml",
32+
"test:conformance:client:all": "conformance client --command 'node --import tsx ./src/everythingClient.ts' --suite all --expected-failures ./expected-failures.yaml",
33+
"test:conformance:client:run": "node --import tsx ./src/everythingClient.ts",
3434
"test:conformance:server": "scripts/run-server-conformance.sh --expected-failures ./expected-failures.yaml",
3535
"test:conformance:server:draft": "scripts/run-server-conformance.sh --suite draft --expected-failures ./expected-failures.yaml",
3636
"test:conformance:server:all": "scripts/run-server-conformance.sh --suite all --expected-failures ./expected-failures.yaml",
37-
"test:conformance:server:run": "npx tsx ./src/everythingServer.ts",
37+
"test:conformance:server:run": "node --import tsx ./src/everythingServer.ts",
3838
"test:conformance:all": "pnpm run test:conformance:client:all && pnpm run test:conformance:server:all"
3939
},
4040
"devDependencies": {
@@ -50,6 +50,7 @@
5050
"@modelcontextprotocol/test-helpers": "workspace:^",
5151
"cors": "catalog:runtimeServerOnly",
5252
"express": "catalog:runtimeServerOnly",
53+
"tsx": "catalog:devTools",
5354
"zod": "catalog:runtimeShared"
5455
}
5556
}

test/conformance/scripts/run-server-conformance.sh

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,21 @@ SERVER_URL="http://localhost:${PORT}/mcp"
1111
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1212
cd "$SCRIPT_DIR/.."
1313

14-
# Start the server in the background
14+
# Refuse to start if something is already listening on the port. The readiness
15+
# check below cannot tell our server apart from a stale one, so a leftover
16+
# listener would mean silently running conformance against old code — or
17+
# hanging forever if the listener never responds.
18+
if (: > "/dev/tcp/localhost/${PORT}") 2>/dev/null; then
19+
echo "Error: port ${PORT} is already in use."
20+
echo "Stop the stale process first (lsof -ti:${PORT} -sTCP:LISTEN | xargs kill) or set PORT to a free port."
21+
exit 1
22+
fi
23+
24+
# Start the server in the background. Use `node --import tsx` rather than
25+
# `npx tsx` so SERVER_PID is the server process itself — killing an npx/tsx
26+
# wrapper leaves the actual server running and squatting the port.
1527
echo "Starting conformance test server on port ${PORT}..."
16-
npx tsx ./src/everythingServer.ts &
28+
node --import tsx ./src/everythingServer.ts &
1729
SERVER_PID=$!
1830

1931
# Function to cleanup on exit
@@ -24,11 +36,16 @@ cleanup() {
2436
}
2537
trap cleanup EXIT
2638

27-
# Wait for server to be ready
39+
# Wait for server to be ready. --max-time keeps a hung listener from wedging
40+
# the loop forever, and a dead server process fails fast instead of retrying.
2841
echo "Waiting for server to be ready..."
2942
MAX_RETRIES=30
3043
RETRY_COUNT=0
31-
while ! curl -s "${SERVER_URL}" > /dev/null 2>&1; do
44+
while ! curl -s --max-time 2 "${SERVER_URL}" > /dev/null 2>&1; do
45+
if ! kill -0 $SERVER_PID 2>/dev/null; then
46+
echo "Server process exited unexpectedly"
47+
exit 1
48+
fi
3249
RETRY_COUNT=$((RETRY_COUNT + 1))
3350
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
3451
echo "Server failed to start after ${MAX_RETRIES} attempts"

test/conformance/src/authTestServer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,19 @@ async function startServer() {
412412
});
413413
414414
// Start server
415-
app.listen(PORT, () => {
415+
const httpServer = app.listen(PORT, () => {
416416
console.log(`MCP Auth Test Server running at http://localhost:${PORT}/mcp`);
417417
console.log(` - PRM endpoint: http://localhost:${PORT}/.well-known/oauth-protected-resource`);
418418
console.log(` - Auth server: ${AUTH_SERVER_URL}`);
419419
console.log(` - Introspection: ${asMetadata.introspection_endpoint}`);
420420
});
421+
httpServer.on('error', (error: NodeJS.ErrnoException) => {
422+
if (error.code !== 'EADDRINUSE') {
423+
throw error;
424+
}
425+
console.error(`Port ${PORT} is already in use is a stale auth test server still running?`);
426+
process.exit(1);
427+
});
421428
}
422429

423430
// Start the server

test/conformance/src/everythingServer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,14 @@ app.delete('/mcp', async (req: Request, res: Response) => {
10201020

10211021
// Start server
10221022
const PORT = process.env.PORT || 3000;
1023-
app.listen(PORT, () => {
1023+
const httpServer = app.listen(PORT, () => {
10241024
console.log(`MCP Conformance Test Server running on http://localhost:${PORT}`);
10251025
console.log(` - MCP endpoint: http://localhost:${PORT}/mcp`);
10261026
});
1027+
httpServer.on('error', (error: NodeJS.ErrnoException) => {
1028+
if (error.code !== 'EADDRINUSE') {
1029+
throw error;
1030+
}
1031+
console.error(`Port ${PORT} is already in use — is a stale conformance server still running?`);
1032+
process.exit(1);
1033+
});

0 commit comments

Comments
 (0)