fix(test-conformance): stop leaking the conformance server and hanging on stale ports (#2276)

This commit is contained in:
Felix Weinberger 2026-06-11 17:04:54 +01:00 • committed by GitHub
commit 0657c3bea9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 52 additions and 10 deletions

View file

@ -60,6 +60,8 @@ npx @modelcontextprotocol/conformance server \
--scenario server-initialize
```
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.
## Files
- `src/everythingClient.ts` - Client that handles all client conformance scenarios

View file

@ -28,13 +28,13 @@
"start": "npm run server",
"server": "tsx watch --clear-screen=false scripts/cli.ts server",
"client": "tsx scripts/cli.ts client",
"test:conformance:client": "conformance client --command 'npx tsx ./src/everythingClient.ts' --expected-failures ./expected-failures.yaml",
"test:conformance:client:all": "conformance client --command 'npx tsx ./src/everythingClient.ts' --suite all --expected-failures ./expected-failures.yaml",
"test:conformance:client:run": "npx tsx ./src/everythingClient.ts",
"test:conformance:client": "conformance client --command 'node --import tsx ./src/everythingClient.ts' --suite core --expected-failures ./expected-failures.yaml",
"test:conformance:client:all": "conformance client --command 'node --import tsx ./src/everythingClient.ts' --suite all --expected-failures ./expected-failures.yaml",
"test:conformance:client:run": "node --import tsx ./src/everythingClient.ts",
"test:conformance:server": "scripts/run-server-conformance.sh --expected-failures ./expected-failures.yaml",
"test:conformance:server:draft": "scripts/run-server-conformance.sh --suite draft --expected-failures ./expected-failures.yaml",
"test:conformance:server:all": "scripts/run-server-conformance.sh --suite all --expected-failures ./expected-failures.yaml",
"test:conformance:server:run": "npx tsx ./src/everythingServer.ts",
"test:conformance:server:run": "node --import tsx ./src/everythingServer.ts",
"test:conformance:all": "pnpm run test:conformance:client:all && pnpm run test:conformance:server:all"
},
"devDependencies": {
@ -50,6 +50,7 @@
"@modelcontextprotocol/test-helpers": "workspace:^",
"cors": "catalog:runtimeServerOnly",
"express": "catalog:runtimeServerOnly",
"tsx": "catalog:devTools",
"zod": "catalog:runtimeShared"
}
}

View file

@ -11,9 +11,21 @@ SERVER_URL="http://localhost:${PORT}/mcp"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
# Start the server in the background
# Refuse to start if something is already listening on the port. The readiness
# check below cannot tell our server apart from a stale one, so a leftover
# listener would mean silently running conformance against old code — or
# hanging forever if the listener never responds.
if (: > "/dev/tcp/localhost/${PORT}") 2>/dev/null; then
echo "Error: port ${PORT} is already in use."
echo "Stop the stale process first (lsof -ti:${PORT} -sTCP:LISTEN | xargs kill) or set PORT to a free port."
exit 1
fi
# Start the server in the background. Use `node --import tsx` rather than
# `npx tsx` so SERVER_PID is the server process itself — killing an npx/tsx
# wrapper leaves the actual server running and squatting the port.
echo "Starting conformance test server on port ${PORT}..."
npx tsx ./src/everythingServer.ts &
node --import tsx ./src/everythingServer.ts &
SERVER_PID=$!
# Function to cleanup on exit
@ -24,11 +36,16 @@ cleanup() {
}
trap cleanup EXIT
# Wait for server to be ready
# Wait for server to be ready. --max-time keeps a hung listener from wedging
# the loop forever, and a dead server process fails fast instead of retrying.
echo "Waiting for server to be ready..."
MAX_RETRIES=30
RETRY_COUNT=0
while ! curl -s "${SERVER_URL}" > /dev/null 2>&1; do
while ! curl -s --max-time 2 "${SERVER_URL}" > /dev/null 2>&1; do
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo "Server process exited unexpectedly"
exit 1
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo "Server failed to start after ${MAX_RETRIES} attempts"

View file

@ -412,12 +412,19 @@ async function startServer() {
});
// Start server
app.listen(PORT, () => {
const httpServer = app.listen(PORT, () => {
console.log(`MCP Auth Test Server running at http://localhost:${PORT}/mcp`);
console.log(` - PRM endpoint: http://localhost:${PORT}/.well-known/oauth-protected-resource`);
console.log(` - Auth server: ${AUTH_SERVER_URL}`);
console.log(` - Introspection: ${asMetadata.introspection_endpoint}`);
});
httpServer.on('error', (error: NodeJS.ErrnoException) => {
if (error.code !== 'EADDRINUSE') {
throw error;
}
console.error(`Port ${PORT} is already in use — is a stale auth test server still running?`);
process.exit(1);
});
}
// Start the server

View file

@ -1020,7 +1020,14 @@ app.delete('/mcp', async (req: Request, res: Response) => {
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
const httpServer = app.listen(PORT, () => {
console.log(`MCP Conformance Test Server running on http://localhost:${PORT}`);
console.log(` - MCP endpoint: http://localhost:${PORT}/mcp`);
});
httpServer.on('error', (error: NodeJS.ErrnoException) => {
if (error.code !== 'EADDRINUSE') {
throw error;
}
console.error(`Port ${PORT} is already in use — is a stale conformance server still running?`);
process.exit(1);
});