@onyxsecurity/mcp-gateway 2.1.38 → 2.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/bin/mcp-gateway.js +2 -2
  2. package/dist/{cli-BBy7iLqP.js → cli-CAJkf_BE.js} +2 -2
  3. package/dist/cli-CAJkf_BE.js.map +1 -0
  4. package/dist/env-DW7bNp9e.js +2 -0
  5. package/dist/env-DW7bNp9e.js.map +1 -0
  6. package/dist/env-HJmBKke9.js +1 -0
  7. package/dist/{helpers-BrpBfS5F.js → helpers-BLNSu673.js} +4 -4
  8. package/dist/{helpers-BrpBfS5F.js.map → helpers-BLNSu673.js.map} +1 -1
  9. package/dist/index.d.ts +4 -9
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/logger-AanoN79Y.js +5 -0
  13. package/dist/{logger-DXekMN0U.js.map → logger-AanoN79Y.js.map} +1 -1
  14. package/dist/main-C1Tyuk4v.js +6 -0
  15. package/dist/main-C1Tyuk4v.js.map +1 -0
  16. package/dist/normalizeUrl-DoppHGlo.js +64 -0
  17. package/dist/normalizeUrl-DoppHGlo.js.map +1 -0
  18. package/dist/oauthProvidersClient-BSKOBEK-.js +1 -0
  19. package/dist/{oauthProvidersClient-DJboK7fo.js → oauthProvidersClient-DHglUqos.js} +2 -2
  20. package/dist/{oauthProvidersClient-DJboK7fo.js.map → oauthProvidersClient-DHglUqos.js.map} +1 -1
  21. package/dist/{providers-DNHlTIxY.js → providers-DqYBslU6.js} +2 -2
  22. package/dist/{providers-DNHlTIxY.js.map → providers-DqYBslU6.js.map} +1 -1
  23. package/dist/transportDetection-UvCkhuUq.js +136 -0
  24. package/dist/transportDetection-UvCkhuUq.js.map +1 -0
  25. package/dist/upstreamNegotiation-CKlqaA_j.js +15 -0
  26. package/dist/upstreamNegotiation-CKlqaA_j.js.map +1 -0
  27. package/package.json +5 -2
  28. package/dist/cli-BBy7iLqP.js.map +0 -1
  29. package/dist/env-BLJVAhvA.js +0 -2
  30. package/dist/env-BLJVAhvA.js.map +0 -1
  31. package/dist/env-lGhkAtBF.js +0 -1
  32. package/dist/logger-DXekMN0U.js +0 -5
  33. package/dist/main-C1plRSYG.js +0 -6
  34. package/dist/main-C1plRSYG.js.map +0 -1
  35. package/dist/normalizeUrl-BrUSzVK4.js +0 -31
  36. package/dist/normalizeUrl-BrUSzVK4.js.map +0 -1
  37. package/dist/oauthProvidersClient-DKPy2QIM.js +0 -1
  38. package/dist/stdio-Cl6hsJCN.js +0 -31
  39. package/dist/stdio-Cl6hsJCN.js.map +0 -1
  40. package/dist/transportDetection-BGzD_A9t.js +0 -136
  41. package/dist/transportDetection-BGzD_A9t.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main-C1Tyuk4v.js","names":["config","config","credentials: StoredCredentials","bearer","refreshInFlight: null | Promise<string | undefined>","parsed: URL","msg: { id?: unknown }","reconnecting: null | Promise<boolean>","notificationHandlers: SetNotificationHandlerArgs[]","jsonLines: string[]","spawnedAt: number | undefined","spawnedPid: number | undefined","messageType: string","result: string","parsedResult: unknown","repoDigests: unknown[]","digests: string[]","activeStdioTransport: StdioClientTransport | undefined","yargs","proxyType: ProxyType","finalCommand: string | undefined","finalArgs: string[] | undefined","remoteUrl: string | undefined","usedTransport: TransportType | undefined","remoteHeaders: Record<string, string>","customEnv: Record<string, string>","missingVar: string | undefined","val","reason","containerMCPCommand: ContainerMCPCommand | undefined","containerImageDigests: string[] | undefined","sessionDataOnce: ReturnType<typeof getSessionData> | undefined","accessControlClient: AccessControlClient | undefined","recentStderr: string[]","headers: Record<string, string>","connectedClient: Client","dynAuth: DynamicAuthHandle","discoveryError: string | undefined","trafficMirrorInit: Promise<unknown> | undefined","upstream: undefined | UpstreamClient","connected","serverVersion","serverCapabilities","server","upstream"],"sources":["../node_modules/eventsource/dist/index.js","../src/accessControl/authorizer.ts","../src/accessControl/client.ts","../src/auth/OAuthCredentialStore.ts","../src/auth/dynamicAuth.ts","../src/lib/errorMessage.ts","../src/lib/protocolEra.ts","../src/utils/redactUrl.ts","../src/lib/initializeErrorResponder.ts","../src/lib/upstreamErrors.ts","../src/lib/connectWithRetry.ts","../src/lib/reconnectingClient.ts","../src/JSONFilterTransform.ts","../src/StdioClientTransport.ts","../src/utils/imageReference.ts","../src/utils/dockerImageDigest.ts","../src/bin/main.ts"],"sourcesContent":["import { createParser } from \"eventsource-parser\";\nclass ErrorEvent extends Event {\n /**\n * Constructs a new `ErrorEvent` instance. This is typically not called directly,\n * but rather emitted by the `EventSource` object when an error occurs.\n *\n * @param type - The type of the event (should be \"error\")\n * @param errorEventInitDict - Optional properties to include in the error event\n */\n constructor(type, errorEventInitDict) {\n var _a, _b;\n super(type), this.code = (_a = errorEventInitDict == null ? void 0 : errorEventInitDict.code) != null ? _a : void 0, this.message = (_b = errorEventInitDict == null ? void 0 : errorEventInitDict.message) != null ? _b : void 0;\n }\n /**\n * Node.js \"hides\" the `message` and `code` properties of the `ErrorEvent` instance,\n * when it is `console.log`'ed. This makes it harder to debug errors. To ease debugging,\n * we explicitly include the properties in the `inspect` method.\n *\n * This is automatically called by Node.js when you `console.log` an instance of this class.\n *\n * @param _depth - The current depth\n * @param options - The options passed to `util.inspect`\n * @param inspect - The inspect function to use (prevents having to import it from `util`)\n * @returns A string representation of the error\n */\n [Symbol.for(\"nodejs.util.inspect.custom\")](_depth, options, inspect) {\n return inspect(inspectableError(this), options);\n }\n /**\n * Deno \"hides\" the `message` and `code` properties of the `ErrorEvent` instance,\n * when it is `console.log`'ed. This makes it harder to debug errors. To ease debugging,\n * we explicitly include the properties in the `inspect` method.\n *\n * This is automatically called by Deno when you `console.log` an instance of this class.\n *\n * @param inspect - The inspect function to use (prevents having to import it from `util`)\n * @param options - The options passed to `Deno.inspect`\n * @returns A string representation of the error\n */\n [Symbol.for(\"Deno.customInspect\")](inspect, options) {\n return inspect(inspectableError(this), options);\n }\n}\nfunction syntaxError(message) {\n const DomException = globalThis.DOMException;\n return typeof DomException == \"function\" ? new DomException(message, \"SyntaxError\") : new SyntaxError(message);\n}\nfunction flattenError(err) {\n return err instanceof Error ? \"errors\" in err && Array.isArray(err.errors) ? err.errors.map(flattenError).join(\", \") : \"cause\" in err && err.cause instanceof Error ? `${err}: ${flattenError(err.cause)}` : err.message : `${err}`;\n}\nfunction inspectableError(err) {\n return {\n type: err.type,\n message: err.message,\n code: err.code,\n defaultPrevented: err.defaultPrevented,\n cancelable: err.cancelable,\n timeStamp: err.timeStamp\n };\n}\nvar __typeError = (msg) => {\n throw TypeError(msg);\n}, __accessCheck = (obj, member, msg) => member.has(obj) || __typeError(\"Cannot \" + msg), __privateGet = (obj, member, getter) => (__accessCheck(obj, member, \"read from private field\"), getter ? getter.call(obj) : member.get(obj)), __privateAdd = (obj, member, value) => member.has(obj) ? __typeError(\"Cannot add the same private member more than once\") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value), __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, \"write to private field\"), member.set(obj, value), value), __privateMethod = (obj, member, method) => (__accessCheck(obj, member, \"access private method\"), method), _readyState, _url, _redirectUrl, _withCredentials, _fetch, _reconnectInterval, _reconnectTimer, _lastEventId, _controller, _parser, _onError, _onMessage, _onOpen, _EventSource_instances, connect_fn, _onFetchResponse, _onFetchError, getRequestOptions_fn, _onEvent, _onRetryChange, failConnection_fn, scheduleReconnect_fn, _reconnect;\nclass EventSource extends EventTarget {\n constructor(url, eventSourceInitDict) {\n var _a, _b;\n super(), __privateAdd(this, _EventSource_instances), this.CONNECTING = 0, this.OPEN = 1, this.CLOSED = 2, __privateAdd(this, _readyState), __privateAdd(this, _url), __privateAdd(this, _redirectUrl), __privateAdd(this, _withCredentials), __privateAdd(this, _fetch), __privateAdd(this, _reconnectInterval), __privateAdd(this, _reconnectTimer), __privateAdd(this, _lastEventId, null), __privateAdd(this, _controller), __privateAdd(this, _parser), __privateAdd(this, _onError, null), __privateAdd(this, _onMessage, null), __privateAdd(this, _onOpen, null), __privateAdd(this, _onFetchResponse, async (response) => {\n var _a2;\n __privateGet(this, _parser).reset();\n const { body, redirected, status, headers } = response;\n if (status === 204) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, \"Server sent HTTP 204, not reconnecting\", 204), this.close();\n return;\n }\n if (redirected ? __privateSet(this, _redirectUrl, new URL(response.url)) : __privateSet(this, _redirectUrl, void 0), status !== 200) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, `Non-200 status code (${status})`, status);\n return;\n }\n if (!(headers.get(\"content-type\") || \"\").startsWith(\"text/event-stream\")) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, 'Invalid content type, expected \"text/event-stream\"', status);\n return;\n }\n if (__privateGet(this, _readyState) === this.CLOSED)\n return;\n __privateSet(this, _readyState, this.OPEN);\n const openEvent = new Event(\"open\");\n if ((_a2 = __privateGet(this, _onOpen)) == null || _a2.call(this, openEvent), this.dispatchEvent(openEvent), typeof body != \"object\" || !body || !(\"getReader\" in body)) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, \"Invalid response body, expected a web ReadableStream\", status), this.close();\n return;\n }\n const decoder = new TextDecoder(), reader = body.getReader();\n let open = !0;\n do {\n const { done, value } = await reader.read();\n value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (open = !1, __privateGet(this, _parser).reset(), __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this));\n } while (open);\n }), __privateAdd(this, _onFetchError, (err) => {\n __privateSet(this, _controller, void 0), !(err.name === \"AbortError\" || err.type === \"aborted\") && __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this, flattenError(err));\n }), __privateAdd(this, _onEvent, (event) => {\n typeof event.id == \"string\" && __privateSet(this, _lastEventId, event.id);\n const messageEvent = new MessageEvent(event.event || \"message\", {\n data: event.data,\n origin: __privateGet(this, _redirectUrl) ? __privateGet(this, _redirectUrl).origin : __privateGet(this, _url).origin,\n lastEventId: event.id || \"\"\n });\n __privateGet(this, _onMessage) && (!event.event || event.event === \"message\") && __privateGet(this, _onMessage).call(this, messageEvent), this.dispatchEvent(messageEvent);\n }), __privateAdd(this, _onRetryChange, (value) => {\n __privateSet(this, _reconnectInterval, value);\n }), __privateAdd(this, _reconnect, () => {\n __privateSet(this, _reconnectTimer, void 0), __privateGet(this, _readyState) === this.CONNECTING && __privateMethod(this, _EventSource_instances, connect_fn).call(this);\n });\n try {\n if (url instanceof URL)\n __privateSet(this, _url, url);\n else if (typeof url == \"string\")\n __privateSet(this, _url, new URL(url, getBaseURL()));\n else\n throw new Error(\"Invalid URL\");\n } catch {\n throw syntaxError(\"An invalid or illegal string was specified\");\n }\n __privateSet(this, _parser, createParser({\n onEvent: __privateGet(this, _onEvent),\n onRetry: __privateGet(this, _onRetryChange)\n })), __privateSet(this, _readyState, this.CONNECTING), __privateSet(this, _reconnectInterval, 3e3), __privateSet(this, _fetch, (_a = eventSourceInitDict == null ? void 0 : eventSourceInitDict.fetch) != null ? _a : globalThis.fetch), __privateSet(this, _withCredentials, (_b = eventSourceInitDict == null ? void 0 : eventSourceInitDict.withCredentials) != null ? _b : !1), __privateMethod(this, _EventSource_instances, connect_fn).call(this);\n }\n /**\n * Returns the state of this EventSource object's connection. It can have the values described below.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/readyState)\n *\n * Note: typed as `number` instead of `0 | 1 | 2` for compatibility with the `EventSource` interface,\n * defined in the TypeScript `dom` library.\n *\n * @public\n */\n get readyState() {\n return __privateGet(this, _readyState);\n }\n /**\n * Returns the URL providing the event stream.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/url)\n *\n * @public\n */\n get url() {\n return __privateGet(this, _url).href;\n }\n /**\n * Returns true if the credentials mode for connection requests to the URL providing the event stream is set to \"include\", and false otherwise.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/withCredentials)\n */\n get withCredentials() {\n return __privateGet(this, _withCredentials);\n }\n /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */\n get onerror() {\n return __privateGet(this, _onError);\n }\n set onerror(value) {\n __privateSet(this, _onError, value);\n }\n /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */\n get onmessage() {\n return __privateGet(this, _onMessage);\n }\n set onmessage(value) {\n __privateSet(this, _onMessage, value);\n }\n /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */\n get onopen() {\n return __privateGet(this, _onOpen);\n }\n set onopen(value) {\n __privateSet(this, _onOpen, value);\n }\n addEventListener(type, listener, options) {\n const listen = listener;\n super.addEventListener(type, listen, options);\n }\n removeEventListener(type, listener, options) {\n const listen = listener;\n super.removeEventListener(type, listen, options);\n }\n /**\n * Aborts any instances of the fetch algorithm started for this EventSource object, and sets the readyState attribute to CLOSED.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/close)\n *\n * @public\n */\n close() {\n __privateGet(this, _reconnectTimer) && clearTimeout(__privateGet(this, _reconnectTimer)), __privateGet(this, _readyState) !== this.CLOSED && (__privateGet(this, _controller) && __privateGet(this, _controller).abort(), __privateSet(this, _readyState, this.CLOSED), __privateSet(this, _controller, void 0));\n }\n}\n_readyState = /* @__PURE__ */ new WeakMap(), _url = /* @__PURE__ */ new WeakMap(), _redirectUrl = /* @__PURE__ */ new WeakMap(), _withCredentials = /* @__PURE__ */ new WeakMap(), _fetch = /* @__PURE__ */ new WeakMap(), _reconnectInterval = /* @__PURE__ */ new WeakMap(), _reconnectTimer = /* @__PURE__ */ new WeakMap(), _lastEventId = /* @__PURE__ */ new WeakMap(), _controller = /* @__PURE__ */ new WeakMap(), _parser = /* @__PURE__ */ new WeakMap(), _onError = /* @__PURE__ */ new WeakMap(), _onMessage = /* @__PURE__ */ new WeakMap(), _onOpen = /* @__PURE__ */ new WeakMap(), _EventSource_instances = /* @__PURE__ */ new WeakSet(), /**\n* Connect to the given URL and start receiving events\n*\n* @internal\n*/\nconnect_fn = function() {\n __privateSet(this, _readyState, this.CONNECTING), __privateSet(this, _controller, new AbortController()), __privateGet(this, _fetch)(__privateGet(this, _url), __privateMethod(this, _EventSource_instances, getRequestOptions_fn).call(this)).then(__privateGet(this, _onFetchResponse)).catch(__privateGet(this, _onFetchError));\n}, _onFetchResponse = /* @__PURE__ */ new WeakMap(), _onFetchError = /* @__PURE__ */ new WeakMap(), /**\n* Get request options for the `fetch()` request\n*\n* @returns The request options\n* @internal\n*/\ngetRequestOptions_fn = function() {\n var _a;\n const init = {\n // [spec] Let `corsAttributeState` be `Anonymous`…\n // [spec] …will have their mode set to \"cors\"…\n mode: \"cors\",\n redirect: \"follow\",\n headers: { Accept: \"text/event-stream\", ...__privateGet(this, _lastEventId) ? { \"Last-Event-ID\": __privateGet(this, _lastEventId) } : void 0 },\n cache: \"no-store\",\n signal: (_a = __privateGet(this, _controller)) == null ? void 0 : _a.signal\n };\n return \"window\" in globalThis && (init.credentials = this.withCredentials ? \"include\" : \"same-origin\"), init;\n}, _onEvent = /* @__PURE__ */ new WeakMap(), _onRetryChange = /* @__PURE__ */ new WeakMap(), /**\n* Handles the process referred to in the EventSource specification as \"failing a connection\".\n*\n* @param error - The error causing the connection to fail\n* @param code - The HTTP status code, if available\n* @internal\n*/\nfailConnection_fn = function(message, code) {\n var _a;\n __privateGet(this, _readyState) !== this.CLOSED && __privateSet(this, _readyState, this.CLOSED);\n const errorEvent = new ErrorEvent(\"error\", { code, message });\n (_a = __privateGet(this, _onError)) == null || _a.call(this, errorEvent), this.dispatchEvent(errorEvent);\n}, /**\n* Schedules a reconnection attempt against the EventSource endpoint.\n*\n* @param message - The error causing the connection to fail\n* @param code - The HTTP status code, if available\n* @internal\n*/\nscheduleReconnect_fn = function(message, code) {\n var _a;\n if (__privateGet(this, _readyState) === this.CLOSED)\n return;\n __privateSet(this, _readyState, this.CONNECTING);\n const errorEvent = new ErrorEvent(\"error\", { code, message });\n (_a = __privateGet(this, _onError)) == null || _a.call(this, errorEvent), this.dispatchEvent(errorEvent), __privateSet(this, _reconnectTimer, setTimeout(__privateGet(this, _reconnect), __privateGet(this, _reconnectInterval)));\n}, _reconnect = /* @__PURE__ */ new WeakMap(), /**\n* ReadyState representing an EventSource currently trying to connect\n*\n* @public\n*/\nEventSource.CONNECTING = 0, /**\n* ReadyState representing an EventSource connection that is open (eg connected)\n*\n* @public\n*/\nEventSource.OPEN = 1, /**\n* ReadyState representing an EventSource connection that is closed (eg disconnected)\n*\n* @public\n*/\nEventSource.CLOSED = 2;\nfunction getBaseURL() {\n const doc = \"document\" in globalThis ? globalThis.document : void 0;\n return doc && typeof doc == \"object\" && \"baseURI\" in doc && typeof doc.baseURI == \"string\" ? doc.baseURI : void 0;\n}\nexport {\n ErrorEvent,\n EventSource\n};\n//# sourceMappingURL=index.js.map\n","import type { AccessControlClient } from \"./client.js\";\n\nimport { logger } from \"../lib/logger.js\";\n\nexport interface AccessControlConfig {\n client?: AccessControlClient;\n enabled: boolean;\n serverName?: string;\n}\n\n/**\n * Authorizes MCP servers based on access control configuration.\n * Queries backend with full client info for authorization decisions.\n */\nexport class AccessControlAuthorizer {\n private client?: AccessControlClient;\n private enabled: boolean;\n private lastBlockReason?: string;\n private serverName?: string;\n\n constructor(config: AccessControlConfig) {\n this.enabled = config.enabled;\n this.client = config.client;\n this.serverName = config.serverName;\n\n logger.info(\"AccessControlAuthorizer initialized\", {\n enabled: this.enabled,\n hasClient: !!this.client,\n serverName: this.serverName,\n });\n }\n\n /**\n * Get human-readable reason for why a server was blocked.\n * Returns the reason from the last authorization check, or a default message.\n *\n * @returns Reason message\n */\n getBlockReason(): string {\n if (this.lastBlockReason) {\n return this.lastBlockReason;\n }\n // Default message with server name if available\n const serverPart = this.serverName ? ` '${this.serverName}'` : \"\";\n return `MCP server${serverPart} is not authorized for use in your organization and has been blocked by Onyx.`;\n }\n\n /**\n * Check if access is allowed by querying the backend.\n * Always fails open (allows) if backend is unavailable or not configured.\n * Stores the block reason from the backend for later retrieval.\n *\n * @returns Promise resolving to true if allowed, false if blocked\n */\n async isAllowed(): Promise<boolean> {\n // If access control not enabled, allow everything\n if (!this.enabled) {\n return true;\n }\n\n // Check with backend if client is available\n if (this.client) {\n try {\n const response = await this.client.authorize();\n if (response.action === \"block\") {\n // Store the reason from backend for user-facing error messages, or use default with server name\n if (response.reason) {\n this.lastBlockReason = response.reason;\n } else {\n const serverPart = this.serverName ? ` '${this.serverName}'` : \"\";\n this.lastBlockReason = `MCP server${serverPart} is not authorized for use in your organization and has been blocked by Onyx.`;\n }\n return false;\n }\n return true;\n } catch (error) {\n logger.error(\"Access control authorization failed with unexpected error\", {\n error: String(error),\n });\n // DEFENSIVE: client.authorize() handles all errors internally and always\n // returns an AccessControlResponse, so this catch is normally unreachable.\n // However, we keep it as defensive fail-open behavior for unexpected errors.\n return true;\n }\n }\n\n // No client configured - allow by default (shouldn't happen if enabled=true with URL)\n logger.warn(\"No access control client configured, allowing by default\");\n return true;\n }\n}\n","import type { SessionData } from \"../utils/getUserData.js\";\n\nimport { compressClientInfo } from \"../lib/compress-client-info.js\";\nimport { logger } from \"../lib/logger.js\";\n\nexport interface AccessControlClientConfig {\n apiKey: string;\n headers: Record<string, string>;\n sessionData: SessionData;\n timeoutMs: number;\n url: string;\n}\n\nexport interface AccessControlResponse {\n action: \"allow\" | \"block\";\n reason?: string;\n}\n\n/**\n * Client for checking MCP server access control with backend API.\n * Always fails open (allows) when backend is unreachable.\n */\nexport class AccessControlClient {\n private clientInfoBase64: string;\n private config: AccessControlClientConfig;\n\n constructor(config: AccessControlClientConfig) {\n this.config = config;\n this.clientInfoBase64 = this.getClientInfoBase64();\n logger.info(\"AccessControlClient initialized\", {\n timeoutMs: config.timeoutMs,\n url: config.url,\n });\n }\n\n /**\n * Authorize by querying the backend with full client info.\n * Always fails open (allows) on any error.\n *\n * @returns Access control response\n */\n async authorize(): Promise<AccessControlResponse> {\n try {\n // Try to get decision from backend\n const response = await this.sendAuthorizeRequest();\n\n logger.debug(\"Access control check successful\", {\n action: response.action,\n });\n\n return response;\n } catch (error) {\n logger.warn(\"Access control check failed, failing open (allowing by default)\", {\n error: String(error),\n });\n\n // Always fail open (allow by default for better UX)\n return { action: \"allow\" };\n }\n }\n\n /**\n * Compress and base64-encode client info for URL.\n * Matches scanner pattern.\n */\n private getClientInfoBase64(): string {\n return compressClientInfo(this.config.sessionData);\n }\n\n /**\n * Send authorization request to backend with compressed client info in URL.\n * Matches scanner URL pattern: ${url}/${apiKey}/mcp/${clientInfoBase64}\n *\n * @returns Access control response\n * @throws Error if request fails\n */\n private async sendAuthorizeRequest(): Promise<AccessControlResponse> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);\n\n try {\n // Build URL with API key and compressed client info (matching scanner pattern)\n const checkUrl = `${this.config.url}/${this.config.apiKey}/mcp/${this.clientInfoBase64}`;\n\n const response = await fetch(checkUrl, {\n headers: {\n ...this.config.headers,\n },\n method: \"POST\",\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Access control service returned ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as AccessControlResponse;\n\n // Validate response format\n if (!result.action || ![\"allow\", \"block\"].includes(result.action)) {\n throw new Error(`Invalid access control response format: action=\"${result.action}\"`);\n }\n\n return result;\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new Error(`Access control check timed out after ${this.config.timeoutMs}ms`);\n }\n throw error;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n}\n","import type { OAuthClientInformationFull, OAuthTokens } from \"@modelcontextprotocol/client\";\n\nimport { createHash } from \"node:crypto\";\nimport { chmod, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { chownToRealUser } from \"../utils/chownToRealUser.js\";\nimport { normalizeUrl } from \"../utils/normalizeUrl.js\";\n\nexport interface OAuthCredentialStoreOptions {\n /**\n * Directory to store OAuth credentials.\n * Defaults to ~/.onyx/mcp-gateway/oauth/\n */\n storageDir: string;\n}\n\n/**\n * Stored credentials structure persisted to disk.\n */\nexport interface StoredCredentials {\n /**\n * DCR client registration information.\n */\n clientInfo?: OAuthClientInformationFull;\n /**\n * Timestamp when the credentials were stored (ISO string).\n */\n createdAt: string;\n /**\n * The original server URL these credentials are for.\n */\n serverUrl: string;\n /**\n * OAuth tokens (access_token, refresh_token, etc.).\n */\n tokens?: OAuthTokens;\n /**\n * Timestamp when tokens were obtained (ISO string).\n * Used to calculate token expiration.\n */\n tokensObtainedAt?: string;\n /**\n * Last update timestamp (ISO string).\n */\n updatedAt: string;\n}\n\n/**\n * File-based credential store for OAuth credentials.\n * Stores credentials in JSON files keyed by server URL hash.\n */\nexport class OAuthCredentialStore {\n private readonly storageDir: string;\n\n constructor(options: OAuthCredentialStoreOptions) {\n this.storageDir = options.storageDir;\n }\n\n /**\n * Deletes stored credentials for a server URL.\n */\n async delete(serverUrl: string): Promise<void> {\n // Normalize URL to ensure consistent key generation\n const normalizedUrl = normalizeUrl(serverUrl);\n const filePath = this.getFilePath(normalizedUrl);\n\n try {\n await rm(filePath);\n logger.debug(\"Deleted stored credentials\", { filePath, serverUrl: normalizedUrl });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n logger.warn(\"Failed to delete stored credentials\", {\n error: String(error),\n filePath,\n serverUrl: normalizedUrl,\n });\n }\n }\n }\n\n /**\n * Checks if stored credentials have a refresh token.\n */\n hasRefreshToken(credentials: StoredCredentials): boolean {\n return !!credentials.tokens?.refresh_token;\n }\n\n /**\n * Checks if stored credentials have a valid (non-expired) access token.\n */\n hasValidAccessToken(credentials: StoredCredentials): boolean {\n return !!credentials.tokens?.access_token && !this.isAccessTokenExpired(credentials);\n }\n\n /**\n * Checks if the stored access token is expired.\n * Returns true if expired or if expiration cannot be determined.\n */\n isAccessTokenExpired(credentials: StoredCredentials): boolean {\n if (!credentials.tokens || !credentials.tokensObtainedAt) {\n return true;\n }\n\n const { expires_in } = credentials.tokens;\n if (!expires_in) {\n // No expiration info - assume not expired\n return false;\n }\n\n const obtainedAt = new Date(credentials.tokensObtainedAt).getTime();\n const expiresAt = obtainedAt + expires_in * 1000;\n const now = Date.now();\n\n // Add 30 second buffer to avoid edge cases\n const isExpired = now >= expiresAt - 30000;\n\n logger.debug(\"Checked access token expiration\", {\n expiresAt: new Date(expiresAt).toISOString(),\n expiresIn: expires_in,\n isExpired,\n now: new Date(now).toISOString(),\n obtainedAt: credentials.tokensObtainedAt,\n });\n\n return isExpired;\n }\n\n /**\n * Loads stored credentials for a server URL.\n * Returns undefined if no credentials exist or if they're invalid.\n */\n async load(serverUrl: string): Promise<StoredCredentials | undefined> {\n // Normalize URL to ensure consistent key generation\n const normalizedUrl = normalizeUrl(serverUrl);\n const filePath = this.getFilePath(normalizedUrl);\n\n try {\n const content = await readFile(filePath, \"utf-8\");\n const credentials = JSON.parse(content) as StoredCredentials;\n\n // Validate the stored credentials match the requested server (using normalized URL)\n // Also accept credentials stored with the original URL format for backward compatibility\n const storedUrlNormalized = normalizeUrl(credentials.serverUrl);\n if (storedUrlNormalized !== normalizedUrl) {\n logger.warn(\"Credential file server URL mismatch\", {\n expected: normalizedUrl,\n filePath,\n found: credentials.serverUrl,\n });\n return undefined;\n }\n\n logger.debug(\"Loaded stored credentials\", {\n clientId: credentials.clientInfo?.client_id,\n hasRefreshToken: !!credentials.tokens?.refresh_token,\n hasTokens: !!credentials.tokens,\n serverUrl: normalizedUrl,\n });\n\n return credentials;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n // File doesn't exist - no stored credentials\n logger.debug(\"No stored credentials found\", { serverUrl: normalizedUrl });\n return undefined;\n }\n\n logger.warn(\"Failed to load stored credentials\", {\n error: String(error),\n filePath,\n serverUrl: normalizedUrl,\n });\n return undefined;\n }\n }\n\n /**\n * Saves credentials to disk.\n */\n async save(credentials: StoredCredentials): Promise<void> {\n await this.ensureStorageDir();\n\n const filePath = this.getFilePath(credentials.serverUrl);\n const content = JSON.stringify(credentials, null, 2);\n\n try {\n await writeFile(filePath, content, { encoding: \"utf-8\", mode: 0o600 });\n // Ensure permissions are correct even if file existed\n await chmod(filePath, 0o600);\n\n logger.debug(\"Saved credentials to disk\", {\n clientId: credentials.clientInfo?.client_id,\n filePath,\n hasTokens: !!credentials.tokens,\n serverUrl: credentials.serverUrl,\n });\n } catch (error) {\n logger.error(\"Failed to save credentials\", {\n error: String(error),\n filePath,\n serverUrl: credentials.serverUrl,\n });\n throw error;\n }\n }\n\n /**\n * Updates specific fields in stored credentials.\n * Creates new credentials if none exist.\n */\n async update(\n serverUrl: string,\n updates: Partial<Pick<StoredCredentials, \"clientInfo\" | \"tokens\" | \"tokensObtainedAt\">>\n ): Promise<StoredCredentials> {\n // Normalize URL to ensure consistent key generation and storage\n const normalizedUrl = normalizeUrl(serverUrl);\n const existing = await this.load(normalizedUrl);\n const now = new Date().toISOString();\n\n const credentials: StoredCredentials = existing\n ? {\n ...existing,\n ...updates,\n // Update serverUrl to normalized form for consistency\n serverUrl: normalizedUrl,\n updatedAt: now,\n }\n : {\n createdAt: now,\n serverUrl: normalizedUrl,\n updatedAt: now,\n ...updates,\n };\n\n await this.save(credentials);\n return credentials;\n }\n\n /**\n * Ensures the storage directory exists with proper permissions.\n */\n private async ensureStorageDir(): Promise<void> {\n try {\n await mkdir(this.storageDir, { mode: 0o700, recursive: true });\n } catch (error) {\n // Directory may already exist\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") {\n throw error;\n }\n }\n\n // Best-effort ownership repair; should not block auth flows\n try {\n await chownToRealUser(this.storageDir);\n } catch (error) {\n logger.warn(\"Failed to fix OAuth storage directory ownership\", {\n error: String(error),\n storageDir: this.storageDir,\n });\n }\n }\n\n /**\n * Generates a URL-safe filename from a server URL.\n * Uses SHA-256 hash truncated to 16 chars for brevity.\n */\n private generateKey(serverUrl: string): string {\n const hash = createHash(\"sha256\").update(serverUrl).digest(\"hex\");\n return hash.substring(0, 16);\n }\n\n /**\n * Gets the file path for a server's credentials.\n */\n private getFilePath(serverUrl: string): string {\n const key = this.generateKey(serverUrl);\n return join(this.storageDir, `${key}.json`);\n }\n}\n\n","/**\n * Dynamic OAuth bearer for the gateway's HTTP transports (PRDCT-3666 Tier 2).\n *\n * The gateway's first cut bound the access_token into the HTTP transport's\n * Authorization header ONCE, at connect time (`main.ts:606`). Once the gateway\n * had successfully connected, an upstream-side token rotation, revocation, or\n * clock-skew-driven expiry would 401 every subsequent request, with no\n * recovery — the user's session was effectively dead until they restarted the\n * MCP client.\n *\n * `createDynamicAuth` returns a handle whose `getCurrentBearer()` is read at\n * fetch time (not connect time). `createAuthenticatingFetch` wraps the\n * transport's underlying `fetch` so that:\n *\n * 1. Each outbound request reads the current bearer just-in-time and\n * attaches it as `Authorization: Bearer ...`.\n * 2. If the upstream returns 401 to a request we sent with a bearer, the\n * wrapper triggers a single silent refresh attempt and retries the SAME\n * request once. Concurrent 401s share the same in-flight refresh\n * (single-flight) to avoid burning the refresh_token via rotation races.\n * 3. Before refreshing, the handle re-reads the on-disk credentials —\n * another gateway process may already have refreshed the token, in which\n * case we use the disk value instead of burning another refresh.\n * 4. If the refresh ultimately fails (`invalid_grant`, no refresh_token,\n * transient error), the wrapper does NOT open a browser — that decision\n * is left to the connect-time `main.ts` handler (which can fall back to\n * interactive OAuth when appropriate). The mid-stream contract is: a\n * browser must never open as a direct reaction to a 401.\n */\n\nimport type { FetchLike } from \"@modelcontextprotocol/client\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { tryRefreshCachedToken } from \"./oauthFlow.js\";\nimport type { OAuthCredentialStore } from \"./OAuthCredentialStore.js\";\n\nexport interface DynamicAuthHandle {\n /** Synchronous read of the in-memory bearer. May be undefined. */\n getCurrentBearer(): string | undefined;\n /**\n * Attempt a silent refresh. Deduplicates concurrent calls via a single-flight\n * promise. Before issuing an HTTP refresh, reads the on-disk credentials to\n * pick up any rotation done by a parallel gateway process.\n *\n * `sentBearer` is the bearer that just got 401'd. If the disk now holds a\n * different bearer, the on-disk value is adopted and returned without a new\n * HTTP call (multi-process race recovery).\n *\n * Returns the new bearer on success, undefined if refresh failed for any\n * reason (`invalid_grant`, transient, no refresh_token on disk).\n */\n refreshIfPossible(sentBearer?: string): Promise<string | undefined>;\n /** Replace the in-memory bearer (e.g., after a successful `performOAuthFlow`). */\n setBearer(bearer: string | undefined): void;\n}\n\n/**\n * Wraps `fetch` to attach the dynamic Authorization header and recover from\n * 401 responses via a single silent-refresh-and-retry. Pass the returned\n * function to the SDK transport's `fetch` option.\n *\n * Transport coverage notes — including the one case we deliberately do NOT\n * support (silent mid-stream drop on an open SSE event-stream) — are\n * documented in detail above `createTransport` in\n * `src/lib/transportDetection.ts`. Read it before changing this wrapper.\n */\nexport function createAuthenticatingFetch(\n auth: DynamicAuthHandle,\n baseFetch: typeof fetch = (...args) => globalThis.fetch(...args),\n): FetchLike {\n return async (url, init) => {\n // Whether the caller supplied a static Authorization (api-key / Basic /\n // static bearer via --env-http-headers, --headers, or --bearer-token-env).\n // The wrapper owns only the OAuth bearer; it must not clobber a credential\n // the caller explicitly provided. Without this guard, non-OAuth auth modes\n // break in any environment without a cached OAuth token (PRDCT-5136).\n const callerSuppliedAuth = new Headers(init?.headers).has(\"Authorization\");\n\n const sendInit = (bearer: string | undefined): RequestInit => {\n const headers = new Headers(init?.headers);\n if (bearer) {\n // OAuth bearer takes precedence and is refreshed mid-session.\n headers.set(\"Authorization\", `Bearer ${bearer}`);\n } else if (!callerSuppliedAuth) {\n // No bearer and no caller-supplied credential: clear only the slot the\n // wrapper would otherwise manage, so we never send a stale OAuth token.\n headers.delete(\"Authorization\");\n }\n // else: no bearer but the caller supplied a static Authorization — leave\n // it intact so the upstream can authenticate it (api-key / Basic / etc.).\n return { ...init, headers };\n };\n\n const bearer = auth.getCurrentBearer();\n let response = await baseFetch(url, sendInit(bearer));\n\n // Diagnostic: capture the wire-level details of any non-2xx upstream\n // response. The SDK transport only surfaces \"HTTP <code>: <body>\" and drops\n // response headers — but WWW-Authenticate is exactly the signal we need\n // post-mortem to tell a non-compliant 500 from a spec-correct 401 + OAuth\n // discovery pointer. The body is bounded and only read on error (a streaming\n // 2xx is never cloned). A 401 is the expected OAuth-challenge case (info);\n // any other non-2xx is surprising (warn).\n if (!response.ok) {\n let bodySnippet = \"\";\n try {\n bodySnippet = (await response.clone().text()).slice(0, 500);\n } catch {\n /* body not readable (already consumed or streaming) */\n }\n const diag = {\n bodySnippet,\n contentType: response.headers.get(\"content-type\") ?? undefined,\n eventType: \"mcp_upstream_non_2xx\",\n method: String(init?.method ?? \"GET\").toUpperCase(),\n requestAccept: new Headers(init?.headers).get(\"accept\") ?? undefined,\n requestHadAuth: Boolean(bearer) || callerSuppliedAuth,\n status: response.status,\n url: sanitizeUrlForLog(url),\n wwwAuthenticate: response.headers.get(\"www-authenticate\") ?? undefined,\n };\n if (response.status === 401) {\n logger.info(\"Upstream returned non-2xx (auth challenge)\", diag);\n } else {\n logger.warn(\"Upstream returned non-2xx\", diag);\n }\n }\n\n // Only attempt refresh-and-retry when we actually sent a bearer that was\n // rejected. A 401 without a bearer means the upstream is asking us to\n // authenticate from scratch — that path is handled by the connect-time\n // logic in main.ts, not here.\n if (response.status === 401 && bearer) {\n logger.info(\"Upstream returned 401; attempting mid-session silent refresh\", {\n eventType: \"mcp_auth_mid_session_refresh_attempt\",\n });\n const refreshed = await auth.refreshIfPossible(bearer);\n if (refreshed && refreshed !== bearer) {\n logger.info(\"Silent refresh succeeded mid-session; retrying request\", {\n eventType: \"mcp_auth_mid_session_refresh_succeeded\",\n });\n response = await baseFetch(url, sendInit(refreshed));\n } else {\n logger.warn(\"Silent refresh produced no new bearer; passing 401 through\", {\n eventType: \"mcp_auth_mid_session_refresh_failed\",\n });\n }\n }\n\n return response;\n };\n}\n\nexport function createDynamicAuth(\n serverUrl: string,\n credentialStore: OAuthCredentialStore,\n initialBearer: string | undefined,\n): DynamicAuthHandle {\n let currentBearer = initialBearer;\n let refreshInFlight: null | Promise<string | undefined> = null;\n\n return {\n getCurrentBearer: () => currentBearer,\n refreshIfPossible: (sentBearer) => {\n // Single-flight: concurrent callers share the same refresh promise so\n // that N parallel 401s yield ONE refresh HTTP call (not N), avoiding\n // refresh_token rotation races within this process.\n if (refreshInFlight) return refreshInFlight;\n refreshInFlight = (async () => {\n try {\n // Multi-process recovery: another gateway process may already have\n // refreshed since we loaded our in-memory bearer. Re-read disk\n // before committing to a refresh HTTP call.\n const onDisk = await credentialStore.load(serverUrl);\n const diskBearer = onDisk?.tokens?.access_token;\n if (diskBearer && diskBearer !== sentBearer) {\n logger.info(\"Adopting newer access_token from disk (other process refreshed)\", {\n eventType: \"mcp_auth_disk_bearer_adopted\",\n serverUrl,\n });\n currentBearer = diskBearer;\n return diskBearer;\n }\n // Disk has the same bearer we tried (or none). Issue a refresh.\n const result = await tryRefreshCachedToken(serverUrl, credentialStore);\n if (result?.tokens.access_token) {\n currentBearer = result.tokens.access_token;\n return currentBearer;\n }\n // Refresh failed (invalid_grant cleared the tokens, invalid_client\n // cleared the whole record, transient, or no refresh_token on disk).\n // Leave currentBearer as-is so the connect-time handler can take over\n // when this gateway process restarts; callers must NOT open a browser\n // from inside the fetch wrapper.\n return undefined;\n } finally {\n refreshInFlight = null;\n }\n })();\n return refreshInFlight;\n },\n setBearer: (bearer) => {\n currentBearer = bearer;\n },\n };\n}\n\n/**\n * Origin + pathname only — drops the query string / fragment, which can carry\n * tokens or other sensitive material. Used for diagnostic logging of upstream\n * requests so a log line never leaks a credential.\n */\nfunction sanitizeUrlForLog(url: unknown): string {\n try {\n const s = typeof url === \"string\" ? url : String(url);\n const u = new URL(s);\n return `${u.origin}${u.pathname}`;\n } catch {\n return \"[unparseable-url]\";\n }\n}\n","/**\n * How every error classifier in this package reads an unknown thrown value.\n *\n * Extracted so `upstreamErrors.ts` and `protocolEra.ts` share one definition.\n * They cannot import from each other — `upstreamErrors` consults the era\n * classifier, so the dependency only runs one way — and a second local copy had\n * already drifted: it handled `Error` and `string` but not a plain object\n * carrying a `message`, which is exactly the shape a JSON-RPC error arrives in\n * when it has not been rehydrated into an `McpError`. A classifier that cannot\n * read the message silently declines to classify.\n */\n\n/** The numeric JSON-RPC `code` of an unknown thrown value, when it has one. */\nexport function errorCodeOf(error: unknown): number | undefined {\n if (typeof error !== \"object\" || error === null) return undefined;\n const code = (error as { code?: unknown }).code;\n return typeof code === \"number\" ? code : undefined;\n}\n\n/** The message of an unknown thrown value, or \"\" when it carries none. */\nexport function messageOf(error: unknown): string {\n if (error instanceof Error) return error.message ?? \"\";\n if (typeof error === \"string\") return error;\n if (error && typeof error === \"object\" && \"message\" in error) {\n return String((error as { message?: unknown }).message ?? \"\");\n }\n return \"\";\n}\n","/**\n * Protocol-era classification for upstream connect failures.\n *\n * MCP has two wire eras. The HANDSHAKE era (revisions 2024-11-05 … 2025-11-25)\n * opens every session with an `initialize` request. The MODERN era (2026-07-28)\n * deletes `initialize` outright: there is no handshake and no session, the\n * negotiated version rides in `_meta` on every request, and a server advertises\n * itself through a mandatory `server/discover` call instead.\n *\n * With the dual-era kill switch ON (the default) the upstream client negotiates\n * the era (`versionNegotiation: 'auto'` — see upstreamNegotiation.ts), so a\n * modern-only upstream is WRAPPED, not classified: this module's classification\n * is then reachable only for a server that refuses both the `server/discover`\n * probe and `initialize` (a broken server wearing the era-mismatch shape). With\n * the switch OFF the gateway is pinned to the handshake era, its `initialize`\n * is simply an unknown method to a modern-only upstream, and the connection\n * cannot be established at all — the case this module classifies.\n *\n * Detecting that specific case matters because the failure is otherwise\n * misdiagnosed. Measured against a crafted modern-only upstream, the unclassified\n * path sent one `initialize` (rejected `-32601`), fell back to SSE (`GET` → 405),\n * then walked the entire cold-start OAuth-metadata discovery chain — six\n * `.well-known` requests hunting for OAuth on a server whose only problem is its\n * protocol era — before dying ~28s later with no message for the agent at all.\n *\n * A dual-era upstream (one that still answers `initialize`) never reaches here:\n * whichever era the connect ran in, its opening exchange succeeds and nothing\n * about this module applies.\n */\n\nimport { errorCodeOf, messageOf } from \"./errorMessage.js\";\n\n/**\n * The handshake-era revisions this gateway can negotiate.\n *\n * Duplicated as `SUPPORTED_PROTOCOL_VERSIONS` in\n * `backend_python/src/sanitization_service/internal/schema_validation/engine.py`,\n * and the two must gain `2026-07-28` together. They answer different questions\n * from the same fact: this copy decides whether the gateway can negotiate a\n * session at all; the guard's copy decides whether it can reason about traffic\n * it is handed. Move one without the other and either the guard validates\n * traffic the gateway cannot produce, or modern traffic reaches a guard that\n * treats its revision as unknown.\n */\nexport const HANDSHAKE_ERA_REVISIONS = [\"2024-11-05\", \"2025-03-26\", \"2025-06-18\", \"2025-11-25\"] as const;\n\n/** The stateless \"modern era\" revision that removed `initialize`. */\nexport const MODERN_ERA_REVISION = \"2026-07-28\";\n\n/**\n * JSON-RPC \"method not found\" — what a modern-era server answers when we send\n * `initialize`, a method its era does not define.\n */\nconst METHOD_NOT_FOUND = -32601;\n\n/**\n * `UnsupportedProtocolVersionError`. The 2026-07-28 spec renumbered this from\n * -32004 during its draft; accept both so a server on either numbering is\n * classified rather than misread as a transport fault.\n */\nconst UNSUPPORTED_PROTOCOL_VERSION = new Set([-32004, -32022]);\n\n/**\n * `initialize` as a whole word.\n *\n * The `\\b` on the right is load-bearing: without it this also matches\n * `notifications/initialized`, which a HANDSHAKE-era server emits with -32601\n * when it does not implement that notification — *after a successful\n * handshake*. Classifying that as an era mismatch would take a working legacy\n * connection and mark it permanently unrecoverable.\n */\nconst INITIALIZE_METHOD_RE = /\\binitialize\\b/i;\n\n/**\n * An upstream reporting `initialize` as an unknown method, in either word order.\n *\n * One definition, consumed by both `isProtocolEraMismatchError` (as part of the\n * umbrella check) and `rejectsInitializeAsUnknown` (which decides WHICH message\n * the operator gets). It was written out twice — the same two-copies shape this\n * PR removed from `messageOf`, where the copies had already silently diverged.\n */\nconst UNKNOWN_INITIALIZE_RE =\n /method not found[^\\w]*\\binitialize\\b|\\binitialize\\b[^\\w]*method not found/;\n\n/**\n * An upstream rejecting our announced protocol version outright.\n */\nconst VERSION_REJECTED_RE = /unsupported protocol version|protocol version is not supported/;\n\n/**\n * Either shape means \"we do not speak the same era\", which no amount of\n * retrying, transport fallback or re-authentication can fix.\n */\nconst eraMismatchInMessage = (message: string): boolean =>\n new RegExp(`${UNKNOWN_INITIALIZE_RE.source}|${VERSION_REJECTED_RE.source}`, \"i\").test(message);\n\n/**\n * The operator-facing explanation. Says \"protocol era\", never the generic\n * \"cannot reach the upstream\" a transport failure produces — the server is\n * healthy and answering.\n *\n * Two different failures reach here and they must not be described identically:\n *\n * * The upstream has no `initialize` at all (`-32601`) — it genuinely dropped\n * the handshake, so naming the modern revision is accurate.\n * * The upstream HAS `initialize` but rejected the revision we offered. Its own\n * message usually names what it does support, so quote it rather than\n * asserting anything about its era. Observed in the wild from\n * `mcp.deepwiki.com`: *\"Unsupported protocol version: … Supported versions:\n * 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25\"*.\n *\n * Claiming the first when it is really the second would send an operator looking\n * for a modern-only server that does not exist.\n */\nexport function describeEraMismatch(url: string, error?: unknown): string {\n const supported = `this gateway speaks the handshake era (${HANDSHAKE_ERA_REVISIONS[0]} … ${HANDSHAKE_ERA_REVISIONS[HANDSHAKE_ERA_REVISIONS.length - 1]})`;\n\n if (error === undefined || rejectsInitializeAsUnknown(error)) {\n return (\n `the MCP server at ${url} does not implement the \\`initialize\\` handshake, which means it ` +\n `speaks only protocol revision ${MODERN_ERA_REVISION} or later (the stateless \"modern era\" ` +\n `removed \\`initialize\\`), while ${supported}`\n );\n }\n\n return (\n `the MCP server at ${url} rejected the protocol revision this gateway offered ` +\n `(${messageOf(error) || \"no detail given\"}); ${supported}`\n );\n}\n\n/**\n * Whether a connect failure is a protocol-era mismatch rather than a transport,\n * auth or availability problem.\n *\n * Deliberately narrow. A bare `-32601` is NOT enough on its own: a handshake-era\n * server may legitimately answer `-32601` for some other method we asked for, and\n * treating that as an era mismatch would mask a real failure. The method name has\n * to be implicated, or the error has to be an explicit protocol-version\n * rejection.\n */\nexport function isProtocolEraMismatchError(error: unknown): boolean {\n const message = messageOf(error);\n const code = errorCodeOf(error);\n\n // The two arms are deliberately asymmetric, tracking how specific each code is.\n // UnsupportedProtocolVersion means only ever a version rejection, so the code\n // alone settles it — demanding a message match too would decline to classify a\n // server that sends the code with an empty or non-English message. METHOD_NOT_FOUND\n // is generic: a handshake-era server also emits it for notifications it does not\n // implement, so that arm needs the message to tell an absent `initialize` apart\n // from an absent `notifications/initialized`.\n if (code !== undefined && UNSUPPORTED_PROTOCOL_VERSION.has(code)) return true;\n if (code === METHOD_NOT_FOUND && INITIALIZE_METHOD_RE.test(message)) return true;\n\n return message.length > 0 && eraMismatchInMessage(message);\n}\n\n\n\n/**\n * Whether the upstream specifically has NO `initialize` method, as opposed to\n * having one that rejected the revision we offered. The distinction decides what\n * we may honestly tell the operator, so it is kept separate from the umbrella\n * `isProtocolEraMismatchError` check.\n */\nfunction rejectsInitializeAsUnknown(error: unknown): boolean {\n const message = messageOf(error);\n const code = errorCodeOf(error);\n if (code === METHOD_NOT_FOUND && INITIALIZE_METHOD_RE.test(message)) return true;\n // Shares UNKNOWN_INITIALIZE_RE with the umbrella check, so the two can never\n // drift into disagreeing about what \"unknown initialize\" means.\n return new RegExp(UNKNOWN_INITIALIZE_RE.source, \"i\").test(message);\n}\n","/**\n * Redacting credentials out of text that mentions an upstream URL, while\n * keeping the origin readable.\n *\n * Its own module because it is pure string handling that needs direct tests. It\n * lived inline in `main.ts` first, and review found a bug in it that no test\n * could reach: the only coverage drove a spawned gateway at an IP-based fixture,\n * so a whole class of input (a hostname that contains its own path segment) was\n * unreachable.\n *\n * Why this exists at all: remote MCP URLs routinely carry secrets in the path or\n * query — `ctx7sk-` (Context7), `tvly-` (Tavily), `sk-`, `github_pat_` — and an\n * era mismatch names the upstream in the agent's error, the stderr banner and a\n * structured log event. Case B of `describeEraMismatch` also relays the\n * upstream's own rejection verbatim, so the upstream chooses which form the\n * credential appears in: full URL, path, path+query, query, or one segment.\n */\n\n/**\n * Short-but-real token shapes, so a credential below the length floor is still\n * caught.\n */\n// Prefixes are drawn from the Go/Python MCP credential-redaction pair (the Go\n// `mcpCredentialPrefixRe` and `_CREDENTIAL_PREFIX_SHAPES` in\n// backend_python/src/common/services/mcp_secret_redaction.py), so a prefix those two\n// recognise is recognised here too. This is deliberately NOT a third member of that\n// locked pair: those two decide whether a string IS a credential, whereas this only\n// widens `isPlausibleSecret` below for segments too SHORT to trip the length rule.\n// Real tokens for every prefix here run well past MIN_CREDENTIAL_SEGMENT_LEN, so the\n// length arm — not this list — is what redacts them; drift costs coverage of\n// implausibly short tokens only. Over-matching is safe in this direction (a redacted\n// non-secret loses a little log detail), so the shape bounds those two need are\n// omitted and matching stays case-insensitive.\nconst CREDENTIAL_PREFIX_RE =\n /^(ctx7sk|tvly|sk|pat|gh[posu]|github_pat|glpat|xox[abprs]|AKIA|Bearer)[-_]/i;\n\n/**\n * Below this, a lone path segment is treated as routing rather than a secret.\n *\n * A bare segment is matched as a raw substring, so redacting a short one\n * corrupts unrelated text — including the hostname. Measured on the first\n * implementation: `https://mcp.context7.com/mcp` redacting the segment `mcp`\n * produced `https:/[REDACTED].context7.com`, eating a slash and destroying the\n * origin the redaction exists to preserve. A `mcp.` host with a `/mcp` path is\n * what most real remote MCP servers look like, so that was the common case, not\n * an edge one.\n */\nconst MIN_CREDENTIAL_SEGMENT_LEN = 12;\n\nconst isPlausibleSecret = (segment: string): boolean =>\n segment.length >= MIN_CREDENTIAL_SEGMENT_LEN || CREDENTIAL_PREFIX_RE.test(segment);\n\n/**\n * Percent-decode, or \"\" when the input is not valid encoding.\n *\n * `decodeURIComponent` throws on a malformed sequence such as `%ZZ`, and this runs\n * while building an error message — throwing here would replace a redacted\n * diagnostic with a crash.\n */\nconst tryDecode = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return \"\";\n }\n};\n\n/** Percent-encode, for the case where the upstream echoes an encoded form. */\nconst tryEncode = (value: string): string => {\n try {\n return encodeURIComponent(value);\n } catch {\n return \"\";\n }\n};\n\n/** The origin of a URL, or the value unchanged when it is not a URL (a command). */\nexport const toSafeOrigin = (value: string): string => {\n try {\n return new URL(value).origin;\n } catch {\n return value;\n }\n};\n\n/**\n * Every substring of a URL that could carry a credential, longest first so a\n * broader match is not pre-empted by a narrower one.\n */\nexport const credentialBearingParts = (rawUrl: string): string[] => {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return [rawUrl];\n }\n const parts = new Set<string>([rawUrl]);\n if (parsed.username) parts.add(parsed.username);\n if (parsed.password) parts.add(parsed.password);\n\n // Add a candidate in every form it could be echoed in. Percent-encoding is the\n // trap: `URLSearchParams` DECODES, while the upstream echoes what was on the\n // wire, so a credential containing a reserved character would enter this set in\n // a form that never appears in the text being scrubbed.\n const addEveryForm = (value: string): void => {\n for (const form of [value, tryDecode(value), tryEncode(value)]) {\n if (form && isPlausibleSecret(form)) parts.add(form);\n }\n };\n\n if (parsed.search) {\n const rawQuery = parsed.search.replace(/^\\?/, \"\");\n parts.add(rawQuery);\n // An upstream may echo only the credential's value rather than the whole query\n // string, so each value has to be redactable on its own — the same reason path\n // segments are decomposed below.\n for (const pair of rawQuery.split(\"&\")) {\n const eq = pair.indexOf(\"=\");\n if (eq >= 0) addEveryForm(pair.slice(eq + 1));\n }\n // URLSearchParams also maps \"+\" to a space, which neither raw nor\n // decodeURIComponent produces.\n for (const value of parsed.searchParams.values()) addEveryForm(value);\n }\n if (parsed.pathname && parsed.pathname !== \"/\") {\n // Path forms keep their leading slash, so they cannot match inside a host.\n parts.add(`${parsed.pathname}${parsed.search}`);\n parts.add(parsed.pathname);\n for (const seg of parsed.pathname.split(\"/\")) if (seg) addEveryForm(seg);\n }\n return [...parts].filter((p) => p.length > 0).sort((a, b) => b.length - a.length);\n};\n\n/** Placeholder substituted for anything that could be a credential. */\nexport const REDACTED = \"[REDACTED]\";\n\n/**\n * Redact `rawUrl`'s credential-bearing parts from `text`, keeping its origin.\n *\n * The origin is lifted out behind a sentinel before any redaction and restored\n * afterwards, so \"the origin survives\" is structural rather than dependent on\n * every part being well chosen — an operator must always be able to tell WHICH\n * server failed.\n */\nexport const redactUrlCredentials = (text: string, rawUrl: string | undefined): string => {\n if (!rawUrl) return text;\n const origin = toSafeOrigin(rawUrl);\n const sentinel = \"\\u0000onyx-origin\\u0000\";\n let out = origin ? text.split(origin).join(sentinel) : text;\n for (const part of credentialBearingParts(rawUrl)) {\n if (part === origin) continue;\n out = out.split(part).join(REDACTED);\n }\n return out.split(sentinel).join(origin);\n};\n","import { createInterface } from \"node:readline\";\nimport type { Readable, Writable } from \"node:stream\";\n\n/**\n * Surface a startup failure to the parent MCP client (e.g. OpenCode, Cursor,\n * Claude Code) as a specific message instead of the generic\n * `MCP error -32000: Connection closed`.\n *\n * Why this is needed: the gateway connects to its upstream BEFORE it opens the\n * client-facing stdio transport, so every startup failure happens before the\n * MCP `Server` exists. Exiting at that point closes the stdio pipe with no\n * JSON-RPC reply, and the client renders that as `-32000`. The only thing a\n * stdio MCP client renders as a readable reason is a JSON-RPC **error reply to\n * the `initialize` request** (an error on `tools/list` is shown only as a\n * generic \"Failed to get tools\" — verified against OpenCode).\n *\n * So on a startup failure we briefly read stdin, wait for the client's first\n * request (`initialize`), reply with a JSON-RPC error carrying `reason`, then\n * let the caller exit. This must run ONLY before the real `Server` transport is\n * connected (otherwise two readers would fight over stdin) and ONLY in stdio\n * mode with a real client attached — hence the `isTTY` short-circuit.\n *\n * `input`/`output` default to process.stdin/stdout; they exist for unit tests.\n *\n * Resolves `true` if it replied to a request, `false` otherwise (TTY / no\n * client / timeout / stdin closed). Never rejects.\n */\nexport async function respondInitializeError(\n reason: string,\n opts: { code?: number; input?: Readable; output?: Writable; timeoutMs?: number } = {},\n): Promise<boolean> {\n const code = opts.code ?? -32603; // JSON-RPC InternalError; clients show `message`\n const timeoutMs = opts.timeoutMs ?? 4000;\n const input = opts.input ?? process.stdin;\n const output = opts.output ?? process.stdout;\n\n // Run directly in a terminal (no MCP client over the pipe) → nothing to answer.\n if ((input as { isTTY?: boolean }).isTTY) return false;\n\n return new Promise<boolean>((resolve) => {\n let settled = false;\n let replied = false;\n // Claimed synchronously the moment we decide to answer a request, so several\n // id-bearing requests buffered before the (async) write callback runs can't\n // each emit their own reply — exactly one JSON-RPC error ever hits stdout.\n let claimed = false;\n const rl = createInterface({ input });\n\n const finish = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n rl.removeAllListeners(\"line\");\n rl.close();\n resolve(replied);\n };\n\n // Cap the wait: a piped-but-silent stdin (no client speaking) must not hang.\n const timer = setTimeout(finish, timeoutMs);\n timer.unref?.();\n\n rl.on(\"line\", (line) => {\n if (claimed) return; // a reply is already in flight — ignore the rest\n const trimmed = line.trim();\n if (!trimmed) return;\n let msg: { id?: unknown };\n try {\n msg = JSON.parse(trimmed) as { id?: unknown };\n } catch {\n return; // ignore non-JSON noise\n }\n // The first request carrying an id is `initialize`. Reply to it with the\n // error so the client shows `reason` rather than `-32000`.\n if (msg && msg.id !== undefined && msg.id !== null) {\n claimed = true; // set BEFORE the async write; stop processing buffered lines now\n // We've committed to replying (the bytes are handed to output.write below).\n // Set `replied` here too, so a stdin end/timeout racing the write callback\n // can't resolve false despite the reply having been written.\n replied = true;\n rl.removeAllListeners(\"line\");\n output.write(\n `${JSON.stringify({ error: { code, message: reason }, id: msg.id, jsonrpc: \"2.0\" })}\\n`,\n () => {\n finish();\n },\n );\n }\n });\n\n input.once(\"end\", finish);\n input.once(\"error\", finish);\n });\n}\n","/**\n * Classification of errors from the upstream MCP server connection.\n *\n * The gateway connects to a remote MCP server whose reachability can flap\n * (DNS/VPN/tailscale blips, brief outages). undici's global `fetch` surfaces\n * such failures as `TypeError: fetch failed` with the real OS error on\n * `err.cause` (e.g. `Error: connect ECONNREFUSED` carrying `.code`). These are\n * transient: retrying the connection usually succeeds once connectivity\n * returns. This module decides which errors are worth retrying/reconnecting on,\n * and extracts the OS errno for diagnostics — the fatal-startup path previously\n * logged only the opaque `\"fetch failed\"` message and dropped the errno, which\n * made this class of failure hard to triage.\n *\n * `is401Error` is the single source of truth for \"the upstream is asking us to\n * authenticate\" (also consumed by the connect-time OAuth logic in main.ts). A\n * 401 is deliberately NOT a recoverable connection error — it needs an auth\n * refresh / OAuth flow, not a blind retry.\n */\n\nimport { ProtocolError, UnauthorizedError } from \"@modelcontextprotocol/client\";\n\nimport { messageOf } from \"./errorMessage.js\";\nimport { AccessControlBlockError, ProtocolEraMismatchError } from \"./errors.js\";\nimport { isProtocolEraMismatchError } from \"./protocolEra.js\";\n\n/**\n * OS / undici error codes that indicate a transport-level failure the gateway\n * can recover from by retrying (or reconnecting) the upstream connection. This\n * spans both connect-phase failures (connection never established) and\n * mid-flight drops; the retry-vs-reconnect decision keys off recoverability,\n * while retry SAFETY for side-effecting calls is a separate concern.\n */\nconst RECOVERABLE_ERRNOS = new Set<string>([\n \"EAI_AGAIN\",\n \"ECONNABORTED\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"EHOSTDOWN\",\n \"EHOSTUNREACH\",\n \"ENETDOWN\",\n \"ENETUNREACH\",\n \"ENOTFOUND\",\n \"EPIPE\",\n \"EPROTO\",\n \"ETIMEDOUT\",\n \"UND_ERR_BODY_TIMEOUT\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n]);\n\n/**\n * Connection-phase errnos: the TCP connection was never established, so the\n * request provably never reached the upstream (let alone a tool). Safe to retry\n * even a side-effecting call — it cannot have executed. A mid-flight reset\n * (ECONNRESET/EPIPE) or timeout is deliberately NOT here: the request may have\n * been received and acted on, so retrying it is unsafe (see isPreDispatchError).\n */\nconst PRE_DISPATCH_ERRNOS = new Set<string>([\n \"EAI_AGAIN\",\n \"ECONNREFUSED\",\n \"EHOSTDOWN\",\n \"EHOSTUNREACH\",\n \"ENETDOWN\",\n \"ENETUNREACH\",\n \"ENOTFOUND\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n]);\n\n/**\n * Message fragments that identify a transport failure when no structured errno\n * is available (e.g. the MCP SDK stringifies transport errors into a plain\n * `Error.message`, and `ProxyConnectionError` embeds the underlying message).\n * Kept to unambiguous transport/timeout phrases to avoid misclassifying an\n * application-level error as a connection problem.\n */\n// A pure client-side request timeout (\"MCP error -32001: Request timed out\") is\n// deliberately NOT here: a tool merely exceeding the forward timeout on a healthy\n// connection would otherwise trigger a needless reconnect that closes the client\n// and aborts sibling in-flight calls. Genuine connection death still surfaces as\n// an errno or a \"connection closed\"/\"not connected\" message, which ARE covered.\nconst RECOVERABLE_MESSAGE_RE =\n /\\bfetch failed\\b|socket hang up|other side closed|\\bterminated\\b|network (?:error|is unreachable)|econnrefused|econnreset|econnaborted|etimedout|enotfound|eai_again|enetunreach|ehostunreach|und_err|not connected|transport (?:is )?closed|connection closed/i;\n\n/**\n * Message fragments meaning the request was NOT transmitted — the transport was\n * already closed when we tried to send. Like a connect-phase errno, this is\n * safe to retry even for a side-effecting call: it provably never executed.\n * (The MCP SDK throws \"Not connected\" when its transport is closed.)\n */\nconst PRE_DISPATCH_MESSAGE_RE = /\\bnot connected\\b|transport (?:is )?closed/i;\n\n// MCP-session-expiry wording. The strong set is safe to trust even inside an\n// ProtocolError (a JSON-RPC error from a tool); the general set may appear in\n// app-level tool text and is only trusted for transport-level (non-ProtocolError)\n// errors — see isStaleSessionError.\nconst SESSION_PHRASE_RE =\n /session (?:not found|expired|has expired|is invalid|invalid|mismatch|does not|no longer)|no valid session|unknown session|mcp-session-id/i;\nconst STRONG_SESSION_PHRASE_RE = /no valid session|unknown session|mcp-session-id|session not found/i;\n\n/**\n * Walks the common nested error shapes (`cause`, `response`, `data`, `body`) to\n * find an OS/undici errno string like `ECONNREFUSED`. undici puts the real\n * cause of a `fetch failed` on `err.cause`. Returns undefined when no errno is\n * present. Mirrors the `.cause`-walking idiom in `src/auth/errors.ts`.\n */\nexport function getUpstreamErrno(error: unknown): string | undefined {\n return walkForErrno(error, new WeakSet<object>());\n}\n\n/**\n * Extracts an HTTP-ish status code from the common nested error shapes. The MCP\n * SDK's Streamable-HTTP transport surfaces some failures as an error carrying a\n * numeric `status`/`statusCode`/`code`. Returns undefined when none is present\n * (e.g. a plain `Error POSTing to endpoint: <body>` that dropped the status).\n */\nexport function getUpstreamHttpStatus(error: unknown): number | undefined {\n return walkForStatus(error, new WeakSet<object>());\n}\n\n/**\n * Whether the upstream is asking us to authenticate (HTTP 401 / SDK\n * UnauthorizedError). Extracted from main.ts so the connect-time OAuth handler\n * and the recoverable-error classifier share one definition.\n */\nexport function is401Error(error: unknown): boolean {\n if (error instanceof UnauthorizedError) return true;\n if (error && typeof error === \"object\") {\n const status = (error as { status?: number }).status;\n const code = (error as { code?: number }).code;\n const message = (error as { message?: string }).message;\n if (status === 401 || code === 401) return true;\n // Require auth/HTTP context around a textual 401 so an unrelated transport\n // error whose text merely contains \"401\" (a port like `:401`, \"401 bytes\")\n // isn't classified as auth — this now gates retry-vs-fail-fast, and the\n // structured status/code checks above already cover real 401 responses.\n if (message && /\\bunauthorized\\b|\\b(?:http|status)(?:\\s+(?:status|code))?\\s*[:=]?\\s*401\\b/i.test(String(message)))\n return true;\n }\n return false;\n}\n\n/**\n * Whether an error is safe to retry even for a side-effecting call (tools/call):\n * the request provably never reached tool execution. True for connect-phase\n * network failures (connection never established) and stale-session rejections\n * (rejected at the session gate). Mid-flight resets/timeouts and 5xx are NOT\n * pre-dispatch — the request may already have executed, so the reconnecting\n * client reconnects but does NOT retry those for side-effecting calls.\n */\nexport function isPreDispatchError(error: unknown): boolean {\n // A JSON-RPC ProtocolError is a response from the server's protocol layer, which\n // proves the request was dispatched and processed — so it can NEVER be\n // pre-dispatch, and a side-effecting tools/call must not be retried on it\n // (that is the double-execution bug). The SDK's closed-transport signal\n // (\"Not connected\") is a plain Error, so the intended path below is unaffected.\n if (error instanceof ProtocolError) return false;\n const errno = getUpstreamErrno(error);\n if (errno && PRE_DISPATCH_ERRNOS.has(errno)) return true;\n if (isStaleSessionError(error)) return true;\n return PRE_DISPATCH_MESSAGE_RE.test(messageOf(error));\n}\n\n/**\n * Whether an upstream connection/forwarding error is a transient transport\n * failure worth retrying the connect or reconnecting the session for.\n *\n * Explicitly NOT recoverable: access-control blocks (a policy decision, not a\n * connection problem), 401s (need auth, not a retry), and protocol-era\n * mismatches (the upstream removed the `initialize` handshake we open with —\n * retrying replays the identical unsupported request).\n */\nexport function isRecoverableUpstreamError(error: unknown): boolean {\n if (error instanceof AccessControlBlockError) return false;\n if (error instanceof ProtocolEraMismatchError) return false;\n if (isProtocolEraMismatchError(error)) return false;\n if (is401Error(error)) return false;\n\n const errno = getUpstreamErrno(error);\n if (errno && RECOVERABLE_ERRNOS.has(errno)) return true;\n\n // A terminated/expired MCP session is recoverable by re-initializing.\n if (isStaleSessionError(error)) return true;\n\n // 5xx from the upstream is transient (deploy, overload) — worth a reconnect.\n const status = getUpstreamHttpStatus(error);\n if (status !== undefined && status >= 500) return true;\n\n const message = messageOf(error);\n if (message.length > 0 && RECOVERABLE_MESSAGE_RE.test(message)) return true;\n\n return false;\n}\n\n/**\n * Whether the upstream rejected the request because its MCP session is no longer\n * valid — the spec's 404-on-terminated-session, the 400 \"no valid session\"\n * variant some servers return, or an explicit session-mismatch message. Such a\n * rejection happens at the session gate, BEFORE the tool runs, so re-initializing\n * and retrying is safe.\n */\nexport function isStaleSessionError(error: unknown): boolean {\n const message = messageOf(error);\n // A tool's own ProtocolError may mention \"session\" in app-level text (e.g. surfacing\n // a backing-API auth error) — that is not a stale MCP *session*. Only strong\n // transport-session wording is trusted inside a ProtocolError. A genuine terminated\n // session arrives as an SdkHttpError (HTTP 404), handled below.\n if (error instanceof ProtocolError) return STRONG_SESSION_PHRASE_RE.test(message);\n const status = getUpstreamHttpStatus(error);\n if (status === 404) return true; // spec: server 404s a request bearing a terminated session id\n if (status === 400 && SESSION_PHRASE_RE.test(message)) return true;\n // The status is frequently absent on the thrown error; fall back to the phrase.\n return SESSION_PHRASE_RE.test(message);\n}\n\nfunction walkForErrno(error: unknown, seen: WeakSet<object>): string | undefined {\n if (error == null || typeof error !== \"object\") return undefined;\n if (seen.has(error)) return undefined;\n seen.add(error);\n\n const code = (error as NodeJS.ErrnoException).code;\n // Node errno codes are uppercase strings (ECONNREFUSED, UND_ERR_SOCKET). A\n // numeric `code` here is an HTTP-ish status, not an errno — skip it.\n if (typeof code === \"string\" && /^[A-Z][A-Z0-9_]*$/.test(code)) {\n return code;\n }\n\n for (const key of [\"cause\", \"response\", \"data\", \"body\"] as const) {\n const found = walkForErrno((error as Record<string, unknown>)[key], seen);\n if (found) return found;\n }\n return undefined;\n}\n\nfunction walkForStatus(error: unknown, seen: WeakSet<object>): number | undefined {\n if (error == null || typeof error !== \"object\") return undefined;\n if (seen.has(error)) return undefined;\n seen.add(error);\n\n // An ProtocolError's `code` is a JSON-RPC error code, not an HTTP status. A tool\n // that returns ProtocolError(404, ...) already executed, so reading its code as an\n // HTTP/stale-session signal would retry a side-effecting call (double exec). A\n // genuine transport HTTP status arrives as an SdkHttpError (not ProtocolError).\n const keys =\n error instanceof ProtocolError ? ([\"status\", \"statusCode\"] as const) : ([\"status\", \"statusCode\", \"code\"] as const);\n for (const key of keys) {\n const value = (error as Record<string, unknown>)[key];\n // Only treat 3-digit HTTP-range numbers as a status. A Node errno (string\n // code) or a negative JSON-RPC code (e.g. -32001) is not an HTTP status.\n if (typeof value === \"number\" && value >= 100 && value <= 599) return value;\n }\n\n for (const key of [\"cause\", \"response\", \"data\", \"body\"] as const) {\n const found = walkForStatus((error as Record<string, unknown>)[key], seen);\n if (found !== undefined) return found;\n }\n return undefined;\n}\n","/**\n * Bounded retry-with-backoff for the initial upstream connection.\n *\n * The gateway connects to the upstream MCP server once at startup and treats a\n * failure as fatal (\"MCP server failed to start\") — so a brief reachability\n * blip at the moment the MCP client spawns the gateway kills the whole session\n * (the tools never appear, and the client won't respawn a failed stdio server).\n * Because upstream reachability flaps and self-recovers within seconds, a small\n * bounded retry turns most of these transient failures into a successful start.\n *\n * Retries ONLY on recoverable transport errors (default:\n * `isRecoverableUpstreamError`); a policy block / 401 / config error fails fast\n * so the existing connect-time handlers (interactive OAuth, clear error) still\n * run. Bounded by both `maxRetries` and a hard wall-clock cap (`maxElapsedMs`)\n * so the retry budget never blows the MCP client's own server-startup timeout.\n *\n * Mirrors the retry shape of `src/probe/index.ts`, adding exponential backoff\n * with jitter.\n */\n\nimport { isRecoverableUpstreamError } from \"./upstreamErrors.js\";\n\nexport interface ConnectRetryHooks {\n /** Decide whether an error is worth retrying. Default: recoverable transport errors. */\n isRetryable?: (error: unknown) => boolean;\n /** Injectable clock/sleep for tests. Default: real setTimeout. */\n now?: () => number;\n /** Called before each backoff sleep — used to emit a retry log event. */\n onRetry?: (info: { attempt: number; delayMs: number; error: unknown }) => void;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport interface ConnectRetryOptions {\n /** Delay before the first retry, in ms (grows exponentially per attempt). */\n initialDelayMs: number;\n /** Ceiling for a single backoff delay, in ms. */\n maxDelayMs: number;\n /** Hard wall-clock cap across all attempts, in ms. 0 disables the cap. */\n maxElapsedMs: number;\n /** Retries after the first attempt. 0 disables retry (single attempt). */\n maxRetries: number;\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Runs `connectFn`, retrying recoverable failures with exponential backoff +\n * jitter until it succeeds, `maxRetries` is exhausted, a non-retryable error is\n * thrown, or the `maxElapsedMs` wall-clock budget runs out. Rethrows the last\n * error on give-up so the caller's existing fatal-startup handling applies.\n */\nexport async function connectWithRetry<T>(\n connectFn: () => Promise<T>,\n options: ConnectRetryOptions,\n hooks: ConnectRetryHooks = {}\n): Promise<T> {\n const isRetryable = hooks.isRetryable ?? isRecoverableUpstreamError;\n const sleep = hooks.sleep ?? defaultSleep;\n const now = hooks.now ?? Date.now;\n\n const start = now();\n let attempt = 0;\n\n for (;;) {\n try {\n return await connectFn();\n } catch (error) {\n attempt += 1;\n if (attempt > options.maxRetries || !isRetryable(error)) {\n throw error;\n }\n\n let delayMs = backoffDelayMs(attempt, options);\n\n if (options.maxElapsedMs > 0) {\n const remaining = options.maxElapsedMs - (now() - start);\n // No budget left for another attempt after the wait — give up now.\n if (remaining <= 0) throw error;\n delayMs = Math.min(delayMs, remaining);\n }\n\n hooks.onRetry?.({ attempt, delayMs, error });\n await sleep(delayMs);\n }\n }\n}\n\n/**\n * Exponential backoff (initialDelayMs * 2^(attempt-1), capped at maxDelayMs)\n * with ±20% jitter so concurrent gateways don't retry in lockstep.\n */\nfunction backoffDelayMs(attempt: number, options: ConnectRetryOptions): number {\n const base = Math.min(options.initialDelayMs * 2 ** (attempt - 1), options.maxDelayMs);\n const jitter = base * 0.2 * (Math.random() * 2 - 1);\n return Math.max(0, Math.round(base + jitter));\n}\n","/**\n * Mid-session reconnect for the upstream MCP connection.\n *\n * The gateway connects to the upstream once and hands that client to\n * `proxyServer`, whose handlers capture it in closures. When the upstream drops\n * mid-session (a reachability flap, or a terminated/expired session) every\n * subsequent forwarded call throws forever — the session is dead until the MCP\n * client restarts the gateway process. That mid-session death is what this fixes.\n *\n * `createReconnectingClient` returns a STABLE façade with the exact method set\n * `proxyServer` uses. It holds the live client in a mutable slot; on a\n * recoverable failure it rebuilds the connection (via the injected `reconnect`\n * factory), replays the notification handlers onto the new client, swaps it in,\n * and retries the failed call — all transparently, so `proxyServer` never knows\n * the connection was replaced. Because the façade identity is stable, the\n * handler closures always hit the current client.\n *\n * Contract:\n * - Reconnect only on `isRecoverableUpstreamError` (transport failure / stale\n * session / 5xx). Never on a policy block or 401.\n * - Single-flight + generation guard: N concurrent failures trigger ONE\n * reconnect; a call that failed on an already-replaced client just retries on\n * the new one instead of reconnecting again.\n * - Cooldown collapses bursts; reconnect is driven by traffic (one attempt per\n * failing call), never a background loop — so a sustained outage self-heals on\n * the next call once connectivity returns, without hammering.\n * - Retry SAFETY is method-aware: idempotent reads retry on any recoverable\n * error; a side-effecting `tools/call` retries ONLY on a pre-dispatch error\n * (connection never established / stale-session reject), never on an ambiguous\n * mid-flight failure that may already have executed.\n *\n * The `reconnect` factory MUST NOT open an interactive OAuth browser (see\n * main.ts `allowInteractiveAuth`); a silent token refresh still happens inside\n * the transport's fetch wrapper.\n */\n\nimport type { Client } from \"@modelcontextprotocol/client\";\n\nimport { logger } from \"./logger.js\";\nimport { getUpstreamErrno, isPreDispatchError, isRecoverableUpstreamError } from \"./upstreamErrors.js\";\n\nexport interface ReconnectingClientOptions {\n /** Minimum gap between reconnect attempts (ms) — collapses bursts. */\n cooldownMs: number;\n /** The already-connected client to start from. */\n initial: Client;\n /** Injectable clock for tests. Defaults to Date.now. */\n now?: () => number;\n /**\n * Builds a fresh, connected client. Called on a recoverable mid-session\n * failure. MUST NOT trigger interactive OAuth (no browser mid-session).\n */\n reconnect: () => Promise<Client>;\n}\n\n/**\n * The subset of the MCP SDK `Client` that `proxyServer` forwards through. The\n * real `Client` structurally satisfies this, so the reconnecting façade can\n * stand in for it wherever `proxyServer` expects a client.\n */\nexport type UpstreamClient = Pick<\n Client,\n | \"callTool\"\n | \"close\"\n | \"complete\"\n | \"getPrompt\"\n | \"listPrompts\"\n | \"listResources\"\n | \"listResourceTemplates\"\n | \"listTools\"\n | \"notification\"\n | \"readResource\"\n | \"setNotificationHandler\"\n | \"subscribeResource\"\n | \"unsubscribeResource\"\n>;\n\ntype SetNotificationHandlerArgs = Parameters<Client[\"setNotificationHandler\"]>;\n\nexport function createReconnectingClient(options: ReconnectingClientOptions): UpstreamClient {\n const now = options.now ?? Date.now;\n\n let current = options.initial;\n // Bumped on every successful swap. A forwarded call captures the generation it\n // dispatched under; if it later fails but the generation has moved on, another\n // call already reconnected — so it just retries on the new client.\n let generation = 0;\n let lastAttemptAt = Number.NEGATIVE_INFINITY;\n let reconnecting: null | Promise<boolean> = null;\n let closed = false;\n let sawSubscribe = false;\n const notificationHandlers: SetNotificationHandlerArgs[] = [];\n\n const doReconnect = async (): Promise<boolean> => {\n logger.info(\"Upstream connection lost mid-session; reconnecting\", {\n eventType: \"mcp_upstream_reconnect_attempt\",\n });\n if (sawSubscribe) {\n // A fresh session has no subscriptions; surface it rather than silently\n // dropping resource-update delivery. (Subscription replay is a later step.)\n logger.warn(\"Reconnecting with resource subscriptions active; they will not survive the new session\", {\n eventType: \"mcp_upstream_reconnect_subscriptions_dropped\",\n });\n }\n try {\n const next = await options.reconnect();\n // close() may have run while this reconnect was in flight. Don't publish a\n // fresh client after shutdown (it would leak a live connection) — close it\n // and report failure.\n if (closed) {\n await Promise.resolve()\n .then(() => next.close())\n .catch(() => {});\n return false;\n }\n // Replay recorded notification handlers onto the new client BEFORE swapping\n // so an upstream notification arriving right after the swap is handled.\n for (const args of notificationHandlers) {\n next.setNotificationHandler(...args);\n }\n const old = current;\n current = next;\n generation += 1;\n // Close the old client after publishing the new one, so no new dispatch\n // lands on a closing connection. Best-effort; errors are irrelevant here.\n void Promise.resolve()\n .then(() => old.close())\n .catch(() => {});\n logger.info(\"Upstream reconnected\", { eventType: \"mcp_upstream_reconnect_succeeded\" });\n return true;\n } catch (error) {\n logger.warn(\"Upstream reconnect failed\", {\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_upstream_reconnect_failed\",\n });\n return false;\n }\n };\n\n const ensureReconnected = (genAtDispatch: number): Promise<boolean> => {\n // Someone already reconnected past the generation this call dispatched under.\n if (generation > genAtDispatch) return Promise.resolve(true);\n // Join an in-flight reconnect (single-flight dedup of concurrent failures).\n if (reconnecting) return reconnecting;\n // Cooldown: don't hammer a still-down upstream from a burst of failed calls.\n if (now() - lastAttemptAt < options.cooldownMs) return Promise.resolve(false);\n lastAttemptAt = now();\n reconnecting = doReconnect().finally(() => {\n reconnecting = null;\n });\n return reconnecting;\n };\n\n const forward = async <T>(idempotent: boolean, invoke: (client: Client) => Promise<T>): Promise<T> => {\n const gen = generation;\n try {\n return await invoke(current);\n } catch (error) {\n const swappedUnderUs = generation > gen;\n if (closed || (!swappedUnderUs && !isRecoverableUpstreamError(error))) throw error;\n const reconnected = await ensureReconnected(gen);\n // Retry once, on the (possibly new) current client. A side-effecting call\n // is retried only when the error proves the request never executed.\n if (reconnected && (idempotent || isPreDispatchError(error))) {\n return await invoke(current);\n }\n throw error;\n }\n };\n\n const facade = {\n callTool: (...args: Parameters<Client[\"callTool\"]>) => forward(false, (c) => c.callTool(...args)),\n close: (...args: Parameters<Client[\"close\"]>) => {\n closed = true;\n return current.close(...args);\n },\n complete: (...args: Parameters<Client[\"complete\"]>) => forward(true, (c) => c.complete(...args)),\n getPrompt: (...args: Parameters<Client[\"getPrompt\"]>) => forward(true, (c) => c.getPrompt(...args)),\n listPrompts: (...args: Parameters<Client[\"listPrompts\"]>) => forward(true, (c) => c.listPrompts(...args)),\n listResources: (...args: Parameters<Client[\"listResources\"]>) => forward(true, (c) => c.listResources(...args)),\n listResourceTemplates: (...args: Parameters<Client[\"listResourceTemplates\"]>) =>\n forward(true, (c) => c.listResourceTemplates(...args)),\n listTools: (...args: Parameters<Client[\"listTools\"]>) => forward(true, (c) => c.listTools(...args)),\n // A notification is one-way (no response). Treat it as non-idempotent so it\n // is only resent on a pre-dispatch failure (provably never sent) — never on\n // an ambiguous mid-flight failure, which could duplicate a delivered message.\n notification: (...args: Parameters<Client[\"notification\"]>) => forward(false, (c) => c.notification(...args)),\n readResource: (...args: Parameters<Client[\"readResource\"]>) => forward(true, (c) => c.readResource(...args)),\n setNotificationHandler: (...args: SetNotificationHandlerArgs) => {\n notificationHandlers.push(args);\n return current.setNotificationHandler(...args);\n },\n subscribeResource: (...args: Parameters<Client[\"subscribeResource\"]>) => {\n sawSubscribe = true;\n return forward(true, (c) => c.subscribeResource(...args));\n },\n unsubscribeResource: (...args: Parameters<Client[\"unsubscribeResource\"]>) =>\n forward(true, (c) => c.unsubscribeResource(...args)),\n };\n\n return facade as UpstreamClient;\n}\n","import { Transform } from \"node:stream\";\n\nimport { logger } from \"./lib/logger.js\";\n\nconst MAX_DROPPED_LINES_LOGGED = 50;\n\n/**\n * Filters out lines that do not start with '{' from the input stream.\n * We use this to drop anything that is obviously not a JSON-RPC message.\n */\nexport class JSONFilterTransform extends Transform {\n private buffer = \"\";\n private droppedLinesLogged = 0;\n\n constructor() {\n super({ objectMode: false });\n }\n\n _flush(callback: (error: Error | null, chunk: Buffer | null) => void) {\n // Handle any remaining data in buffer\n if (this.buffer.trim().startsWith(\"{\")) {\n callback(null, Buffer.from(this.buffer));\n } else {\n if (this.buffer.trim().length > 0) {\n this.logDroppedLine(this.buffer);\n }\n callback(null, null);\n }\n }\n\n _transform(chunk: Buffer, _encoding: string, callback: (error: Error | null, chunk: Buffer | null) => void) {\n this.buffer += chunk.toString();\n const lines = this.buffer.split(\"\\n\");\n\n // Keep the last incomplete line in the buffer\n this.buffer = lines.pop() || \"\";\n\n const jsonLines: string[] = [];\n for (const line of lines) {\n if (line.trim().startsWith(\"{\")) {\n jsonLines.push(line);\n } else if (line.trim().length > 0) {\n this.logDroppedLine(line);\n }\n }\n\n if (jsonLines.length > 0) {\n // Send filtered lines with newlines\n const output = `${jsonLines.join(\"\\n\")}\\n`;\n\n callback(null, Buffer.from(output));\n } else {\n callback(null, null);\n }\n }\n\n private logDroppedLine(line: string) {\n // Already past the suppression sentinel — stay silent.\n if (this.droppedLinesLogged > MAX_DROPPED_LINES_LOGGED) return;\n\n // First line past the cap: emit the sentinel exactly once. The increment\n // moves us into the `>` branch above, so subsequent calls fall through.\n if (this.droppedLinesLogged === MAX_DROPPED_LINES_LOGGED) {\n this.droppedLinesLogged += 1;\n logger.info(\"Suppressing further non-JSON stdout lines\", {\n eventType: \"mcp_server_stdout_dropped_suppressed\",\n loggedSoFar: MAX_DROPPED_LINES_LOGGED,\n });\n return;\n }\n\n this.droppedLinesLogged += 1;\n logger.info(\"MCP server stdout (non-JSON, dropped)\", {\n eventType: \"mcp_server_stdout_dropped\",\n line: line.length > 1024 ? `${line.slice(0, 1024)}…` : line,\n });\n }\n}\n","/**\n * Forked from https://github.com/modelcontextprotocol/typescript-sdk/blob/a1608a6513d18eb965266286904760f830de96fe/src/client/stdio.ts\n */\n\nimport type { JSONRPCMessage, Transport } from \"@modelcontextprotocol/client\";\n\nimport {\n ReadBuffer,\n serializeMessage,\n} from \"@modelcontextprotocol/client\";\nimport { type ChildProcess, type IOType, spawn, spawnSync } from \"node:child_process\";\nimport { PassThrough, type Stream } from \"node:stream\";\n\nimport { JSONFilterTransform } from \"./JSONFilterTransform.js\";\nimport { logger } from \"./lib/logger.js\";\nimport { redactCliArgs } from \"./utils/redactCliArgs.js\";\n\nexport type StdioServerParameters = {\n\t/**\n\t * Command line arguments to pass to the executable.\n\t */\n\targs?: string[];\n\n\t/**\n\t * The executable to run to start the server.\n\t */\n\tcommand: string;\n\n\t/**\n\t * The working directory to use when spawning the process.\n\t *\n\t * If not specified, the current working directory will be inherited.\n\t */\n\tcwd?: string;\n\n\t/**\n\t * The environment to use when spawning the process.\n\t *\n\t * If not specified, the result of getDefaultEnvironment() will be used.\n\t */\n\tenv: Record<string, string>;\n\n\t/**\n\t * A function to call when an event occurs.\n\t */\n\tonEvent?: (event: TransportEvent) => void;\n\n\t/**\n\t * When true, spawn the child process using the user's shell.\n\t */\n\tshell?: boolean;\n\n\t/**\n\t * How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`.\n\t *\n\t * The default is \"inherit\", meaning messages to stderr will be printed to the parent process's stderr.\n\t */\n\tstderr?: IOType | number | Stream;\n};\n\ntype TransportEvent =\n\t| {\n\t\t\tchunk: string;\n\t\t\ttype: \"data\";\n\t }\n\t| {\n\t\t\terror: Error;\n\t\t\ttype: \"error\";\n\t }\n\t| {\n\t\t\tmessage: JSONRPCMessage;\n\t\t\ttype: \"message\";\n\t }\n\t| {\n\t\t\ttype: \"close\";\n\t };\n\n/**\n * Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout.\n *\n * This transport is only available in Node.js environments.\n */\nexport class StdioClientTransport implements Transport {\n\tonclose?: () => void;\n\n\tonerror?: (error: Error) => void;\n\tonmessage?: (message: JSONRPCMessage) => void;\n\t/**\n\t * The child process pid spawned by this transport.\n\t *\n\t * This is only available after the transport has been started.\n\t */\n\tget pid(): null | number {\n\t\treturn this._process?.pid ?? null;\n\t}\n\t/**\n\t * PID of the MCP server child captured at \"spawn\" time. Unlike `pid`, this\n\t * survives close() nulling `this._process`, so session-metadata assembly can\n\t * report the child even after it exits. Undefined until spawn. On win32 with\n\t * shell:true this is the cmd wrapper the server runs under — still the direct\n\t * child the OS process tree shows between the gateway and the server.\n\t */\n\tget spawnedServerPid(): number | undefined {\n\t\treturn this._spawnedPid;\n\t}\n\t/**\n\t * Epoch-ms timestamp captured at the child's \"spawn\" event. Undefined until\n\t * spawn. Callers convert to ISO-8601 for the wire.\n\t */\n\tget spawnedServerStartedAt(): number | undefined {\n\t\treturn this._spawnedAt;\n\t}\n\t/**\n\t * The stderr stream of the child process, if `StdioServerParameters.stderr` was set to \"pipe\" or \"overlapped\".\n\t *\n\t * If stderr piping was requested, a PassThrough stream is returned _immediately_, allowing callers to\n\t * attach listeners before the start method is invoked. This prevents loss of any early\n\t * error output emitted by the child process.\n\t */\n\tget stderr(): null | Stream {\n\t\tif (this._stderrStream) {\n\t\t\treturn this._stderrStream;\n\t\t}\n\n\t\treturn this._process?.stderr ?? null;\n\t}\n\tprivate _abortController: AbortController = new AbortController();\n\tprivate _process?: ChildProcess;\n\tprivate _readBuffer: ReadBuffer = new ReadBuffer();\n\tprivate _serverParams: StdioServerParameters;\n\tprivate _spawnedAt?: number;\n\tprivate _spawnedPid?: number;\n\tprivate _stderrStream: null | PassThrough = null;\n\n\tprivate onEvent?: (event: TransportEvent) => void;\n\n\tconstructor(server: StdioServerParameters) {\n\t\tthis._serverParams = server;\n\t\tif (server.stderr === \"pipe\" || server.stderr === \"overlapped\") {\n\t\t\tthis._stderrStream = new PassThrough();\n\t\t}\n\t\tthis.onEvent = server.onEvent;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.onEvent?.({\n\t\t\ttype: \"close\",\n\t\t});\n\n\t\t// Kill the entire process tree to avoid orphaned grandchildren.\n\t\t// AbortController only kills the direct child, but MCP servers\n\t\t// may spawn subprocesses (workers, file watchers, etc.) that\n\t\t// would otherwise be left running as orphans.\n\t\tif (this._process?.pid) {\n\t\t\ttry {\n\t\t\t\tif (process.platform === \"win32\") {\n\t\t\t\t\t// Windows doesn't support negative-PID process group kills.\n\t\t\t\t\t// Use taskkill /T to terminate the entire process tree.\n\t\t\t\t\tconst result = spawnSync(\"taskkill\", [\"/PID\", String(this._process.pid), \"/T\", \"/F\"], {\n\t\t\t\t\t\tstdio: \"ignore\",\n\t\t\t\t\t});\n\t\t\t\t\tif (result.error) {\n\t\t\t\t\t\tthrow result.error;\n\t\t\t\t\t}\n\t\t\t\t\tif (result.status !== 0) {\n\t\t\t\t\t\tthrow new Error(`taskkill exited with status ${result.status}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprocess.kill(-this._process.pid, \"SIGTERM\");\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tlogger.debug(\"Process tree cleanup failed (process may already be dead)\", {\n\t\t\t\t\terror: (error as Error).message,\n\t\t\t\t\tpid: this._process.pid,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tthis._abortController.abort();\n\t\tthis._process = undefined;\n\t\tthis._readBuffer.clear();\n\t}\n\n\tsend(message: JSONRPCMessage): Promise<void> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (!this._process?.stdin) {\n\t\t\t\tthrow new Error(\"Not connected\");\n\t\t\t}\n\n\t\t\tconst json = serializeMessage(message);\n\t\t\tif (this._process.stdin.write(json)) {\n\t\t\t\tresolve();\n\t\t\t} else {\n\t\t\t\tthis._process.stdin.once(\"drain\", resolve);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Starts the server process and prepares to communicate with it.\n\t */\n\tasync start(): Promise<void> {\n\t\tif (this._process) {\n\t\t\tthrow new Error(\n\t\t\t\t\"StdioClientTransport already started! If using Client class, note that connect() calls start() automatically.\",\n\t\t\t);\n\t\t}\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst useShell = this._serverParams.shell ?? false;\n\t\t\t// Node's spawn with shell:true on Windows runs\n\t\t\t// `cmd.exe /d /s /c \"<command> <arg1> <arg2>...\"` by joining the\n\t\t\t// command + args array with single spaces, with no per-token\n\t\t\t// escaping. Any segment containing whitespace (e.g. a path like\n\t\t\t// `C:\\Users\\Foo\\OneDrive - Acme Corp\\bin\\python.exe`) is then\n\t\t\t// re-split by cmd.exe at every space — the child sees a\n\t\t\t// truncated command (`C:\\Users\\Foo\\OneDrive`) plus a few stray\n\t\t\t// tokens and dies with \"not recognized as an internal or\n\t\t\t// external command\" (or, for `python <prefix> ...`, Python\n\t\t\t// follows the first arg as a script directory and reports\n\t\t\t// `can't find '__main__' module in '...'`). Pre-quote each\n\t\t\t// segment containing whitespace or cmd.exe metacharacters so\n\t\t\t// the joined command line round-trips through the parser.\n\t\t\tconst shouldQuote = useShell && process.platform === \"win32\";\n\t\t\tconst spawnCommand = shouldQuote\n\t\t\t\t? quoteForWindowsShell(this._serverParams.command)\n\t\t\t\t: this._serverParams.command;\n\t\t\tconst spawnArgs = shouldQuote\n\t\t\t\t? (this._serverParams.args ?? []).map(quoteForWindowsShell)\n\t\t\t\t: this._serverParams.args ?? [];\n\n\t\t\tthis._process = spawn(\n\t\t\t\tspawnCommand,\n\t\t\t\tspawnArgs,\n\t\t\t\t{\n\t\t\t\t\tcwd: this._serverParams.cwd,\n\t\t\t\t\t// On POSIX, detached creates a new process group (setsid) so we\n\t\t\t\t\t// can kill the entire tree with kill(-pid). On Windows, detached\n\t\t\t\t\t// breaks the child out of the parent's job object, which would\n\t\t\t\t\t// prevent automatic cleanup if the parent crashes — so we skip it.\n\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\tenv: this._serverParams.env,\n\t\t\t\t\tshell: useShell,\n\t\t\t\t\tsignal: this._abortController.signal,\n\t\t\t\t\tstdio: [\"pipe\", \"pipe\", this._serverParams.stderr ?? \"inherit\"],\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tthis._process.on(\"error\", (error) => {\n\t\t\t\tif (error.name === \"AbortError\") {\n\t\t\t\t\t// Expected when close() is called.\n\t\t\t\t\tthis.onclose?.();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlogger.error(\"Process spawn error\", {\n\t\t\t\t\targs: this._serverParams.args ? redactCliArgs(this._serverParams.args) : undefined,\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\tcwd: this._serverParams.cwd,\n\t\t\t\t\terror: error.message,\n\t\t\t\t\terrorCode: (error as NodeJS.ErrnoException).code,\n\t\t\t\t\terrorErrno: (error as NodeJS.ErrnoException).errno,\n\t\t\t\t\terrorSyscall: (error as NodeJS.ErrnoException).syscall,\n\t\t\t\t\toperation: \"spawn\",\n\t\t\t\t\tshell: this._serverParams.shell,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\treject(error);\n\t\t\t\tthis.onerror?.(error);\n\t\t\t});\n\n\t\t\t// Capture pid + spawn time at \"spawn\" time so they remain available in\n\t\t\t// the \"close\" handler. close() nulls `this._process` synchronously\n\t\t\t// before the OS fires the close event, so reading `this._process?.pid`\n\t\t\t// from the close handler would always return undefined on that path.\n\t\t\tlet spawnedAt: number | undefined;\n\t\t\tlet spawnedPid: number | undefined;\n\t\t\tthis._process.on(\"spawn\", () => {\n\t\t\t\tspawnedAt = Date.now();\n\t\t\t\tspawnedPid = this._process?.pid;\n\t\t\t\t// Mirror onto the instance so session-metadata assembly can read the\n\t\t\t\t// spawned child via the public getters.\n\t\t\t\tthis._spawnedAt = spawnedAt;\n\t\t\t\tthis._spawnedPid = spawnedPid;\n\t\t\t\t// Log argCount, not args — MCP servers commonly take secrets as\n\t\t\t\t// CLI args (--api-key, --token, etc.) and these logs land in the\n\t\t\t\t// file transport and remote ingest endpoint.\n\t\t\t\tlogger.info(\"MCP server process spawned\", {\n\t\t\t\t\targCount: this._serverParams.args?.length,\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\teventType: \"mcp_server_process_spawned\",\n\t\t\t\t\tpid: spawnedPid,\n\t\t\t\t});\n\t\t\t\tresolve();\n\t\t\t});\n\n\t\t\tthis._process.on(\"close\", (code, signal) => {\n\t\t\t\tconst uptimeMs = spawnedAt === undefined ? undefined : Date.now() - spawnedAt;\n\t\t\t\tconst cleanExit = code === 0 && signal === null;\n\t\t\t\tconst logLevel = cleanExit ? \"info\" : \"warn\";\n\t\t\t\tlogger[logLevel](\"MCP server process closed\", {\n\t\t\t\t\targCount: this._serverParams.args?.length,\n\t\t\t\t\tcode,\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\teventType: \"mcp_server_process_closed\",\n\t\t\t\t\tpid: spawnedPid,\n\t\t\t\t\tsignal,\n\t\t\t\t\tuptimeMs,\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\ttype: \"close\",\n\t\t\t\t});\n\n\t\t\t\tthis._process = undefined;\n\t\t\t\tthis.onclose?.();\n\t\t\t});\n\n\t\t\tthis._process.stdin?.on(\"error\", (error) => {\n\t\t\t\tlogger.error(\"Stdin error\", {\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\terror: error.message,\n\t\t\t\t\toperation: \"write\",\n\t\t\t\t\tpid: this._process?.pid,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\terror,\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t});\n\n\t\t\t\tthis.onerror?.(error);\n\t\t\t});\n\n\t\t\tconst jsonFilterTransform = new JSONFilterTransform();\n\n\t\t\tthis._process.stdout?.pipe(jsonFilterTransform);\n\n\t\t\tjsonFilterTransform.on(\"data\", (chunk) => {\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\tchunk: chunk.toString(),\n\t\t\t\t\ttype: \"data\",\n\t\t\t\t});\n\n\t\t\t\tthis._readBuffer.append(chunk);\n\t\t\t\tthis.processReadBuffer();\n\t\t\t});\n\n\t\t\tjsonFilterTransform.on(\"error\", (error) => {\n\t\t\t\tlogger.error(\"JSON filter error\", {\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\terror: error.message,\n\t\t\t\t\toperation: \"parse\",\n\t\t\t\t\tpid: this._process?.pid,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\terror,\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t});\n\n\t\t\t\tthis.onerror?.(error);\n\t\t\t});\n\n\t\t\tif (this._stderrStream && this._process.stderr) {\n\t\t\t\tthis._process.stderr.pipe(this._stderrStream);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate processReadBuffer() {\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\tconst message = this._readBuffer.readMessage();\n\n\t\t\t\tif (message === null) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\tmessage,\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t});\n\n\t\t\t\tthis.onmessage?.(message);\n\t\t\t} catch (error) {\n\t\t\t\t// Try to read message for context (may fail)\n\t\t\t\tlet messageType: string = \"unknown\";\n\t\t\t\ttry {\n\t\t\t\t\tconst msg = this._readBuffer.readMessage();\n\t\t\t\t\tif (msg) {\n\t\t\t\t\t\tmessageType = typeof msg;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore error when trying to read message for context\n\t\t\t\t}\n\n\t\t\t\tlogger.error(\"Message processing error\", {\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\terror: (error as Error).message,\n\t\t\t\t\tmessageType,\n\t\t\t\t\toperation: \"process\",\n\t\t\t\t\tpid: this._process?.pid,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\terror: error as Error,\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t});\n\n\t\t\t\tthis.onerror?.(error as Error);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Quote a token for Node's `spawn(..., { shell: true })` on Windows.\n *\n * Returns `s` unchanged when no quoting is needed — preserves the pre-fix\n * shape of the command line for common cases (`npx`, `python`, `node`, etc.),\n * so existing installs aren't re-quoted unnecessarily. Wraps in `\"...\"` and\n * doubles embedded quotes (cmd.exe convention inside a quoted token) when\n * the token contains whitespace or any cmd.exe metacharacter.\n *\n * Exported for the unit test; production code reaches this through\n * `StdioClientTransport.start()`.\n */\nexport function quoteForWindowsShell(s: string): string {\n\tif (s.length === 0) return s;\n\tif (!/[\\s\"&|<>^()]/.test(s)) return s;\n\t// Double embedded quotes (cmd.exe convention inside a quoted token), then\n\t// double any run of trailing backslashes. Under the Windows/MSVC argv rules\n\t// (CommandLineToArgvW), backslashes are only special immediately before a\n\t// quote: an odd count escapes the closing `\"`, so a token ending in `\\` like\n\t// `C:\\Path\\` would otherwise corrupt argv. Doubling the trailing run makes the\n\t// count even, preserving the backslash and terminating the quote correctly\n\t// (e.g. `C:\\Path\\` -> `\"C:\\Path\\\\\"`).\n\tconst escaped = s.replace(/\"/g, '\"\"').replace(/(\\\\+)$/, \"$1$1\");\n\treturn `\"${escaped}\"`;\n}\n","/**\n * OCI image-reference validation for container MCP images.\n *\n * DEFENSE IN DEPTH — not the primary control. The primary control is that the\n * digest lookup passes the image reference as a single argv element to the\n * container runtime (`execFileSync`, no shell — see\n * {@link ./dockerImageDigest.ts}), so no metacharacter can be interpreted by a\n * shell in the first place. This validator additionally refuses to hand a\n * reference that is not a well-formed OCI image reference to the runtime at all,\n * so a malformed/hostile value is rejected and logged before any local process\n * is spawned.\n *\n * The grammar mirrors the OCI distribution spec (the same one\n * `github.com/distribution/reference` implements):\n *\n * reference := name [ \":\" tag ] [ \"@\" digest ]\n * name := [domain \"/\"] path-component [\"/\" path-component]*\n * domain := host [\":\" port]\n * host := domain-name | IPv4 | \"[\" IPv6 \"]\"\n * path-component := [a-z0-9]+ (separator [a-z0-9]+)*\n * separator := \"_\" | \"__\" | \".\" | \"-\"+\n * tag := [A-Za-z0-9_][A-Za-z0-9_.-]{0,127}\n * digest := algorithm \":\" hex\n */\n\n/** The outcome of validating an image reference. `reason` is set iff invalid. */\nexport interface ImageReferenceCheck {\n /** Bounded, log-safe label naming why the reference was rejected. */\n reason?: string;\n valid: boolean;\n}\n\n/**\n * Longest reference we accept. The OCI spec caps a repository name at 255\n * characters; 512 leaves room for a registry host, a tag and a digest while\n * still bounding what reaches the runtime (and the log line).\n */\nconst MAX_REFERENCE_LENGTH = 512;\n\n/**\n * Characters that are meaningful to a POSIX or Windows shell. Used ONLY to\n * describe why a reference the grammar already rejected is dangerous, so the log\n * line names the metacharacter instead of a generic grammar failure.\n */\nconst SHELL_METACHARACTERS = [\n \"$\",\n \"`\",\n \";\",\n \"|\",\n \"&\",\n \">\",\n \"<\",\n \"(\",\n \")\",\n \"{\",\n \"}\",\n \"'\",\n '\"',\n \"\\\\\",\n \"!\",\n \"*\",\n \"?\",\n \"~\",\n \"#\",\n] as const;\n\nconst ALPHA_NUMERIC = \"[a-z0-9]+\";\nconst SEPARATOR = \"(?:[._]|__|-+)\";\nconst PATH_COMPONENT = `${ALPHA_NUMERIC}(?:${SEPARATOR}${ALPHA_NUMERIC})*`;\nconst DOMAIN_COMPONENT = \"(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\";\nconst DOMAIN_NAME = `${DOMAIN_COMPONENT}(?:\\\\.${DOMAIN_COMPONENT})*`;\nconst IPV6_HOST = \"\\\\[[a-fA-F0-9:]+\\\\]\";\nconst DOMAIN = `(?:${DOMAIN_NAME}|${IPV6_HOST})(?::[0-9]+)?`;\nconst NAME = `(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*`;\nconst TAG = \"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}\";\nconst DIGEST = \"[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[0-9a-fA-F]{32,}\";\n\nconst IMAGE_REFERENCE = new RegExp(`^${NAME}(?::${TAG})?(?:@${DIGEST})?$`);\n\n/**\n * Validate a container image reference against the OCI image-reference grammar.\n *\n * @param imageName - The raw, untrusted image reference from the MCP server config\n * @returns `{ valid: true }`, or `{ valid: false, reason }` naming the defect\n */\nexport function validateImageReference(imageName: string): ImageReferenceCheck {\n if (typeof imageName !== \"string\" || imageName.trim() === \"\") {\n return { reason: \"empty image reference\", valid: false };\n }\n\n if (imageName.length > MAX_REFERENCE_LENGTH) {\n return { reason: `image reference longer than ${MAX_REFERENCE_LENGTH} characters`, valid: false };\n }\n\n if (!IMAGE_REFERENCE.test(imageName)) {\n return { reason: describeDefect(imageName), valid: false };\n }\n\n return { valid: true };\n}\n\n/**\n * Describe why a reference that FAILED the grammar is malformed. Diagnostic\n * only — the grammar above is the single authority on validity, so this can\n * never widen what is accepted.\n *\n * @param imageName - A reference already known to be invalid\n * @returns A bounded, log-safe reason that never embeds the reference itself\n */\nfunction describeDefect(imageName: string): string {\n // eslint-disable-next-line no-control-regex -- deliberately matching C0 control characters\n if (/[\\u0000-\\u001f\\u007f]/.test(imageName)) {\n return \"contains a control character\";\n }\n\n if (/\\s/.test(imageName)) {\n return \"contains whitespace\";\n }\n\n const metacharacter = SHELL_METACHARACTERS.find((character) => imageName.includes(character));\n if (metacharacter) {\n return `contains the shell metacharacter ${metacharacter}`;\n }\n\n // An argv element starting with \"-\" is read as an option by the runtime CLI,\n // never as an image name.\n if (imageName.startsWith(\"-\")) {\n return \"starts with '-' and would be parsed as a runtime option\";\n }\n\n return \"does not match the OCI image-reference grammar\";\n}\n","import { execFileSync } from \"node:child_process\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { validateImageReference } from \"./imageReference.js\";\n\n/**\n * A container-based MCP command, as parsed from the CLI arguments.\n *\n * Deliberately carries NO image digests: resolving those invokes the container\n * runtime, which the gateway must not do until the access-control decision has\n * allowed the server. Detection is pure string parsing and safe to run at startup;\n * resolution is {@link getContainerImageDigests}, called after the gate. The Go\n * twin (scanner/internal/container/container.go) keeps them in one struct because\n * the scanner has no access-control gate to run first.\n */\nexport interface ContainerMCPCommand {\n /**\n * The image name/tag used in the command. Untrusted: it comes verbatim from\n * the MCP server config and is validated before it reaches the runtime.\n */\n imageName: string;\n /**\n * Whether the command is a container MCP (docker, podman, etc.)\n */\n isContainerMCP: boolean;\n /**\n * The detected container runtime (docker, podman, nerdctl, finch, etc.)\n */\n runtime: ContainerRuntime;\n}\n\n/**\n * Supported container runtimes\n */\nexport type ContainerRuntime = \"docker\" | \"finch\" | \"nerdctl\" | \"podman\" | \"unknown\";\n\n/**\n * Detect the container runtime from a command string.\n *\n * Supports:\n * - docker / docker.exe\n * - podman / podman.exe\n * - nerdctl / nerdctl.exe\n * - finch / finch.exe\n *\n * @param command - The executable command\n * @returns The detected container runtime or \"unknown\"\n */\nexport function detectContainerRuntime(command: string): ContainerRuntime {\n // Normalize command to lowercase for comparison\n const normalizedCommand = command.toLowerCase();\n\n // Get the base command name (without path)\n const baseName = normalizedCommand.split(/[/\\\\]/).pop() || \"\";\n\n // Check for known container runtimes\n if (baseName === \"docker\" || baseName === \"docker.exe\") {\n return \"docker\";\n }\n if (baseName === \"podman\" || baseName === \"podman.exe\") {\n return \"podman\";\n }\n if (baseName === \"nerdctl\" || baseName === \"nerdctl.exe\") {\n return \"nerdctl\";\n }\n if (baseName === \"finch\" || baseName === \"finch.exe\") {\n return \"finch\";\n }\n\n return \"unknown\";\n}\n\n/**\n * Extract the image name from container run arguments.\n *\n * Docker/Podman/nerdctl run syntax: <runtime> run [OPTIONS] IMAGE [COMMAND] [ARG...]\n *\n * The image is the first argument after all options (which start with - or --).\n * Some options take values: -e, --env, -v, --volume, -p, --publish, etc.\n *\n * @param args - The run arguments (after \"<runtime> run\")\n * @returns The image name or undefined if not found\n */\nexport function extractContainerImageName(args: string[]): string | undefined {\n // Find the index of \"run\" in args\n const runIndex = args.indexOf(\"run\");\n if (runIndex === -1) {\n return undefined;\n }\n\n // Args after \"run\"\n const runArgs = args.slice(runIndex + 1);\n\n // Options that take a value (the next arg is not the image)\n // These are common across Docker, Podman, nerdctl, and finch\n // Sorted alphabetically for linter\n const optionsWithValue = new Set([\n \"--add-host\",\n \"--annotation\",\n \"--arch\",\n \"--attach\",\n \"--blkio-weight\",\n \"--blkio-weight-device\",\n \"--cap-add\",\n \"--cap-drop\",\n \"--cgroup-parent\",\n \"--cgroupns\",\n \"--cidfile\",\n \"--cosign-key\",\n \"--cpu-period\",\n \"--cpu-quota\",\n \"--cpu-rt-period\",\n \"--cpu-rt-runtime\",\n \"--cpu-shares\",\n \"--cpus\",\n \"--cpuset-cpus\",\n \"--cpuset-mems\",\n \"--decryption-key\",\n \"--device\",\n \"--device-cgroup-rule\",\n \"--device-read-bps\",\n \"--device-read-iops\",\n \"--device-write-bps\",\n \"--device-write-iops\",\n \"--dns\",\n \"--dns-option\",\n \"--dns-search\",\n \"--domainname\",\n \"--entrypoint\",\n \"--env\",\n \"--env-file\",\n \"--env-host\",\n \"--expose\",\n \"--gidmap\",\n \"--gpus\",\n \"--group-add\",\n \"--health-cmd\",\n \"--health-interval\",\n \"--health-retries\",\n \"--health-start-period\",\n \"--health-timeout\",\n \"--hostname\",\n \"--image-volume\",\n \"--ip\",\n \"--ip6\",\n \"--ipc\",\n \"--isolation\",\n \"--kernel-memory\",\n \"--label\",\n \"--label-file\",\n \"--link\",\n \"--link-local-ip\",\n \"--log-driver\",\n \"--log-opt\",\n \"--mac-address\",\n \"--memory\",\n \"--memory-reservation\",\n \"--memory-swap\",\n \"--memory-swappiness\",\n \"--mount\",\n \"--name\",\n \"--network\",\n \"--network-alias\",\n \"--oom-kill-disable\",\n \"--oom-score-adj\",\n \"--os\",\n \"--pid\",\n \"--pidfile\",\n \"--pids-limit\",\n \"--platform\",\n \"--pod\",\n \"--privileged\",\n \"--publish\",\n \"--publish-all\",\n \"--pull\",\n \"--requires\",\n \"--restart\",\n \"--rootfs\",\n \"--runtime\",\n \"--secret\",\n \"--security-opt\",\n \"--shm-size\",\n \"--stop-signal\",\n \"--stop-timeout\",\n \"--storage-opt\",\n \"--subgidname\",\n \"--subuidname\",\n \"--sysctl\",\n \"--systemd\",\n \"--timeout\",\n \"--tmpfs\",\n \"--tz\",\n \"--uidmap\",\n \"--ulimit\",\n \"--user\",\n \"--userns\",\n \"--uts\",\n \"--variant\",\n \"--volume\",\n \"--volume-driver\",\n \"--volumes-from\",\n \"--workdir\",\n \"-a\",\n \"-c\",\n \"-e\",\n \"-h\",\n \"-l\",\n \"-m\",\n \"-p\",\n \"-P\",\n \"-u\",\n \"-v\",\n \"-w\",\n ]);\n\n let i = 0;\n while (i < runArgs.length) {\n const arg = runArgs[i];\n\n // Skip options\n if (arg.startsWith(\"-\")) {\n // Check if this option takes a value\n // Handle --option=value format\n if (arg.includes(\"=\")) {\n i++;\n continue;\n }\n\n // Check if the option (without leading dashes) is in our set\n if (optionsWithValue.has(arg)) {\n // Skip this option and its value\n i += 2;\n continue;\n }\n\n // Boolean option, just skip it\n i++;\n continue;\n }\n\n // This is the image name\n return arg;\n }\n\n return undefined;\n}\n\n/** Bound on how long the runtime's `image inspect` may take. */\nconst DIGEST_LOOKUP_TIMEOUT_MS = 5000;\n\n/**\n * Detect whether a command runs a container (Docker, Podman, nerdctl, finch) and\n * extract the image reference from its arguments.\n *\n * PURE string parsing: it spawns no process and touches no shell, so it is safe\n * to run before the access-control decision. Resolving the digests — the step\n * that invokes the container runtime — is {@link getContainerImageDigests}, which\n * runs only after the server has been allowed.\n *\n * @param command - The executable command\n * @param args - Command arguments\n * @returns The detected runtime and (untrusted) image reference\n */\nexport function detectContainerMCP(command: string, args?: string[]): ContainerMCPCommand {\n const runtime = detectContainerRuntime(command);\n\n if (runtime === \"unknown\") {\n return {\n imageName: \"\",\n isContainerMCP: false,\n runtime: \"unknown\",\n };\n }\n\n // Check if there's a \"run\" subcommand in the args\n if (!args || !args.includes(\"run\")) {\n return {\n imageName: \"\",\n isContainerMCP: false,\n runtime,\n };\n }\n\n const imageName = extractContainerImageName(args);\n\n if (!imageName) {\n logger.debug(\"Container command detected but could not extract image name\", {\n args,\n command,\n runtime,\n });\n return {\n imageName: \"\",\n isContainerMCP: true,\n runtime,\n };\n }\n\n return {\n imageName,\n isContainerMCP: true,\n runtime,\n };\n}\n\n/**\n * Get all container image digests using the appropriate runtime's inspect command.\n *\n * The digests are sha256 hashes of the image manifest, which are consistent across\n * registries and uniquely identify specific image versions.\n * An image may have multiple digests if it exists in multiple registries.\n *\n * SECURITY: `imageName` is attacker-controllable — it comes from a (possibly\n * source-controlled) MCP server config. It is therefore\n * 1. validated against the OCI image-reference grammar and skipped-with-a-log if\n * it does not match, and\n * 2. passed to the runtime as a single argv element via `execFileSync` — NEVER\n * interpolated into a shell command string. `execSync` always spawns\n * `/bin/sh -c`, where `$(...)` and backticks stay live inside double quotes, so\n * a crafted reference could execute arbitrary commands as the local user.\n * This mirrors the Go twin's argv call (scanner/internal/container/container.go).\n * This function MUST only be called after the access-control decision (main.ts):\n * it runs a local binary on a value the MCP config controls, so a server the\n * policy denies must never reach it.\n *\n * @param imageName - The untrusted image name/tag to inspect\n * @param runtime - The container runtime to use\n * @returns Array of image digests (sha256 hashes), empty if rejected, not found or locally built\n */\nexport function getContainerImageDigests(imageName: string, runtime: ContainerRuntime): string[] {\n // All supported runtimes use the same inspect syntax\n const runtimeCommand = runtime === \"unknown\" ? \"docker\" : runtime;\n\n const check = validateImageReference(imageName);\n if (!check.valid) {\n // A deliberate skip is an outcome someone has to be able to see: log it at warn\n // with the offending reference. The session/machine ids and the trace id ride on\n // every line via the logger's session context.\n logger.warn(\"Rejected container image reference; skipping digest lookup\", {\n eventType: \"mcp_container_image_ref_rejected\",\n imageName,\n reason: check.reason,\n runtime: runtimeCommand,\n });\n return [];\n }\n\n let result: string;\n try {\n // Get RepoDigests as JSON array - this contains the manifest digests\n // Format: [\"repo@sha256:abc123...\", \"other-repo@sha256:def456...\"] or [] if image was built locally\n result = execFileSync(\n runtimeCommand,\n [\"image\", \"inspect\", \"--format\", \"{{json .RepoDigests}}\", imageName],\n {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n timeout: DIGEST_LOOKUP_TIMEOUT_MS,\n }\n );\n } catch (error) {\n // Image not found locally, or runtime not available\n logger.warn(\"Container image digest lookup failed\", {\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_container_image_digest_lookup_failed\",\n imageName,\n runtime: runtimeCommand,\n });\n return [];\n }\n\n const trimmedResult = result.trim();\n\n // Parse the JSON array of digests. The lookup must stay TOTAL: whatever the\n // runtime prints, it degrades to \"no digests\" and never throws — a digest is\n // metadata, so a surprising `image inspect` output must not take the MCP server\n // down with it. Note a Go nil slice marshals to `null`, not `[]`, and\n // `--format '{{json …}}'` does print `null` for a nil field (docker 29.6.2 on\n // `.Config.Entrypoint`); whether RepoDigests is nil or empty is a daemon\n // implementation detail, so `null` is treated as \"none\", not iterated.\n let parsedResult: unknown;\n try {\n parsedResult = JSON.parse(trimmedResult);\n } catch {\n logger.warn(\"Could not parse RepoDigests JSON\", {\n eventType: \"mcp_container_image_digest_lookup_failed\",\n imageName,\n result: trimmedResult,\n runtime: runtimeCommand,\n });\n return [];\n }\n\n if (!Array.isArray(parsedResult)) {\n logger.warn(\"RepoDigests was not a JSON array\", {\n eventType: \"mcp_container_image_digest_lookup_failed\",\n imageName,\n result: trimmedResult,\n runtime: runtimeCommand,\n });\n return [];\n }\n const repoDigests: unknown[] = parsedResult;\n\n // Extract sha256 digests from all entries\n // Format is typically: \"repo@sha256:abc123...\" or just \"sha256:...\"\n const digests: string[] = [];\n for (const repoDigest of repoDigests) {\n if (typeof repoDigest !== \"string\") {\n continue;\n }\n // Extract just the sha256:... portion\n const digestMatch = repoDigest.match(/sha256:[a-f0-9]{64}/i);\n if (digestMatch) {\n digests.push(digestMatch[0]);\n }\n }\n\n if (digests.length > 0) {\n logger.info(\"Container image digests resolved\", {\n digests,\n eventType: \"mcp_container_image_digests_resolved\",\n imageName,\n runtime: runtimeCommand,\n });\n return digests;\n }\n\n // No digest available (likely a locally built image that was never pushed)\n logger.info(\"No container image digests available (image may be locally built)\", {\n eventType: \"mcp_container_image_digests_empty\",\n imageName,\n runtime: runtimeCommand,\n });\n return [];\n}\n","import { StringDecoder } from \"node:string_decoder\";\nimport { clearInterval, setInterval, setTimeout } from \"node:timers\";\nimport util from \"node:util\";\nimport { Client } from \"@modelcontextprotocol/client\";\nimport { Server, type ServerCapabilities } from \"@modelcontextprotocol/server\";\nimport { serveStdio, StdioServerTransport } from \"@modelcontextprotocol/server/stdio\";\nimport { EventSource } from \"eventsource\";\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\n\nimport { AccessControlAuthorizer } from \"../accessControl/authorizer.js\";\nimport { AccessControlClient } from \"../accessControl/client.js\";\nimport { OAuthCredentialStore, performOAuthFlow } from \"../auth/index.js\";\nimport { discoverAuthServer } from \"../auth/discovery.js\";\nimport { createAuthenticatingFetch, createDynamicAuth, type DynamicAuthHandle } from \"../auth/dynamicAuth.js\";\nimport { initializeOAuthProviders } from \"../auth/oauthProvidersClient.js\";\nimport { config } from \"../config/env.js\";\nimport {\n AccessControlBlockError,\n ConfigurationError,\n ProtocolEraMismatchError,\n ProxyConnectionError,\n} from \"../lib/errors.js\";\nimport { messageOf } from \"../lib/errorMessage.js\";\nimport { describeEraMismatch, HANDSHAKE_ERA_REVISIONS, isProtocolEraMismatchError } from \"../lib/protocolEra.js\";\nimport { redactUrlCredentials, toSafeOrigin } from \"../utils/redactUrl.js\";\nimport { respondInitializeError } from \"../lib/initializeErrorResponder.js\";\nimport { emitKeychainOutcomeLog } from \"../lib/keychainOutcome.js\";\nimport { extractTraceIdFromHeaders, flushLogs, logger } from \"../lib/logger.js\";\nimport { emitProxySetupLog } from \"../lib/proxySetup.js\";\nimport { initializeTrafficMirror } from \"../lib/trafficMirror.js\";\n\n// The live stdio MCP-server transport, registered at creation so the traffic\n// mirror's per-event metadata can report the spawned child's pid + start time.\n// Reconnects re-register, so a respawned server's new pid is picked up on the\n// next scan. Undefined for remote proxies (no spawned child).\nlet activeStdioTransport: StdioClientTransport | undefined;\n\nconst spawnedServerInfoProvider = () => ({\n pid: activeStdioTransport?.spawnedServerPid,\n startedAtMs: activeStdioTransport?.spawnedServerStartedAt,\n});\nimport { connectWithRetry } from \"../lib/connectWithRetry.js\";\nimport { createReconnectingClient, type UpstreamClient } from \"../lib/reconnectingClient.js\";\nimport { upstreamVersionNegotiation } from \"../lib/upstreamNegotiation.js\";\nimport { connectWithTransportFallback, type TransportType } from \"../lib/transportDetection.js\";\nimport { getUpstreamErrno, is401Error } from \"../lib/upstreamErrors.js\";\nimport { logRequestCounts, proxyServer } from \"../proxyServer.js\";\nimport { StdioClientTransport } from \"../StdioClientTransport.js\";\nimport { startHTTPServer } from \"../startHTTPServer.js\";\nimport { type ContainerMCPCommand, detectContainerMCP, getContainerImageDigests } from \"../utils/dockerImageDigest.js\";\nimport { getSessionData, type RemoteSessionCliArgs, type StdioSessionCliArgs } from \"../utils/getUserData.js\";\nimport { normalizeUrl } from \"../utils/normalizeUrl.js\";\nimport { redactCliArgs } from \"../utils/redactCliArgs.js\";\n\nutil.inspect.defaultOptions.depth = 8;\n\nif (!(\"EventSource\" in global)) {\n // @ts-expect-error - figure out how to use --experimental-eventsource with vitest\n global.EventSource = EventSource;\n}\n\n// Export ProxyType for use by other modules\nexport type ProxyType = \"remote\" | \"stdio\";\n\n// sysexits EX_CONFIG (78): a required auth env var resolved empty/unset. Distinct\n// from the generic exit 1 so wrappers and parent agents can tell a configuration\n// error apart from a crash.\nconst EXIT_AUTH_CONFIG = 78;\n\n// PRDCT-6638: make a startup failure visible to ALL three audiences instead of\n// the silent process.exit() that the parent agent only sees as\n// \"-32000: Connection closed\":\n// - the parent AGENT (OpenCode/Cursor/Claude Code) — reply to its `initialize`\n// with a JSON-RPC error whose message is `reason`, so it shows the specific\n// cause instead of -32000 (see lib/initializeErrorResponder). Gated on\n// `respondToClient`, which the caller sets only in stdio mode with the real\n// Server transport NOT yet connected, so we never fight it for stdin;\n// - GROUNDCOVER (+ the local file) — logger.error then flushLogs() BEFORE exit,\n// because process.exit otherwise drops the async transports;\n// - a developer running the gateway directly — an actionable stderr line.\n// keepAlive: flushLogs' timers are .unref()'d and there may be no other pending\n// I/O at this pre-connect exit, so without a ref'd timer Node would drain and\n// exit 13 (unsettled top-level await) before the flush completes. A flush failure\n// must not bypass the intended exit code.\nconst failStartupVisibly = async (opts: {\n exitCode: number;\n logMessage: string;\n meta: Record<string, unknown>;\n reason: string;\n respondToClient: boolean;\n stderrLine: string;\n}): Promise<never> => {\n logger.error(opts.logMessage, opts.meta);\n // Await the stderr write so it drains BEFORE process.exit: writes to a pipe/TTY\n // are async on POSIX, so an un-awaited write can be truncated on exit.\n await new Promise<void>((resolve) => {\n process.stderr.write(`${opts.stderrLine}\\n`, () => resolve());\n });\n if (opts.respondToClient) {\n await respondInitializeError(opts.reason);\n }\n const keepAlive = setInterval(() => {}, 50);\n try {\n await flushLogs(2000);\n } catch {\n // still fail loud with opts.exitCode rather than a generic unhandled rejection\n } finally {\n clearInterval(keepAlive);\n }\n process.exit(opts.exitCode);\n};\n\n// A synthetic auth env var (the scanner lifts a remote server's header value into\n// MCP_GATEWAY_HEADER_*) resolved empty/unset.\nconst exitAuthEnvMissing = (args: {\n envVar: string;\n eventType: string;\n header?: string;\n logMessage: string;\n respondToClient: boolean;\n}): Promise<never> => {\n const target = args.header ? `HTTP header \"${args.header}\"` : \"the bearer token\";\n const reason =\n `Onyx MCP gateway: required auth env var \"${args.envVar}\" for ${target} is empty or unset. ` +\n `Export the source value your agent config references (e.g. the {env:...} / \\${...} token) in the launch environment.`;\n return failStartupVisibly({\n exitCode: EXIT_AUTH_CONFIG,\n logMessage: args.logMessage,\n meta: { envVar: args.envVar, eventType: args.eventType, ...(args.header ? { header: args.header } : {}) },\n reason,\n respondToClient: args.respondToClient,\n stderrLine: `[mcp-gateway] FATAL: ${reason}`,\n });\n};\n\n/**\n * Classify an upstream connect failure as a protocol-era mismatch and throw.\n *\n * One helper rather than three copies of classify-log-throw. There are three\n * sites that need it — the stdio connect, the remote cold-start connect, and the\n * remote outer wrapper that also covers the two OAuth retry connects — and this\n * change already shipped a bug of exactly that shape: the classification lived\n * at one site and the others silently kept producing the old misdiagnosis. A\n * single definition makes \"covered at every site\" checkable by reading the call\n * sites instead of diffing three blocks.\n *\n * `subject` is what the message names: a URL for a remote upstream, the command\n * for a spawned one.\n *\n * Everything that leaves this function is credential-safe. Remote MCP URLs\n * routinely carry secrets in the path or query — `ctx7sk-` (Context7), `tvly-`\n * (Tavily), `sk-`, `github_pat_` — so a raw URL in a log field, in the typed\n * error's metadata, or in the message would put a live credential into logs and\n * telemetry. `ProxyConnectionError` already reduces its `url` to an origin for\n * this reason; the same rule applies here, and to the message text and every\n * caller-supplied extra as well. `originalError` is scrubbed too, because the\n * upstream's own message often echoes the URL it rejected.\n */\nconst throwProtocolEraMismatch = (args: {\n error: unknown;\n extra?: Record<string, unknown>;\n logMessage: string;\n subject: string;\n url?: string;\n}): never => {\n // messageOf, not an inline instanceof check: a JSON-RPC error that never got\n // rehydrated into an Error is a plain object carrying `message`, which String()\n // renders \"[object Object]\" — losing the upstream's own reason from the log.\n const rawMessage = messageOf(args.error);\n const safeSubject = toSafeOrigin(args.subject);\n const scrubUrl = (text: string): string => redactUrlCredentials(text, args.url);\n // Redact every credential-bearing component of the URL, not just the whole\n // string — see credentialBearingParts. The origin survives so the operator can\n // still tell WHICH server; only the secret-carrying parts are replaced.\n const originalError = scrubUrl(rawMessage);\n const safeExtra = Object.fromEntries(\n Object.entries(args.extra ?? {}).map(([k, v]) => [k, typeof v === \"string\" ? scrubUrl(toSafeOrigin(v)) : v])\n );\n\n logger.error(args.logMessage, {\n eventType: \"mcp_protocol_era_mismatch\",\n originalError,\n supportedEra: `${HANDSHAKE_ERA_REVISIONS[0]}..${HANDSHAKE_ERA_REVISIONS[HANDSHAKE_ERA_REVISIONS.length - 1]}`,\n ...safeExtra,\n });\n throw new ProtocolEraMismatchError(scrubUrl(describeEraMismatch(safeSubject, args.error)), args.url, {\n originalError,\n ...safeExtra,\n });\n};\n\nexport const main = async () => {\n // Emit the proxy-dispatcher install/failure record captured during phase 0\n // (see lib/proxySetup.ts). Done here, not at the install site, so it goes\n // through the structured logger (file + ingest) instead of stdio.\n emitProxySetupLog(logger);\n\n // Same deal for the startup api-key resolution outcome captured during\n // phase 2b (see lib/keychainOutcome.ts): re-emit it here so the reader-side\n // `keychain_outcome` event reaches groundcover via the ingest transport,\n // instead of dying on the pre-logger stderr write.\n emitKeychainOutcomeLog(logger);\n\n const argv = await yargs(hideBin(process.argv))\n .scriptName(\"mcp-gateway\")\n .command(\"stdio [command] [args...]\", \"Run a local command with stdio transport (default)\", (yargs) => {\n return yargs\n .positional(\"command\", {\n describe: \"The command to run\",\n type: \"string\",\n })\n .positional(\"args\", {\n array: true,\n describe: \"The arguments to pass to the command\",\n type: \"string\",\n });\n })\n .command(\"remote\", \"Connect to a remote MCP server\", (yargs) => {\n return yargs.options({\n \"bearer-token-env\": {\n describe:\n \"Environment variable name containing a bearer token for remote authentication\",\n type: \"string\",\n },\n \"env-http-headers\": {\n describe:\n \"HTTP headers resolved from environment variables (format: HeaderName:ENV_VAR_NAME)\",\n string: true,\n type: \"array\",\n },\n headers: {\n deprecated: \"Use --env-http-headers with env var names instead. --headers exposes values in process listings.\",\n describe: \"Legacy: headers with literal values (format: key:value)\",\n string: true,\n type: \"array\",\n },\n \"remote-transport\": {\n choices: [\"sse\", \"stream\"],\n deprecated: \"Transport is now auto-detected per MCP spec. This option is ignored.\",\n describe: \"(Deprecated) The transport type - now auto-detected\",\n type: \"string\",\n },\n url: {\n demandOption: true,\n describe: \"The URL of the remote MCP server\",\n type: \"string\",\n },\n });\n })\n .command(\"$0 [command] [args...]\", \"Run a command with MCP arguments (backward compatible)\", (yargs) => {\n return yargs\n .positional(\"command\", {\n describe: \"The command to run\",\n type: \"string\",\n })\n .positional(\"args\", {\n array: true,\n describe: \"The arguments to pass to the command\",\n type: \"string\",\n });\n })\n .env(\"MCP_GATEWAY\")\n .parserConfiguration({\n \"populate--\": true,\n \"unknown-options-as-args\": true,\n })\n .options({\n // NEW: Scanner/Gateway configuration CLI args (these override env vars)\n // Note: These are pre-parsed in mcp-gateway.ts and set as env vars before this file loads\n \"access-control-url\": {\n describe: \"URL of the access control service (overrides MCP_GATEWAY_ACCESS_CONTROL_URL)\",\n type: \"string\",\n },\n debug: {\n default: false,\n describe: \"Enable debug logging\",\n type: \"boolean\",\n },\n env: {\n describe: \"Environment variables to pass to the command (format: KEY=value)\",\n string: true,\n type: \"array\",\n },\n gracefulShutdownTimeout: {\n default: 5000,\n describe: \"The timeout (in milliseconds) for graceful shutdown\",\n type: \"number\",\n },\n \"oauth-providers-url\": {\n describe: \"URL of the OAuth providers config endpoint (overrides MCP_GATEWAY_OAUTH_PROVIDERS_URL)\",\n type: \"string\",\n },\n port: {\n describe: \"Run as HTTP server on the specified port instead of stdio\",\n type: \"number\",\n },\n \"scanner-api-key\": {\n describe: \"API key for scanner authentication (overrides MCP_GATEWAY_SCANNER_API_KEY)\",\n type: \"string\",\n },\n \"scanner-fail-open\": {\n describe: \"Allow requests when scanner is unavailable (overrides MCP_GATEWAY_SCANNER_FAIL_OPEN)\",\n type: \"boolean\",\n },\n \"scanner-timeout-ms\": {\n describe: \"Scanner request timeout in milliseconds (overrides MCP_GATEWAY_SCANNER_TIMEOUT_MS)\",\n type: \"number\",\n },\n \"scanner-url\": {\n describe: \"URL of the scanner/evaluator service (overrides MCP_GATEWAY_SCANNER_URL)\",\n type: \"string\",\n },\n shell: {\n default: process.platform === \"win32\",\n describe: \"Spawn the server via the user's shell (defaults to true on Windows)\",\n type: \"boolean\",\n },\n })\n .help()\n .parseAsync();\n\n // Fetch OAuth provider config from the web extension service before any\n // OAuth flow can run. Fail-safe: on error, the in-memory provider list\n // stays empty and OAuth requests fall through to Dynamic Client\n // Registration. See auth/oauthProvidersClient.ts.\n //\n // Placed AFTER yargs parsing so commands like `--help` / `--version` exit\n // immediately without paying the OAuth fetch latency cost.\n await initializeOAuthProviders(config.oauthProviders);\n\n // Determine the proxy type based on the command\n let proxyType: ProxyType;\n // True once the client-facing stdio transport is connected. The startup-failure\n // path checks this so it only answers `initialize` itself BEFORE the real Server\n // owns stdin (never both — they would fight over the same pipe).\n let clientConnected = false;\n let finalCommand: string | undefined;\n let finalArgs: string[] | undefined;\n let remoteUrl: string | undefined;\n // Transport type used for connection (for logging)\n let usedTransport: TransportType | undefined;\n const remoteHeaders: Record<string, string> = {};\n\n // Parse --env KEY=VALUE args for stdio child processes (general-purpose flag).\n const customEnv: Record<string, string> = {};\n if (argv.env) {\n for (const envVar of argv.env as string[]) {\n const [key, ...valueParts] = envVar.split(\"=\");\n if (key && valueParts.length > 0) {\n customEnv[key.trim()] = valueParts.join(\"=\").trim();\n }\n }\n }\n\n // Resolve an env var reference from process.env. Handles plain env var names\n // (used by --env-http-headers and --bearer-token-env) and ${VAR} template\n // patterns (defense-in-depth, not used by current scanner output).\n // Returns the resolved string, or the name of the first missing/empty var.\n const resolveEnvValue = (value: string): { missingVar?: string; resolved: string } => {\n if (value.includes(\"${\")) {\n let missingVar: string | undefined;\n const resolved = value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_, varName: string) => {\n const val = process.env[varName];\n if (!val) {\n missingVar = varName;\n return \"\";\n }\n return val;\n });\n return missingVar ? { missingVar, resolved: \"\" } : { resolved };\n }\n // Plain env var name\n const val = process.env[value];\n if (!val) {\n return { missingVar: value, resolved: \"\" };\n }\n return { resolved: val };\n };\n\n // Legacy: --headers with literal values (old installs). New installs use\n // --env-http-headers with synthetic env var names instead. Passthrough only.\n if (argv.headers) {\n for (const header of argv.headers as string[]) {\n const [key, ...valueParts] = header.split(\":\");\n if (key && valueParts.length > 0) {\n remoteHeaders[key.trim()] = valueParts.join(\":\").trim();\n }\n }\n }\n\n // Resolve bearer token from environment variable if specified\n if (argv.bearerTokenEnv) {\n const envVarName = argv.bearerTokenEnv as string;\n const { missingVar, resolved: token } = resolveEnvValue(envVarName);\n if (missingVar) {\n await exitAuthEnvMissing({\n envVar: missingVar,\n eventType: \"mcp_auth_bearer_env_missing\",\n logMessage: \"Bearer token env var specified but not set\",\n respondToClient: !argv.port,\n });\n }\n remoteHeaders[\"Authorization\"] = `Bearer ${token}`;\n logger.info(\"Bearer token loaded from environment variable\", {\n authType: \"bearer_token\",\n envVar: envVarName,\n eventType: \"mcp_auth_bearer_env\",\n remoteUrl: argv.url as string,\n });\n }\n\n // Resolve env-http-headers: each entry is either:\n // \"HeaderName:ENV_VAR_NAME\" — full value from a single env var\n // \"HeaderName:Bearer ${ENV_VAR_NAME}\" — template with ${VAR} references resolved from env\n if (argv.envHttpHeaders) {\n for (const entry of argv.envHttpHeaders as string[]) {\n const [headerName, ...valueParts] = entry.split(\":\");\n const valueOrTemplate = valueParts.join(\":\").trim();\n if (headerName && valueOrTemplate) {\n const trimmedHeader = headerName.trim();\n const { missingVar, resolved } = resolveEnvValue(valueOrTemplate);\n\n if (missingVar) {\n await exitAuthEnvMissing({\n envVar: missingVar,\n eventType: \"mcp_auth_env_header_missing\",\n header: trimmedHeader,\n logMessage: \"Env var for HTTP header not set\",\n respondToClient: !argv.port,\n });\n }\n\n remoteHeaders[trimmedHeader] = resolved;\n logger.info(\"HTTP header loaded from environment\", {\n eventType: \"mcp_auth_env_header\",\n header: trimmedHeader,\n remoteUrl: argv.url as string,\n });\n }\n }\n }\n\n // Extract and set trace ID from final resolved headers (legacy + env-backed)\n const headerTraceId = extractTraceIdFromHeaders(remoteHeaders);\n if (headerTraceId) {\n logger.setTraceId(headerTraceId);\n }\n\n if (remoteHeaders.Authorization) {\n const authType = remoteHeaders.Authorization.startsWith(\"Bearer \")\n ? \"bearer_token\"\n : remoteHeaders.Authorization.startsWith(\"Basic \")\n ? \"basic\"\n : \"api_key\";\n\n logger.info(\"Authorization header provided\", {\n authType,\n eventType: \"mcp_auth_header_provided\",\n remoteUrl: argv.url as string,\n });\n }\n\n // Initialize traffic scanner from config\n const scannerHeaders = { ...config.scanner.headers };\n\n logger.debug(\"Initializing traffic scanner\", {\n failOpen: config.scanner.failOpen,\n hasApiKey: !!config.scanner.apiKey,\n scannerEnabled: config.scanner.enabled,\n scannerUrl: config.scanner.url,\n timeoutMs: config.scanner.timeoutMs,\n });\n\n // Centralized handling for ANY startup failure (config/usage error, upstream\n // unreachable, access-control block). Each surfaces a SPECIFIC reason to the\n // agent via an `initialize` error reply (never the opaque -32000), to\n // Groundcover, and to stderr — then exits. Called both from the proxy-type\n // determination below (pre-connect config errors) and the post-connect catch;\n // `clientConnected` decides whether we may still answer the client's stdin.\n const handleStartupFailure = async (error: unknown): Promise<never> => {\n const respondToClient = !argv.port && !clientConnected;\n // A remote URL may carry credentials/query tokens — never surface them. Use\n // origin-only, and scrub any verbatim occurrence the upstream error embedded.\n const safeRemoteUrl = remoteUrl\n ? (() => {\n try {\n return new URL(remoteUrl).origin;\n } catch {\n return \"[invalid remote URL]\";\n }\n })()\n : undefined;\n const scrub = (text: string): string =>\n remoteUrl && safeRemoteUrl ? text.split(remoteUrl).join(safeRemoteUrl) : text;\n\n if (error instanceof AccessControlBlockError) {\n // Onyx access-control block at connect time. error.message carries the\n // org's policy reason (or a default naming Onyx).\n const reason =\n error.message ||\n \"This MCP server has been blocked by your organization's Onyx access-control policy. Contact your administrator if you believe this is an error.\";\n // Prominent multi-line banner on stderr — in addition to the agent-facing\n // initialize error and the Groundcover event — so a developer running the\n // gateway directly (the ticket's --debug flow) sees the block clearly.\n const banner = [\n \"\",\n \"❌ ACCESS DENIED\",\n \"━\".repeat(60),\n reason,\n \"Please contact your administrator if you believe this is an error.\",\n \"\",\n ].join(\"\\n\");\n return failStartupVisibly({\n exitCode: 1,\n logMessage: \"MCP server blocked by access control policy\",\n meta: { error: error.message, eventType: \"mcp_access_control_block\", reason: error.reason },\n reason,\n respondToClient,\n stderrLine: banner,\n });\n }\n\n // Any other startup failure (config/usage error, upstream unreachable, …).\n //\n // A protocol-era mismatch gets its own wording. It must NOT read as \"cannot\n // reach the upstream\": the server is reachable and healthy, and telling an\n // operator to check connectivity sends them after the wrong problem.\n const reason = scrub(\n error instanceof ConfigurationError\n ? `Onyx MCP gateway: configuration error — ${error.message}`\n : error instanceof ProtocolEraMismatchError\n ? `Onyx MCP gateway: unsupported MCP protocol era — ${error.message}`\n : error instanceof ProxyConnectionError\n ? `Onyx MCP gateway: cannot reach the upstream MCP server${safeRemoteUrl ? ` at ${safeRemoteUrl}` : \"\"} — ${error.message}`\n : `Onyx MCP gateway: failed to start — ${error instanceof Error ? error.message : String(error)}`,\n );\n return failStartupVisibly({\n exitCode: 1,\n logMessage: \"MCP server failed to start\",\n meta: {\n args: finalArgs ? redactCliArgs(finalArgs) : undefined,\n command: finalCommand,\n errno: getUpstreamErrno(error),\n error: scrub(`Could not start the proxy: ${error}`),\n eventType: \"mcp_server_start_failed\",\n proxyType,\n remoteUrl: safeRemoteUrl,\n transport: usedTransport,\n },\n reason,\n respondToClient,\n stderrLine: `[mcp-gateway] FATAL: ${reason}`,\n });\n };\n\n // Determine proxy type based on command\n if (argv._[0] === \"remote\") {\n proxyType = \"remote\";\n\n // Warn if deprecated remote-transport argument is used\n if (argv[\"remote-transport\"]) {\n logger.warn(\n \"The --remote-transport option is deprecated and will be ignored. \" +\n \"Transport is now auto-detected per MCP spec (Streamable HTTP with SSE fallback).\",\n { providedTransport: argv[\"remote-transport\"] }\n );\n }\n\n const rawUrl = argv.url as string;\n if (!rawUrl) {\n // Route through the visible-startup handler (not a bare throw) so the agent\n // gets a specific reason rather than -32000.\n await handleStartupFailure(new ConfigurationError(\"Remote URL is required for remote proxy type\", \"url\"));\n }\n // Normalize URL to avoid redirect issues on Windows\n // Windows may convert POST to GET on 301/302 redirects (e.g., trailing slash differences)\n remoteUrl = normalizeUrl(rawUrl);\n if (remoteUrl !== rawUrl) {\n logger.debug(\"Normalized remote URL\", { normalizedUrl: remoteUrl, originalUrl: rawUrl });\n }\n } else {\n // Default to stdio for backward compatibility\n proxyType = \"stdio\";\n\n // If -- separator was used, first item in argv[\"--\"] is the command\n if (argv[\"--\"] && (argv[\"--\"] as string[]).length > 0) {\n const dashDashArgs = argv[\"--\"] as string[];\n finalCommand = dashDashArgs[0];\n // Ensure all args are strings (yargs may parse numbers as numeric types)\n finalArgs = dashDashArgs.slice(1).map(String);\n } else {\n // Use positional arguments\n finalCommand = argv.command as string;\n // Ensure all args are strings (yargs may parse numbers as numeric types)\n finalArgs = ((argv.args as unknown[]) || []).map(String);\n }\n\n if (!finalCommand) {\n // Route through the visible-startup handler (not a bare throw) so the agent\n // gets a specific reason rather than -32000.\n await handleStartupFailure(new ConfigurationError(\"No command specified for stdio proxy\", \"command\"));\n }\n }\n\n // Detect a container-based MCP (Docker, Podman, nerdctl, finch). This is PURE\n // string parsing — no process is spawned. Resolving the image DIGESTS invokes\n // the container runtime with a value the (possibly source-controlled) MCP\n // config controls, so that step is deferred to\n // resolveContainerImageDigests() below, which runs only after the\n // access-control decision has allowed this server.\n let containerMCPCommand: ContainerMCPCommand | undefined;\n if (proxyType === \"stdio\" && finalCommand) {\n containerMCPCommand = detectContainerMCP(finalCommand, finalArgs);\n if (containerMCPCommand.isContainerMCP) {\n logger.info(\"Container MCP detected\", {\n imageName: containerMCPCommand.imageName,\n runtime: containerMCPCommand.runtime,\n });\n }\n }\n\n // Resolved (post-gate) container image digests. `undefined` until\n // resolveContainerImageDigests() has run — never resolved for a server the\n // access-control decision denied.\n let containerImageDigests: string[] | undefined;\n\n // Session identity (machine/session ids), read at most once per process.\n // Resolved LAZILY and awaited ONLY by the paths that need it: the probe behind it\n // is slow on Windows, and a plain stdio server must not pay for it on the way to\n // being spawned (that latency is what a client sees as a hung start). Two paths\n // do await it: the access-control request, which awaited it before this was lazy,\n // and the container digest lookup, which needs the ids on its own log lines — so\n // a CONTAINER server does wait for it pre-spawn. That is cheap in the deployed\n // shape (access control configured ⇒ already resolved) and only a first read in\n // dev runs without access control. Keep it that way if you reorder this.\n let sessionDataOnce: ReturnType<typeof getSessionData> | undefined;\n const attachSessionDataToLogger = async (): Promise<void> => {\n sessionDataOnce ??= getSessionData();\n logger.setSessionData({ ...(await sessionDataOnce), proxyType });\n };\n\n /**\n * Invoke the container runtime to resolve the MCP server's image digests.\n *\n * SECURITY: this is the one step that runs a local binary\n * with a value taken from the MCP server config, so it MUST NOT run before\n * accessControlAuthorizer.isAllowed() has allowed the server — otherwise a\n * server Onyx policy denies still causes local execution. Idempotent, so a\n * mid-session reconnect does not re-inspect the image.\n */\n const resolveContainerImageDigests = async (): Promise<void> => {\n if (containerImageDigests !== undefined || !containerMCPCommand?.isContainerMCP || !containerMCPCommand.imageName) {\n return;\n }\n // Attach the machine/session ids first, so the lookup's own outcome logs —\n // the validator's deliberate skip, the failure path and the success path —\n // carry ids that tie the line to this host and session.\n await attachSessionDataToLogger();\n containerImageDigests = getContainerImageDigests(\n containerMCPCommand.imageName,\n containerMCPCommand.runtime\n );\n };\n\n const buildSessionCliArgs = (): RemoteSessionCliArgs | StdioSessionCliArgs =>\n proxyType === \"stdio\"\n ? {\n args: finalArgs as string[],\n command: finalCommand as string,\n dockerImageDigests: containerImageDigests,\n dockerImageName: containerMCPCommand?.imageName,\n proxyType,\n }\n : {\n proxyType,\n url: remoteUrl as string,\n };\n\n // Initialize the access control client if configured. NOTE: the session data\n // sent with the authorize request carries dockerImageName but not the digests —\n // they are resolved only after this decision. The backend's MCP access-control\n // path keys on the command+args cache hash and resolves the version from its\n // own mcp_latest_version cache, so the decision is unaffected.\n let accessControlClient: AccessControlClient | undefined;\n if (config.accessControl.url && config.scanner.apiKey) {\n const cliArgs = buildSessionCliArgs();\n // This path already awaited the session identity before the fix, so reading\n // it here adds no startup latency. It also makes the ids available to every\n // log line from here on, digest lookup included.\n await attachSessionDataToLogger();\n const sessionDataBase = await sessionDataOnce;\n\n const sessionData =\n cliArgs.proxyType === \"stdio\"\n ? {\n ...sessionDataBase,\n proxyType: \"stdio\" as const,\n stdioCliArgs: { ...cliArgs, args: redactCliArgs(cliArgs.args) },\n }\n : {\n ...sessionDataBase,\n proxyType: \"remote\" as const,\n remoteCliArgs: cliArgs,\n };\n\n accessControlClient = new AccessControlClient({\n apiKey: config.scanner.apiKey,\n headers: config.accessControl.headers,\n sessionData,\n timeoutMs: config.accessControl.timeoutMs,\n url: config.accessControl.url,\n });\n }\n\n // Determine server name for error messages\n const serverName = proxyType === \"remote\" ? remoteUrl : finalCommand;\n\n // Initialize access control authorizer\n const accessControlAuthorizer = new AccessControlAuthorizer({\n client: accessControlClient,\n enabled: config.accessControl.enabled,\n serverName,\n });\n\n // Initialize OAuth credential store for remote connections\n const oauthCredentialStore = new OAuthCredentialStore({\n storageDir: config.oauthDir,\n });\n\n // Factory function to create a new Client instance. EVERY upstream connect\n // path — initial connect, transport fallback, bounded retry, and mid-session\n // reconnect — builds its client here, so the era-negotiation policy below is\n // uniform: a reconnect negotiates exactly like the session it replaces.\n const createClient = (): Client =>\n new Client(\n {\n name: \"mcp-gateway\",\n version: config.appVersion,\n },\n {\n capabilities: {},\n versionNegotiation: upstreamVersionNegotiation(),\n }\n );\n\n const connect = async (\n client: Client,\n { allowInteractiveAuth = true }: { allowInteractiveAuth?: boolean } = {}\n ): Promise<Client> => {\n // Check access control (async - may query backend)\n const isAllowed = await accessControlAuthorizer.isAllowed();\n if (!isAllowed) {\n const reason = accessControlAuthorizer.getBlockReason();\n logger.warn(\"MCP server blocked by access control\", {\n proxyType,\n reason,\n });\n\n throw new AccessControlBlockError(reason, \"policy_block\");\n }\n\n logger.info(\"MCP server allowed by access control\");\n\n // ONLY NOW may the container runtime be invoked for this server's image\n // digests: the access-control decision has resolved and allowed it. Running it\n // earlier is what let a policy-denied server achieve local host execution\n // before the gateway enforced its own control.\n await resolveContainerImageDigests();\n\n if (proxyType === \"stdio\") {\n // Merge custom CLI environment variables with process.env for child processes\n // Note: This is for MCP server processes, not the proxy's internal configuration\n const mergedEnv = {\n ...process.env,\n ...customEnv,\n } as Record<string, string>;\n\n const transport = new StdioClientTransport({\n args: finalArgs,\n command: finalCommand as string,\n env: mergedEnv,\n onEvent: (event) => {\n if (argv.debug) {\n logger.debug(\"Transport event\", { event });\n }\n },\n shell: argv.shell,\n // Pipe stderr so we can both forward to the parent agent client (preserving\n // the previous \"inherit\" behavior) AND log each line into the gateway's\n // structured log. Without this we have no record of why a spawned MCP\n // server crashed during startup.\n stderr: \"pipe\",\n });\n activeStdioTransport = transport;\n\n // Cap each captured stderr line so a misbehaving server emitting multi-MB\n // stack dumps or base64 blobs to stderr can't balloon log entries or the\n // in-memory tail buffer. Matches JSONFilterTransform.logDroppedLine.\n const STDERR_LINE_MAX = 1024;\n // If the child never emits a newline we still need an upper bound on the\n // pending line buffer — otherwise a binary blob to stderr would grow\n // memory unbounded and defeat the per-line cap.\n const STDERR_BUFFER_MAX = STDERR_LINE_MAX * 8;\n const truncate = (line: string): string =>\n line.length > STDERR_LINE_MAX ? `${line.slice(0, STDERR_LINE_MAX)}…` : line;\n\n let stderrLineBuffer = \"\";\n const recentStderr: string[] = [];\n const logStderrLine = (line: string) => {\n const truncated = truncate(line);\n recentStderr.push(truncated);\n if (recentStderr.length > 100) recentStderr.shift();\n logger.info(\"MCP server stderr\", {\n command: finalCommand,\n eventType: \"mcp_server_stderr\",\n line: truncated,\n });\n };\n const flushBufferAsLine = () => {\n if (stderrLineBuffer.length === 0) return;\n logStderrLine(stderrLineBuffer);\n stderrLineBuffer = \"\";\n };\n // Decoder buffers incomplete multi-byte UTF-8 sequences across chunk\n // boundaries — without it `chunk.toString()` would emit replacement\n // characters when a codepoint straddles a chunk.\n const stderrDecoder = new StringDecoder(\"utf8\");\n // Drain any pending decoder bytes + remaining line buffer. Idempotent —\n // both `end` and `close` may fire, and the connect-failure catch below\n // also drains explicitly so we don't depend on the SDK ordering.\n let stderrTailDrained = false;\n const drainStderrTail = () => {\n if (stderrTailDrained) return;\n stderrTailDrained = true;\n const decoderTail = stderrDecoder.end();\n if (decoderTail) {\n stderrLineBuffer += decoderTail;\n }\n flushBufferAsLine();\n };\n transport.stderr?.on(\"data\", (chunk: Buffer) => {\n // Forward raw bytes to the parent's stderr to preserve the byte-fidelity\n // the previous `stderr: \"inherit\"` provided. The structured-log path\n // below uses StringDecoder for proper UTF-8 handling.\n process.stderr.write(chunk);\n stderrLineBuffer += stderrDecoder.write(chunk);\n const lines = stderrLineBuffer.split(\"\\n\");\n stderrLineBuffer = lines.pop() ?? \"\";\n for (const line of lines) {\n if (line.length === 0) continue;\n logStderrLine(line);\n }\n // Cap residual partial line — only after extracting any complete lines\n // above, otherwise a chunk containing newlines + an oversize tail would\n // silently discard the parseable lines together with the tail.\n if (stderrLineBuffer.length > STDERR_BUFFER_MAX) {\n flushBufferAsLine();\n }\n });\n transport.stderr?.once(\"end\", drainStderrTail);\n transport.stderr?.once(\"close\", drainStderrTail);\n\n try {\n await client.connect(transport);\n return client;\n } catch (error) {\n drainStderrTail();\n logger.warn(\"Failed original server access\", {\n command: finalCommand,\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_server_access_failed\",\n proxyType,\n stderrTail: recentStderr.slice(-20),\n });\n // Tear down the spawned child before rethrowing. connectWithStartupRetry\n // re-runs connect() on a recoverable failure, so without this each retry\n // would orphan the previous attempt's stdio subprocess.\n await transport.close().catch(() => {});\n // A locally-spawned server can drop the handshake exactly like a remote\n // one, and it must not be reported as unreachable — we spawned it and it\n // answered. Classified here as well as on the remote leg because these\n // are two independent wrap sites; covering only one leaves the other\n // producing the misdiagnosis this classification exists to remove.\n if (isProtocolEraMismatchError(error)) {\n throwProtocolEraMismatch({\n error,\n extra: { command: finalCommand },\n logMessage: \"Spawned MCP server speaks a different protocol era; cannot negotiate a session\",\n subject: finalCommand ?? \"the spawned MCP server\",\n });\n }\n throw new ProxyConnectionError(`Failed to connect to stdio process: ${error}`, undefined, \"stdio\", {\n argCount: finalArgs?.length,\n command: finalCommand,\n }, error);\n }\n } else if (proxyType === \"remote\") {\n // Connect to remote server\n // Strategy: Try Streamable HTTP first, fall back to SSE if not supported (404/405)\n // Handle OAuth (401) separately - if auth fails, get token and retry the whole process\n\n const url = remoteUrl as string;\n\n // Build request init carrying the caller-supplied headers. An OAuth\n // bearer is NOT set here — it's injected per-request by the dynamic-auth\n // fetch wrapper below, so that mid-session refreshes pick up the latest\n // bearer without needing to recreate the transport (PRDCT-3666 Tier 2).\n // A static Authorization (api-key / Basic) MAY be present in remoteHeaders;\n // the wrapper preserves it when there is no OAuth bearer (PRDCT-5136).\n const buildNonAuthRequestInit = (): RequestInit | undefined => {\n const headers: Record<string, string> = { ...remoteHeaders };\n return Object.keys(headers).length > 0 ? { headers } : undefined;\n };\n\n // Get OAuth token (from cache or via flow)\n const getOAuthToken = async (): Promise<string> => {\n const oauthResult = await performOAuthFlow(url, { credentialStore: oauthCredentialStore });\n logger.info(\"Authentication performed\", {\n authType: \"oauth\",\n eventType: \"mcp_auth_performed\",\n fromCache: oauthResult.fromCache,\n remoteUrl: url,\n });\n return oauthResult.tokens.access_token;\n };\n\n let connectedClient: Client;\n try {\n // Check for cached OAuth token\n const cached = await oauthCredentialStore.load(url);\n const initialBearer =\n cached && oauthCredentialStore.hasValidAccessToken(cached) ? cached.tokens?.access_token : undefined;\n\n // Dynamic auth handle: holds the in-memory bearer. The transport's\n // custom fetch reads from this handle on every request, refreshes\n // mid-session on 401, and dedupes concurrent refreshes (PRDCT-3666\n // Tier 2). When `performOAuthFlow` produces fresh tokens (cold start\n // or browser fallback), we feed them back into the handle so future\n // requests use the new bearer too.\n const dynAuth: DynamicAuthHandle = createDynamicAuth(url, oauthCredentialStore, initialBearer);\n const authFetch = createAuthenticatingFetch(dynAuth);\n\n let usedFreshOAuth = false; // Track if we've already tried with a fresh token\n\n try {\n // Try connecting with transport fallback (Streamable HTTP → SSE)\n // Uses a client factory to create fresh clients for each transport attempt\n const result = await connectWithTransportFallback(\n createClient,\n url,\n buildNonAuthRequestInit(),\n authFetch,\n );\n connectedClient = result.client;\n usedTransport = result.transportType;\n logger.info(\"Connected to remote server\", { hasAuth: !!initialBearer, transport: usedTransport });\n } catch (err) {\n // Protocol-era mismatch is classified FIRST, ahead of every auth and\n // transport branch below, because those branches actively misdiagnose\n // it. A modern-era upstream rejects our `initialize` as an unknown\n // method; that is a non-401 with no credential attached, which is\n // exactly the shape the cold-start OAuth-metadata recovery below looks\n // for. Measured against a crafted modern-only server, the\n // unclassified path issued six `.well-known` OAuth probes against a\n // server whose only problem was its protocol era, then died ~28s later\n // having sent the agent nothing at all. Neither auth nor a different\n // transport can bridge two eras, so fail immediately and say why.\n if (isProtocolEraMismatchError(err)) {\n throwProtocolEraMismatch({\n error: err,\n extra: { remoteUrl: url },\n logMessage: \"Upstream speaks a different MCP protocol era; cannot negotiate a session\",\n subject: url,\n url,\n });\n }\n\n // Handle 401 errors - need OAuth authentication or token refresh\n if (is401Error(err)) {\n // If we already tried with a fresh OAuth token, don't retry again (avoid infinite loop)\n if (usedFreshOAuth) {\n throw err;\n }\n\n // PRDCT-3666: when the cached token was rejected by upstream and\n // the dynamic-auth fetch wrapper couldn't silently recover (no\n // refresh_token on disk, or invalid_grant, or transient failure\n // mid-retry), fall through to the full interactive OAuth flow.\n // Tier 1 already preserved refresh_token across transient errors;\n // Tier 2 added the mid-session refresh inside `authFetch`. By the\n // time we land here, both fast paths have been exhausted and a\n // browser popup is the spec-correct next step (matches what a\n // non-gateway MCP client would do).\n if (dynAuth.getCurrentBearer() ?? initialBearer) {\n logger.info(\"Cached + refresh both rejected; falling back to full OAuth flow\");\n\n // Mark the cached access_token as expired BEFORE falling back.\n //\n // Without this, `performOAuthFlow` → `tryLoadExistingCredentials`\n // would happily return the SAME dead access_token (its\n // time-based `hasValidAccessToken` check can't know upstream\n // rejected it), we'd set that dead token as the bearer, retry,\n // get 401 again, and the `usedFreshOAuth` guard would\n // propagate the error WITHOUT ever opening a browser. Users\n // on no-refresh_token servers (ClickUp, GitHub Copilot,\n // Sentry) would be permanently stuck.\n //\n // Clearing `tokensObtainedAt` is enough — `isAccessTokenExpired`\n // returns true when it's missing, so `hasValidAccessToken`\n // returns false. We deliberately preserve:\n // - `clientInfo` (so we don't have to re-DCR)\n // - `tokens.refresh_token` (a transient refresh failure may\n // have just left it on disk; the next attempt may succeed)\n await oauthCredentialStore.update(url, { tokensObtainedAt: undefined });\n } else {\n logger.info(\"Server requires OAuth, starting authentication flow...\");\n }\n\n // A mid-session reconnect must never open a browser (the dynamicAuth\n // contract). The silent refresh already ran inside authFetch; a\n // surviving 401 is surfaced (non-recoverable) and the user re-auths\n // interactively on the next fresh connect.\n if (!allowInteractiveAuth) {\n logger.info(\"Skipping interactive OAuth on reconnect; surfacing 401\", {\n eventType: \"mcp_reconnect_skip_interactive_auth\",\n });\n throw err;\n }\n\n const freshToken = await getOAuthToken();\n dynAuth.setBearer(freshToken);\n usedFreshOAuth = true;\n // If even the freshly-issued OAuth token is then rejected, the\n // error propagates to the outer catch (wrapped as\n // ProxyConnectionError) — usedFreshOAuth gates further retries.\n const result = await connectWithTransportFallback(\n createClient,\n url,\n buildNonAuthRequestInit(),\n authFetch,\n );\n connectedClient = result.client;\n usedTransport = result.transportType;\n logger.info(\"Connected to remote server with OAuth\", { transport: usedTransport });\n } else if (\n !usedFreshOAuth &&\n !(dynAuth.getCurrentBearer() ?? initialBearer) &&\n !new Headers(remoteHeaders).has(\"Authorization\")\n ) {\n // COLD-START non-401 recovery. We sent NO credential at all — neither\n // an OAuth bearer NOR a caller-supplied static Authorization (api-key\n // / Basic / bearer via --bearer-token-env / --headers /\n // --env-http-headers) — and the upstream rejected with a non-401. It\n // may be an OAuth-protected server that answers an unauthenticated\n // request non-compliantly — e.g. Okta's llm.atko.ai returns 500\n // instead of a spec-correct 401. `is401Error` is false here and the\n // transport layer only falls back on 404/405, so without this branch\n // the gateway would give up without ever authenticating, even though\n // the server exposes standard, discoverable OAuth metadata.\n //\n // Deliberately scoped to the NO-CREDENTIAL case (cronyx + coderabbit,\n // PR #8418). If we had ALREADY sent a credential (a cached OAuth token\n // OR a static Authorization) and got a non-401 (500/502/503/429), that\n // is NOT evidence the credential is bad — a genuine rejection is a\n // 401, handled by the branch above. A non-401 on an authenticated\n // request is most likely transient (deploy, rate-limit), so re-authing\n // there would wipe a still-valid token on disk and pop a browser on a\n // blip. We let that case fall through to `throw err` and propagate the\n // original error unchanged.\n //\n // Probe the well-known OAuth metadata directly (independent of any\n // 401). Only if an authorization server is actually advertised do we\n // run the OAuth flow once and retry; otherwise propagate the ORIGINAL\n // error (just +1 well-known probe of latency on an already-doomed\n // connection). Because we only get here with no bearer, there is no\n // cached token to invalidate first — `performOAuthFlow` does a clean\n // fresh acquisition.\n let advertisesOAuth = false;\n let discoveryError: string | undefined;\n try {\n const { authMetadata } = await discoverAuthServer(new URL(url), { logger });\n advertisesOAuth = Boolean(authMetadata);\n } catch (discoveryErr) {\n discoveryError = discoveryErr instanceof Error ? discoveryErr.message : String(discoveryErr);\n }\n if (!advertisesOAuth) {\n // Surface at warn so a production recurrence shows WHY recovery did\n // not happen: the upstream rejected us with a non-401 AND exposed\n // no discoverable OAuth metadata, so there is nothing to bootstrap\n // from. (discoverAuthServer logs the individual metadata-probe\n // results — protected-resource + auth-server + registration_endpoint\n // — separately.)\n logger.warn(\"Non-401 failure and no discoverable OAuth metadata; propagating original error\", {\n discoveryError,\n eventType: \"mcp_non401_no_oauth_discovery\",\n originalError: err instanceof Error ? err.message : String(err),\n remoteUrl: url,\n });\n throw err;\n }\n logger.info(\"Cold-start non-401 but server advertises OAuth; attempting OAuth flow\", {\n originalError: err instanceof Error ? err.message : String(err),\n });\n if (!allowInteractiveAuth) {\n logger.info(\"Skipping interactive OAuth on reconnect; surfacing error\", {\n eventType: \"mcp_reconnect_skip_interactive_auth\",\n });\n throw err;\n }\n const freshToken = await getOAuthToken();\n dynAuth.setBearer(freshToken);\n usedFreshOAuth = true;\n const result = await connectWithTransportFallback(\n createClient,\n url,\n buildNonAuthRequestInit(),\n authFetch,\n );\n connectedClient = result.client;\n usedTransport = result.transportType;\n logger.info(\"Connected to remote server with OAuth (recovered from non-401)\", {\n transport: usedTransport,\n });\n } else {\n throw err;\n }\n }\n return connectedClient;\n } catch (error) {\n logger.warn(\"Failed original server access\", {\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_server_access_failed\",\n proxyType,\n remoteUrl: url,\n });\n if (error instanceof ProxyConnectionError) throw error;\n // An era mismatch must survive this wrapper. Re-wrapping it as a\n // connection error would restore the very misdiagnosis this classification\n // exists to remove — the operator would be told to check reachability for\n // a server that answered us perfectly well.\n if (error instanceof ProtocolEraMismatchError) throw error;\n // Classify a RAW era error here too, not just an already-wrapped one.\n // The inner classification runs on the cold-start connect only; the two\n // retry connects in this same catch (post-401 OAuth, and the non-401\n // OAuth-discovery recovery) are not covered by it, so an era error\n // thrown by either would otherwise arrive here unclassified and be\n // wrapped as unreachable.\n if (isProtocolEraMismatchError(error)) {\n throwProtocolEraMismatch({\n error: error,\n extra: { remoteUrl: url },\n logMessage: \"Upstream speaks a different MCP protocol era; cannot negotiate a session\",\n subject: url,\n url,\n });\n }\n throw new ProxyConnectionError(\n `Failed to connect to remote server: ${error instanceof Error ? error.message : String(error)}`,\n url,\n usedTransport,\n { headerCount: Object.keys(remoteHeaders).length },\n error\n );\n }\n }\n\n // Should never reach here, but satisfy TypeScript\n return client;\n };\n\n // Connect to the upstream with bounded retry on transient transport failures,\n // so a brief reachability blip when the gateway starts doesn't fail the whole\n // session. connect() returns a fresh client per attempt via createClient().\n const connectWithStartupRetry = (): Promise<Client> =>\n connectWithRetry(() => connect(createClient()), config.upstream.connect, {\n onRetry: ({ attempt, delayMs, error }) => {\n logger.warn(\"Upstream connect failed; retrying\", {\n attempt,\n delayMs,\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_upstream_connect_retry\",\n proxyType,\n });\n },\n });\n\n // Whether to wrap the upstream in the mid-session reconnecting façade. Remote\n // by default; stdio is opt-in (a stdio \"reconnect\" respawns the child and\n // loses its in-memory state). The reconnect factory passes\n // allowInteractiveAuth:false so a mid-session reconnect never opens a browser.\n const reconnectEnabled =\n proxyType === \"remote\"\n ? config.upstream.reconnect.enabled\n : config.upstream.reconnect.enabledForStdio;\n const wrapUpstream = (connected: Client): UpstreamClient =>\n reconnectEnabled\n ? createReconnectingClient({\n cooldownMs: config.upstream.reconnect.cooldownMs,\n initial: connected,\n reconnect: () => connect(createClient(), { allowInteractiveAuth: false }),\n })\n : connected;\n\n // The traffic mirror snapshots the session data — including the container image\n // digests — when it is initialized, so it must be initialized AFTER the first\n // connect() resolved them (i.e. after the access-control gate). Memoized: one\n // mirror per process, however many sessions connect.\n let trafficMirrorInit: Promise<unknown> | undefined;\n const ensureTrafficMirror = (): Promise<unknown> => {\n trafficMirrorInit ??= initializeTrafficMirror({\n cliArgs: buildSessionCliArgs(),\n scanApiKey: config.scanner.apiKey,\n // Scanner config from env vars\n scanEnabled: config.scanner.enabled,\n scanFailOpen: config.scanner.failOpen,\n scanHeaders: scannerHeaders,\n scanTimeoutMs: config.scanner.timeoutMs,\n scanUrl: config.scanner.url || \"\",\n spawnedServerInfoProvider,\n });\n return trafficMirrorInit;\n };\n\n // The negotiated upstream era is the first fact an operator needs when a\n // wrapped server misbehaves now that leg 2 negotiates: it says which wire the\n // gateway is speaking upstream, and whether the kill switch would change it.\n const logUpstreamEra = (connected: Client) => {\n logger.info(\"Upstream protocol era negotiated\", {\n era: connected.getProtocolEra(),\n eventType: \"mcp_upstream_era_negotiated\",\n negotiation: config.supportNewMcpProtocol ? \"auto\" : \"legacy\",\n protocolVersion: connected.getNegotiatedProtocolVersion(),\n });\n };\n\n const proxy = async () => {\n // If port is specified, start HTTP server mode\n //\n // This lane serves the HANDSHAKE ERA ONLY, in either kill-switch state, and\n // that is deliberate — dual-era *serving* is scoped to stdio. It is not an\n // oversight to be fixed by flipping a flag here: startHTTPServer creates a\n // transport only for a request that either carries an `mcp-session-id` or is\n // an `initialize` (startHTTPServer.ts:152), and its stateless branch\n // (:211) is unreachable because this call passes no `stateless`. A modern\n // client opens with `server/discover` — no session id, not an `initialize` —\n // so it lands on the `else` at :243 and gets 400 / -32000. Serving modern\n // here needs that request-routing changed, not a gate added.\n //\n // The upstream leg DOES negotiate in this mode: createServer below builds\n // through the same connectWithStartupRetry/createClient path as stdio, so\n // upstreamNegotiation applies and a modern-only upstream is still wrappable.\n // The switch is therefore half-scoped in --port mode, and saying so is why\n // this comment exists.\n if (argv.port) {\n let upstream: undefined | UpstreamClient;\n const httpServer = await startHTTPServer({\n createServer: async () => {\n // connect() may return a different instance for remote connections;\n // connectWithStartupRetry retries a transient upstream failure.\n const connected = await connectWithStartupRetry();\n logUpstreamEra(connected);\n\n // Initialize the traffic evaluator once, now that access control has\n // allowed the server and the image digests are resolved.\n await ensureTrafficMirror();\n\n const serverVersion = connected.getServerVersion() as {\n name: string;\n version: string;\n };\n\n const serverCapabilities =\n connected.getServerCapabilities() as ServerCapabilities;\n\n // Wrap so a mid-session upstream drop self-heals instead of killing the\n // session. proxyServer holds this stable façade, not the raw client.\n upstream = wrapUpstream(connected);\n\n const server = new Server(serverVersion, {\n capabilities: serverCapabilities,\n });\n\n proxyServer({\n authorizer: accessControlAuthorizer,\n client: upstream,\n server,\n serverCapabilities,\n });\n\n return server;\n },\n onClose: async (server) => {\n if (upstream) {\n await upstream.close();\n }\n await server.close();\n },\n port: argv.port,\n });\n\n logger.info(\"MCP gateway started\", {\n eventType: \"mcp_gateway_started\",\n mode: \"http\",\n port: argv.port,\n proxyType,\n });\n if (proxyType === \"stdio\") {\n logger.info(\"MCP server configuration\", {\n args: redactCliArgs(finalArgs ?? []).join(\" \"),\n command: finalCommand,\n eventType: \"mcp_server_config\",\n proxyType,\n });\n } else {\n logger.info(\"MCP server configuration\", {\n eventType: \"mcp_server_config\",\n proxyType,\n remoteUrl,\n });\n }\n\n return {\n close: () => {\n return httpServer.close();\n },\n };\n }\n\n // Original stdio mode\n // connect() may return a different instance for remote connections;\n // connectWithStartupRetry retries a transient upstream failure.\n const connected = await connectWithStartupRetry();\n logUpstreamEra(connected);\n\n const serverVersion = connected.getServerVersion() as {\n name: string;\n version: string;\n };\n\n const serverCapabilities =\n connected.getServerCapabilities() as ServerCapabilities;\n\n // Wrap so a mid-session upstream drop self-heals (remote only by default).\n const upstream = wrapUpstream(connected);\n\n // Always create a stdio server for client-facing interface\n const server = new Server(serverVersion, {\n capabilities: serverCapabilities,\n });\n\n proxyServer({\n authorizer: accessControlAuthorizer,\n client: upstream,\n server,\n serverCapabilities,\n });\n\n // Initialize traffic evaluator with scanner config (post-gate, so the\n // resolved container image digests are part of the session data it reports).\n await ensureTrafficMirror();\n\n const logStarted = () => {\n logger.info(\"MCP gateway started\", {\n eventType: \"mcp_gateway_started\",\n mode: \"stdio\",\n proxyType,\n });\n\n // Log MCP server configuration\n logger.info(\"MCP server configuration\", {\n capabilities: Object.keys(serverCapabilities || {}),\n eventType: \"mcp_server_config\",\n proxyType,\n serverName: serverVersion?.name,\n serverVersion: serverVersion?.version,\n ...(proxyType === \"stdio\"\n ? { args: redactCliArgs(finalArgs ?? []).join(\" \"), command: finalCommand }\n : { remoteUrl, transport: usedTransport }),\n });\n };\n\n if (config.supportNewMcpProtocol) {\n // Dual-era serving. serveStdio owns the process's stdio transport and\n // pins ONE era-appropriate instance per connection from the factory: a\n // handshake opening (`initialize`) is served exactly as the hand-wired\n // path below (legacy: \"serve\"), and a modern opening (`server/discover`\n // plus the per-request `_meta` envelope) is served against the SAME\n // pre-built server — so proxyServer's capability-gated enforcement runs\n // for both eras. Do NOT construct a StdioServerTransport or call\n // server.connect() here: serveStdio does both and undoes them in\n // handle.close().\n const handle = serveStdio(() => server, { legacy: \"serve\" });\n clientConnected = true;\n\n logStarted();\n\n return {\n close: async () => {\n await handle.close();\n // Close the upstream connection (the façade closes the live client).\n await upstream.close();\n },\n };\n }\n\n // Handshake-only serving (kill switch off): the exact pre-dual-era path — a\n // single hand-wired StdioServerTransport connected directly to the server.\n const stdioTransport = new StdioServerTransport();\n\n await server.connect(stdioTransport);\n clientConnected = true;\n\n logStarted();\n\n return {\n close: () => {\n // Close the upstream connection (the façade closes the live client).\n return upstream.close();\n },\n };\n };\n\n const createGracefulShutdown = ({\n server,\n timeout,\n }: {\n server: { close: () => Promise<void> | void };\n timeout: number;\n }) => {\n let shutdownTriggered = false;\n\n const gracefulShutdown = async (trigger: string) => {\n if (shutdownTriggered) {\n return;\n }\n shutdownTriggered = true;\n\n logger.info(\"MCP gateway stopping\", {\n eventType: \"mcp_gateway_stopping\",\n signal: trigger,\n });\n\n try {\n await server.close();\n // Log final request counts before shutdown\n logRequestCounts();\n // Flush logs to ingest endpoint before exit\n await flushLogs(2000);\n process.exit(0);\n } catch (error) {\n const errorMessage = `Error during shutdown: ${error}`;\n logger.error(\"Error during shutdown\", {\n error: errorMessage,\n eventType: \"mcp_gateway_stopped\",\n shutdownTimeout: timeout,\n });\n // Flush logs to ingest endpoint before exit\n await flushLogs(2000);\n process.exit(1);\n }\n };\n\n const timeoutHandler = (trigger: string) => async () => {\n setTimeout(() => {\n logger.error(\"Graceful shutdown timeout exceeded, forcing exit\");\n process.exit(1);\n }, timeout).unref();\n\n await gracefulShutdown(trigger);\n };\n\n process.once(\"SIGTERM\", timeoutHandler(\"SIGTERM\"));\n process.once(\"SIGINT\", timeoutHandler(\"SIGINT\"));\n\n // Stdin EOF — fires when the parent (e.g. claude) closes our stdio pipe.\n // On Windows, parents typically TerminateProcess without graceful signals,\n // but the MCP SDK closes stdio first; this gives us a window to reap our\n // child process tree before being killed. On POSIX it provides a backup\n // path when SIGHUP/SIGTERM doesn't reach us.\n process.stdin.once(\"end\", timeoutHandler(\"stdin-end\"));\n\n return () => {\n return server.close();\n };\n };\n\n // Log startup before attempting to connect\n logger.info(\"MCP gateway starting\", {\n command: finalCommand,\n eventType: \"mcp_gateway_starting\",\n proxyType,\n transport: proxyType === \"remote\" ? \"auto-detect\" : \"stdio\",\n url: remoteUrl,\n });\n\n try {\n const server = await proxy();\n\n createGracefulShutdown({\n server,\n timeout: argv.gracefulShutdownTimeout,\n });\n } catch (error) {\n // Startup failed after proxy() began (upstream unreachable, access-control\n // block, …). Surface a specific reason to the agent + Groundcover + stderr.\n await handleStartupFailure(error);\n }\n};\n"],"x_google_ignoreList":[0],"mappings":"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"}