From e363c540577d13c008cd039b72dd2245da954f4a Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Wed, 8 Apr 2026 15:34:34 -0700 Subject: [PATCH] fix: treat oauth mcp 401 probes as reachable --- scripts/hooks/mcp-health-check.js | 5 ++- tests/hooks/mcp-health-check.test.js | 62 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/scripts/hooks/mcp-health-check.js b/scripts/hooks/mcp-health-check.js index ec425148..4c40e114 100644 --- a/scripts/hooks/mcp-health-check.js +++ b/scripts/hooks/mcp-health-check.js @@ -24,7 +24,10 @@ const DEFAULT_TTL_MS = 2 * 60 * 1000; const DEFAULT_TIMEOUT_MS = 5000; const DEFAULT_BACKOFF_MS = 30 * 1000; const MAX_BACKOFF_MS = 10 * 60 * 1000; -const HEALTHY_HTTP_CODES = new Set([200, 201, 202, 204, 301, 302, 303, 304, 307, 308, 400, 405]); +// The preflight HTTP probe only checks reachability; it does not have access to +// Claude Code's stored OAuth bearer token. Treat auth-gated responses as +// reachable so the real MCP client can attempt the authenticated call. +const HEALTHY_HTTP_CODES = new Set([200, 201, 202, 204, 301, 302, 303, 304, 307, 308, 400, 401, 403, 405]); const RECONNECT_STATUS_CODES = new Set([401, 403, 429, 503]); const FAILURE_PATTERNS = [ { code: 401, pattern: /\b401\b|unauthori[sz]ed|auth(?:entication)?\s+(?:failed|expired|invalid)/i }, diff --git a/tests/hooks/mcp-health-check.test.js b/tests/hooks/mcp-health-check.test.js index 28650b27..04d4a7b5 100644 --- a/tests/hooks/mcp-health-check.test.js +++ b/tests/hooks/mcp-health-check.test.js @@ -358,6 +358,68 @@ async function runTests() { } })) passed++; else failed++; + if (await asyncTest('treats HTTP 401 probe responses as healthy reachable OAuth-protected servers', 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-401-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) => {", + " res.writeHead(401, {", + " 'Content-Type': 'application/json',", + " 'WWW-Authenticate': 'Bearer realm=\"OAuth\", error=\"invalid_token\"'", + " });", + " res.end(JSON.stringify({ error: 'missing bearer token' }));", + "});", + "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(); + + writeConfig(configPath, { + mcpServers: { + atlassian: { + type: 'http', + url: `http://127.0.0.1:${port}/mcp` + } + } + }); + + const input = { tool_name: 'mcp__atlassian__search', 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: '500' + }); + + assert.strictEqual(result.code, 0, `Expected HTTP 401 probe to be treated as healthy, got ${result.code}`); + assert.strictEqual(result.stdout.trim(), JSON.stringify(input), 'Expected original JSON on stdout'); + + const state = readState(statePath); + assert.strictEqual(state.servers.atlassian.status, 'healthy', 'Expected OAuth-protected HTTP MCP server to be marked healthy'); + } finally { + serverProcess.kill('SIGTERM'); + cleanupTempDir(tempDir); + } + })) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }