diff --git a/scripts/hooks/mcp-health-check.js b/scripts/hooks/mcp-health-check.js index d6e728324..b78b5ac57 100644 --- a/scripts/hooks/mcp-health-check.js +++ b/scripts/hooks/mcp-health-check.js @@ -256,28 +256,19 @@ function detectFailureCode(text) { return null; } -function requestHttp(urlString, headers, timeoutMs, options = {}) { +function requestHttp(urlString, headers, timeoutMs) { return new Promise(resolve => { let settled = false; let timedOut = false; const url = new URL(urlString); const client = url.protocol === 'https:' ? https : http; - const method = options.method || 'GET'; - const body = options.body || null; - const requestHeaders = { ...headers }; - - if (body) { - requestHeaders['content-type'] = 'application/json'; - requestHeaders['content-length'] = Buffer.byteLength(body); - requestHeaders.accept = 'application/json, text/event-stream'; - } const req = client.request( url, { - method, - headers: requestHeaders, + method: 'GET', + headers, }, res => { if (settled) return; @@ -306,23 +297,7 @@ function requestHttp(urlString, headers, timeoutMs, options = {}) { }); }); - req.end(body || undefined); - }); -} - -// Some Streamable HTTP MCP servers (e.g. api.telnyx.com/v2/mcp) only route POST -// and answer any GET with 404, so a bare GET proves nothing. Replay the probe as -// a real JSON-RPC initialize before declaring the server unreachable. -function mcpInitializeBody() { - return JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'initialize', - params: { - protocolVersion: '2025-06-18', - capabilities: {}, - clientInfo: { name: 'ecc-mcp-health-check', version: '1' } - } + req.end(); }); } @@ -541,19 +516,7 @@ async function probeServer(serverName, resolvedConfig) { const config = resolvedConfig.config; if (config.type === 'http' || config.url) { - const timeoutMs = envNumber('ECC_MCP_HEALTH_TIMEOUT_MS', DEFAULT_TIMEOUT_MS); - let result = await requestHttp(config.url, config.headers || {}, timeoutMs); - - if (!result.ok) { - const posted = await requestHttp(config.url, config.headers || {}, timeoutMs, { - method: 'POST', - body: mcpInitializeBody() - }); - - if (posted.ok) { - result = posted; - } - } + const result = await requestHttp(config.url, config.headers || {}, envNumber('ECC_MCP_HEALTH_TIMEOUT_MS', DEFAULT_TIMEOUT_MS)); return { ok: result.ok, diff --git a/tests/hooks/mcp-health-check.test.js b/tests/hooks/mcp-health-check.test.js index 34205eb69..fe86290e4 100644 --- a/tests/hooks/mcp-health-check.test.js +++ b/tests/hooks/mcp-health-check.test.js @@ -1095,86 +1095,6 @@ async function runTests() { } })) passed++; else failed++; - if (await asyncTest('treats POST-only Streamable HTTP MCP servers that answer every GET with 404 as healthy', async () => { - const tempDir = createTempDir(); - const configPath = path.join(tempDir, 'claude.json'); - const statePath = path.join(tempDir, 'mcp-health.json'); - const serverScript = path.join(tempDir, 'http-post-only-server.js'); - const portFile = path.join(tempDir, 'server-port.txt'); - - fs.writeFileSync( - serverScript, - [ - "const fs = require('fs');", - "const http = require('http');", - "const portFile = process.argv[2];", - "const server = http.createServer((req, res) => {", - " if (req.method !== 'POST') {", - " res.writeHead(404, { 'Content-Type': 'application/json' });", - " res.end(JSON.stringify({ error: 'not found' }));", - " return;", - " }", - " let body = '';", - " req.on('data', chunk => { body += chunk; });", - " req.on('end', () => {", - " let parsed = null;", - " try { parsed = JSON.parse(body); } catch { parsed = null; }", - " if (!parsed || parsed.jsonrpc !== '2.0' || parsed.method !== 'initialize') {", - " res.writeHead(400, { 'Content-Type': 'application/json' });", - " res.end(JSON.stringify({ error: 'expected a JSON-RPC initialize body' }));", - " return;", - " }", - " res.writeHead(200, { 'Content-Type': 'application/json' });", - " res.end(JSON.stringify({ jsonrpc: '2.0', id: parsed.id, result: {} }));", - " });", - "});", - "server.listen(0, '127.0.0.1', () => {", - " fs.writeFileSync(portFile, String(server.address().port));", - "});", - "setInterval(() => {}, 1000);" - ].join('\n') - ); - - const serverProcess = spawn(process.execPath, [serverScript, portFile], { - stdio: 'ignore' - }); - - try { - const port = waitForFile(portFile).trim(); - await waitForHttpReady(`http://127.0.0.1:${port}/mcp`); - - writeConfig(configPath, { - mcpServers: { - postonly: { - type: 'http', - url: `http://127.0.0.1:${port}/mcp` - } - } - }); - - const input = { tool_name: 'mcp__postonly__list_api_endpoints', tool_input: {} }; - const result = runHook(input, { - CLAUDE_HOOK_EVENT_NAME: 'PreToolUse', - ECC_MCP_CONFIG_PATH: configPath, - ECC_MCP_HEALTH_STATE_PATH: statePath, - ECC_MCP_HEALTH_TIMEOUT_MS: '2000' - }); - - assert.strictEqual( - result.code, - 0, - `Expected POST-only MCP server to survive a 404 GET probe: ${hookFailureDetails(result, statePath)}` - ); - assert.strictEqual(result.stdout.trim(), JSON.stringify(input), 'Expected original JSON on stdout'); - - const state = readState(statePath); - assert.strictEqual(state.servers.postonly.status, 'healthy', 'Expected POST-only MCP server to be marked healthy'); - } finally { - serverProcess.kill('SIGTERM'); - cleanupTempDir(tempDir); - } - })) passed++; else failed++; - // Windows-only: child_process.spawn cannot resolve .cmd/.bat shims for // bare PATH commands without an extension, and Node 18.20+/20.12+ refuse // to spawn .cmd targets without `shell: true` (CVE-2024-27980). The probe