From 4b94286ec9745e94bab2b78332b2b9b19530519b Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Fri, 24 Apr 2026 00:05:30 -0400 Subject: [PATCH 1/7] Add Replay MCP stdio bridge --- packages/replayio/README.md | 25 + packages/replayio/package.json | 1 + packages/replayio/src/bin.ts | 1 + packages/replayio/src/commands/mcp.ts | 485 +++++++++++++++++++ packages/replayio/src/config.ts | 8 + yarn.lock | 658 +++++++++++++++++++++++++- 6 files changed, 1164 insertions(+), 14 deletions(-) create mode 100644 packages/replayio/src/commands/mcp.ts diff --git a/packages/replayio/README.md b/packages/replayio/README.md index 22fd6060d..80722bc39 100644 --- a/packages/replayio/README.md +++ b/packages/replayio/README.md @@ -26,6 +26,31 @@ This CLI will automatically prompt you to log into your Replay account (or to re The CLI will also prompt you to download the Replay runtime if you have not already done so. +## MCP + +The CLI can run Replay's MCP server over stdio using your existing Replay CLI authentication: + +```json +{ + "mcpServers": { + "replay": { + "type": "stdio", + "command": "npx", + "args": ["-y", "replayio", "mcp"] + } + } +} +``` + +The command tries existing Replay CLI authentication first, using `replayio login` or +`REPLAY_API_KEY`. If no CLI token is available, it falls back to MCP OAuth using a +stable pre-registered client ID and PKCE. + +The HTTP endpoint can be overridden with `REPLAY_MCP_SERVER` or `replayio mcp --url `. +The OAuth client can be overridden with `REPLAY_MCP_OAUTH_CLIENT_ID`, and the loopback +callback can be overridden with `REPLAY_MCP_OAUTH_REDIRECT_URL`. The default OAuth +callback is `http://127.0.0.1:42813/callback` and must be registered for the client. + ## Contributing Contributing guide can be found [here](contributing.md). diff --git a/packages/replayio/package.json b/packages/replayio/package.json index 7247ae63e..b57bcdcde 100644 --- a/packages/replayio/package.json +++ b/packages/replayio/package.json @@ -29,6 +29,7 @@ ], "homepage": "https://github.com/replayio/replay-cli/blob/main/packages/replayio/README.md", "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.3", "@replayio/protocol": "^0.71.0", "@replayio/sourcemap-upload": "workspace:^", "@types/semver": "^7.5.6", diff --git a/packages/replayio/src/bin.ts b/packages/replayio/src/bin.ts index 062e09a06..ea88fe406 100644 --- a/packages/replayio/src/bin.ts +++ b/packages/replayio/src/bin.ts @@ -11,6 +11,7 @@ import "./commands/info"; import "./commands/list"; import "./commands/login"; import "./commands/logout"; +import "./commands/mcp"; import "./commands/open"; import "./commands/record"; import "./commands/remove"; diff --git a/packages/replayio/src/commands/mcp.ts b/packages/replayio/src/commands/mcp.ts new file mode 100644 index 000000000..dd0bb706b --- /dev/null +++ b/packages/replayio/src/commands/mcp.ts @@ -0,0 +1,485 @@ +import { readFromCache, writeToCache } from "@replay-cli/shared/cache"; +import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken"; +import { getReplayPath } from "@replay-cli/shared/getReplayPath"; +import { randomBytes } from "node:crypto"; +import { createServer, type Server as HttpServer } from "node:http"; +import open from "open"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { + type OAuthClientProvider, + type OAuthDiscoveryState, + UnauthorizedError, +} from "@modelcontextprotocol/sdk/client/auth.js"; +import { + StreamableHTTPClientTransport, + StreamableHTTPError, +} from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + type OAuthClientInformationFull, + type OAuthClientMetadata, + type OAuthTokens, +} from "@modelcontextprotocol/sdk/shared/auth.js"; +import { + CallToolRequestSchema, + CompleteRequestSchema, + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourceTemplatesRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, + type ServerCapabilities, +} from "@modelcontextprotocol/sdk/types.js"; +import { program } from "commander"; +import { name as packageName, version as packageVersion } from "../../package.json"; +import { replayMcpOAuthClientId, replayMcpOAuthRedirectUrl, replayMcpServer } from "../config"; + +type AuthMode = "auto" | "cli" | "oauth"; + +type McpOptions = { + auth: string; + oauthClientId: string; + oauthRedirectUrl: string; + url: string; +}; + +type NormalizedMcpOptions = Omit & { + auth: AuthMode; +}; + +type CachedMcpOAuthDetails = { + codeVerifier?: string; + discoveryState?: OAuthDiscoveryState; + state?: string; + tokens?: OAuthTokens; +}; + +const McpOAuthCachePath = getReplayPath("profile", "mcp-oauth.json"); + +function parseAuthMode(value: string): AuthMode { + if (value === "auto" || value === "cli" || value === "oauth") { + return value; + } + + throw new Error(`Unsupported MCP auth mode "${value}". Expected auto, cli, or oauth.`); +} + +program + .command("mcp") + .description("Run the Replay MCP server over stdio") + .option("--auth ", "Authentication mode: auto, cli, or oauth", "auto") + .option("--oauth-client-id ", "Pre-registered MCP OAuth client ID", replayMcpOAuthClientId) + .option( + "--oauth-redirect-url ", + "Loopback redirect URL registered for the MCP OAuth client", + replayMcpOAuthRedirectUrl + ) + .option("--url ", "Replay MCP server URL", replayMcpServer) + .action((options: McpOptions) => { + void runMcp(options).catch(error => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + }); + }); + +async function runMcp(options: McpOptions) { + const normalizedOptions = { + ...options, + auth: parseAuthMode(options.auth), + }; + const { url } = normalizedOptions; + const remoteUrl = new URL(url); + const remoteClient = await connectRemoteClient(remoteUrl, normalizedOptions); + + const remoteCapabilities = remoteClient.getServerCapabilities() ?? {}; + const server = new McpServer( + { + name: "replay", + version: packageVersion, + }, + { + capabilities: getLocalCapabilities(remoteCapabilities), + instructions: remoteClient.getInstructions(), + } + ); + + server.setRequestHandler(ListToolsRequestSchema, (request, extra) => + remoteClient.listTools(request.params, { signal: extra.signal }) + ); + server.setRequestHandler(CallToolRequestSchema, (request, extra) => + remoteClient.callTool(request.params, undefined, { signal: extra.signal }) + ); + + if (remoteCapabilities.resources) { + server.setRequestHandler(ListResourcesRequestSchema, (request, extra) => + remoteClient.listResources(request.params, { signal: extra.signal }) + ); + server.setRequestHandler(ReadResourceRequestSchema, (request, extra) => + remoteClient.readResource(request.params, { signal: extra.signal }) + ); + server.setRequestHandler(ListResourceTemplatesRequestSchema, (request, extra) => + remoteClient.listResourceTemplates(request.params, { signal: extra.signal }) + ); + } + + if (remoteCapabilities.prompts) { + server.setRequestHandler(ListPromptsRequestSchema, (request, extra) => + remoteClient.listPrompts(request.params, { signal: extra.signal }) + ); + server.setRequestHandler(GetPromptRequestSchema, (request, extra) => + remoteClient.getPrompt(request.params, { signal: extra.signal }) + ); + } + + if (remoteCapabilities.completions) { + server.setRequestHandler(CompleteRequestSchema, (request, extra) => + remoteClient.complete(request.params, { signal: extra.signal }) + ); + } + + server.onerror = error => { + console.error(`Replay MCP stdio error: ${error.message}`); + }; + + let isClosing = false; + const cleanup = async (exitCode?: number) => { + if (isClosing) { + return; + } + + isClosing = true; + process.off("SIGINT", handleSignal); + process.off("SIGTERM", handleSignal); + process.stdin.off("end", handleStdinEnd); + + await Promise.allSettled([server.close(), remoteClient.close()]); + + if (typeof exitCode === "number") { + process.exit(exitCode); + } + }; + const handleSignal = () => { + void cleanup(0); + }; + const handleStdinEnd = () => { + void cleanup(0); + }; + + server.onclose = () => { + void cleanup(0); + }; + + process.on("SIGINT", handleSignal); + process.on("SIGTERM", handleSignal); + process.stdin.on("end", handleStdinEnd); + + await server.connect(new StdioServerTransport()); +} + +async function connectRemoteClient(remoteUrl: URL, options: NormalizedMcpOptions): Promise { + if (options.auth !== "oauth") { + const { accessToken } = await getAccessToken(); + if (accessToken) { + const remoteClient = createRemoteClient(); + + try { + await remoteClient.connect( + new StreamableHTTPClientTransport(remoteUrl, { + requestInit: { + headers: { + ...getCommonHeaders(), + Authorization: `Bearer ${accessToken}`, + }, + }, + }) + ); + return remoteClient; + } catch (error) { + await remoteClient.close(); + + if (options.auth === "cli" || !isAuthenticationError(error)) { + throw error; + } + + console.error("Replay CLI auth was rejected by the MCP server; falling back to MCP OAuth."); + } + } else if (options.auth === "cli") { + throw new Error( + "Replay MCP requires Replay CLI authentication. Run `replayio login` or set REPLAY_API_KEY." + ); + } + } + + return await connectRemoteClientWithOAuth(remoteUrl, options); +} + +async function connectRemoteClientWithOAuth( + remoteUrl: URL, + options: NormalizedMcpOptions +): Promise { + const remoteClient = createRemoteClient(); + const oauthProvider = new ReplayMcpOAuthProvider({ + clientId: options.oauthClientId, + redirectUrl: options.oauthRedirectUrl, + }); + let remoteTransport = createOAuthTransport(remoteUrl, oauthProvider); + + try { + await remoteClient.connect(remoteTransport); + return remoteClient; + } catch (error) { + if (!(error instanceof UnauthorizedError)) { + throw error; + } + + const authorizationCode = await oauthProvider.waitForAuthorizationCode(); + await remoteTransport.finishAuth(authorizationCode); + await remoteClient.close(); + + remoteTransport = createOAuthTransport(remoteUrl, oauthProvider); + await remoteClient.connect(remoteTransport); + return remoteClient; + } +} + +function createRemoteClient() { + const remoteClient = new Client( + { + name: `${packageName}-mcp`, + version: packageVersion, + }, + { + capabilities: {}, + } + ); + + remoteClient.onerror = error => { + console.error(`Replay MCP remote error: ${error.message}`); + }; + + return remoteClient; +} + +function createOAuthTransport(remoteUrl: URL, authProvider: OAuthClientProvider) { + return new StreamableHTTPClientTransport(remoteUrl, { + authProvider, + requestInit: { + headers: getCommonHeaders(), + }, + }); +} + +function getCommonHeaders() { + return { + "x-client-info": `${packageName}-mcp/${packageVersion}`, + "x-replay-source": "replay-cli-mcp", + }; +} + +function isAuthenticationError(error: unknown) { + return ( + error instanceof UnauthorizedError || + (error instanceof StreamableHTTPError && error.code === 401) + ); +} + +function getLocalCapabilities(remoteCapabilities: ServerCapabilities): ServerCapabilities { + return { + completions: remoteCapabilities.completions, + experimental: remoteCapabilities.experimental, + prompts: remoteCapabilities.prompts, + resources: remoteCapabilities.resources, + tools: remoteCapabilities.tools ?? {}, + }; +} + +class ReplayMcpOAuthProvider implements OAuthClientProvider { + readonly clientMetadataUrl = undefined; + private callbackServer: HttpServer | undefined; + private callbackPromise: Promise | undefined; + + constructor( + private readonly config: { + clientId: string; + redirectUrl: string; + } + ) {} + + get redirectUrl() { + return this.config.redirectUrl; + } + + get clientMetadata(): OAuthClientMetadata { + return { + client_name: "Replay CLI MCP", + redirect_uris: [this.config.redirectUrl], + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + scope: "openid profile email offline_access", + token_endpoint_auth_method: "none", + }; + } + + clientInformation(): OAuthClientInformationFull { + return { + ...this.clientMetadata, + client_id: this.config.clientId, + }; + } + + tokens() { + return this.readCache().tokens; + } + + saveTokens(tokens: OAuthTokens) { + this.writeCache({ + ...this.readCache(), + codeVerifier: undefined, + state: undefined, + tokens, + }); + } + + state() { + const state = randomBytes(16).toString("hex"); + this.writeCache({ ...this.readCache(), state }); + return state; + } + + saveCodeVerifier(codeVerifier: string) { + this.writeCache({ ...this.readCache(), codeVerifier }); + } + + codeVerifier() { + const codeVerifier = this.readCache().codeVerifier; + if (!codeVerifier) { + throw new Error("Missing MCP OAuth PKCE code verifier"); + } + return codeVerifier; + } + + discoveryState() { + return this.readCache().discoveryState; + } + + saveDiscoveryState(discoveryState: OAuthDiscoveryState) { + this.writeCache({ ...this.readCache(), discoveryState }); + } + + async redirectToAuthorization(authorizationUrl: URL) { + await this.startCallbackServer(); + + console.error("Replay MCP OAuth required. Opening browser for authorization."); + console.error(`If the browser does not open, visit: ${authorizationUrl.toString()}`); + + try { + await open(authorizationUrl.toString()); + } catch (error) { + console.error( + `Failed to open browser automatically: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + async waitForAuthorizationCode() { + await this.startCallbackServer(); + return await this.callbackPromise!; + } + + invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier" | "discovery") { + if (scope === "all" || scope === "client") { + writeToCache(McpOAuthCachePath, undefined); + return; + } + + const cache = this.readCache(); + if (scope === "tokens") { + cache.tokens = undefined; + } else if (scope === "verifier") { + cache.codeVerifier = undefined; + cache.state = undefined; + } else if (scope === "discovery") { + cache.discoveryState = undefined; + } + this.writeCache(cache); + } + + private async startCallbackServer() { + if (this.callbackPromise) { + return; + } + + const redirectUrl = new URL(this.config.redirectUrl); + const port = Number(redirectUrl.port); + if (!port) { + throw new Error(`MCP OAuth redirect URL must include a loopback port: ${redirectUrl}`); + } + + this.callbackPromise = new Promise((resolve, reject) => { + this.callbackServer = createServer((request, response) => { + const requestUrl = new URL(request.url || "/", redirectUrl); + if (requestUrl.pathname !== redirectUrl.pathname) { + response.writeHead(404); + response.end(); + return; + } + + const error = requestUrl.searchParams.get("error"); + const code = requestUrl.searchParams.get("code"); + const state = requestUrl.searchParams.get("state"); + const expectedState = this.readCache().state; + + if (error) { + response.writeHead(400, { "Content-Type": "text/html" }); + response.end("

Replay MCP authorization failed

"); + this.closeCallbackServer(); + reject(new Error(`MCP OAuth authorization failed: ${error}`)); + return; + } + + if (!code) { + response.writeHead(400, { "Content-Type": "text/html" }); + response.end( + "

Replay MCP authorization failed

Missing authorization code.

" + ); + this.closeCallbackServer(); + reject(new Error("MCP OAuth callback did not include an authorization code")); + return; + } + + if (expectedState && state !== expectedState) { + response.writeHead(400, { "Content-Type": "text/html" }); + response.end("

Replay MCP authorization failed

State mismatch.

"); + this.closeCallbackServer(); + reject(new Error("MCP OAuth callback state mismatch")); + return; + } + + response.writeHead(200, { "Content-Type": "text/html" }); + response.end("

Replay MCP authorization complete

You can close this window.

"); + this.closeCallbackServer(); + resolve(code); + }); + + this.callbackServer.once("error", reject); + this.callbackServer.listen(port, redirectUrl.hostname); + }); + } + + private closeCallbackServer() { + this.callbackServer?.close(); + this.callbackServer = undefined; + } + + private readCache() { + return readFromCache(McpOAuthCachePath) ?? {}; + } + + private writeCache(cache: CachedMcpOAuthDetails) { + writeToCache(McpOAuthCachePath, cache); + } +} diff --git a/packages/replayio/src/config.ts b/packages/replayio/src/config.ts index 1364860fa..2bc349291 100644 --- a/packages/replayio/src/config.ts +++ b/packages/replayio/src/config.ts @@ -1,6 +1,14 @@ // TODO [PRO-720] Remove these in favor of values exported by "shared" export const replayApiServer = process.env.REPLAY_API_SERVER || "https://api.replay.io"; export const replayAppHost = process.env.REPLAY_APP_SERVER || "https://app.replay.io"; +export const replayMcpServer = + process.env.REPLAY_MCP_SERVER || + process.env.RECORD_REPLAY_MCP_SERVER || + "https://dispatch.replay.io/mcp"; +export const replayMcpOAuthClientId = + process.env.REPLAY_MCP_OAUTH_CLIENT_ID || "OIteqhJF3KieHSauCGduBqU8shNKzBuO"; +export const replayMcpOAuthRedirectUrl = + process.env.REPLAY_MCP_OAUTH_REDIRECT_URL || "http://127.0.0.1:42813/callback"; export const replayWsServer = process.env.RECORD_REPLAY_SERVER || process.env.REPLAY_SERVER || "wss://dispatch.replay.io"; diff --git a/yarn.lock b/yarn.lock index 7418327ad..de418e2ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2520,6 +2520,15 @@ __metadata: languageName: node linkType: hard +"@hono/node-server@npm:^1.19.9": + version: 1.19.14 + resolution: "@hono/node-server@npm:1.19.14" + peerDependencies: + hono: ^4 + checksum: 10c0/41a099bb3705d96aac44b7a8db8805f2a22ce8a0f767a27b6d10b74a9964925df01c5f35d3631e882f8bcdeee3518884c30f40588ac8c960d88bf71048ba0df3 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.14": version: 0.11.14 resolution: "@humanwhocodes/config-array@npm:0.11.14" @@ -3133,6 +3142,39 @@ __metadata: languageName: node linkType: hard +"@modelcontextprotocol/sdk@npm:^1.25.3": + version: 1.29.0 + resolution: "@modelcontextprotocol/sdk@npm:1.29.0" + dependencies: + "@hono/node-server": "npm:^1.19.9" + ajv: "npm:^8.17.1" + ajv-formats: "npm:^3.0.1" + content-type: "npm:^1.0.5" + cors: "npm:^2.8.5" + cross-spawn: "npm:^7.0.5" + eventsource: "npm:^3.0.2" + eventsource-parser: "npm:^3.0.0" + express: "npm:^5.2.1" + express-rate-limit: "npm:^8.2.1" + hono: "npm:^4.11.4" + jose: "npm:^6.1.3" + json-schema-typed: "npm:^8.0.2" + pkce-challenge: "npm:^5.0.0" + raw-body: "npm:^3.0.0" + zod: "npm:^3.25 || ^4.0" + zod-to-json-schema: "npm:^3.25.1" + peerDependencies: + "@cfworker/json-schema": ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + "@cfworker/json-schema": + optional: true + zod: + optional: false + checksum: 10c0/7c4bc339205b1652330cd4e6b121cc859079655f2b9c0506bbb15563ba0d07924bda3d949705530532db7f4d2cb86d633dc8f92bc32803d97c7bece2ac63e29f + languageName: node + linkType: hard + "@napi-rs/snappy-android-arm-eabi@npm:7.2.2": version: 7.2.2 resolution: "@napi-rs/snappy-android-arm-eabi@npm:7.2.2" @@ -5320,6 +5362,16 @@ __metadata: languageName: node linkType: hard +"accepts@npm:^2.0.0": + version: 2.0.0 + resolution: "accepts@npm:2.0.0" + dependencies: + mime-types: "npm:^3.0.0" + negotiator: "npm:^1.0.0" + checksum: 10c0/98374742097e140891546076215f90c32644feacf652db48412329de4c2a529178a81aa500fbb13dd3e6cbf6e68d829037b123ac037fc9a08bcec4b87b358eef + languageName: node + linkType: hard + "accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -5449,6 +5501,20 @@ __metadata: languageName: node linkType: hard +"ajv-formats@npm:^3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" + dependencies: + ajv: "npm:^8.0.0" + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10c0/168d6bca1ea9f163b41c8147bae537e67bd963357a5488a1eaf3abe8baa8eec806d4e45f15b10767e6020679315c7e1e5e6803088dfb84efa2b4e9353b83dd0a + languageName: node + linkType: hard + "ajv-keywords@npm:^3.4.1, ajv-keywords@npm:^3.5.2": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" @@ -5493,6 +5559,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^8.17.1": + version: 8.18.0 + resolution: "ajv@npm:8.18.0" + dependencies: + fast-deep-equal: "npm:^3.1.3" + fast-uri: "npm:^3.0.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + checksum: 10c0/e7517c426173513a07391be951879932bdf3348feaebd2199f5b901c20f99d60db8cd1591502d4d551dc82f594e82a05c4fe1c70139b15b8937f7afeaed9532f + languageName: node + linkType: hard + "ansi-colors@npm:^4.1.1, ansi-colors@npm:^4.1.3": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -5839,6 +5917,20 @@ __metadata: languageName: node linkType: hard +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73 + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186 + languageName: node + linkType: hard + "async@npm:^3.2.0, async@npm:^3.2.3": version: 3.2.5 resolution: "async@npm:3.2.5" @@ -6248,6 +6340,23 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:^2.2.1": + version: 2.2.2 + resolution: "body-parser@npm:2.2.2" + dependencies: + bytes: "npm:^3.1.2" + content-type: "npm:^1.0.5" + debug: "npm:^4.4.3" + http-errors: "npm:^2.0.0" + iconv-lite: "npm:^0.7.0" + on-finished: "npm:^2.4.1" + qs: "npm:^6.14.1" + raw-body: "npm:^3.0.1" + type-is: "npm:^2.0.1" + checksum: 10c0/95a830a003b38654b75166ca765358aa92ee3d561bf0e41d6ccdde0e1a0c9783cab6b90b20eb635d23172c010b59d3563a137a738e74da4ba714463510d05137 + languageName: node + linkType: hard + "bonjour-service@npm:^1.0.11": version: 1.2.1 resolution: "bonjour-service@npm:1.2.1" @@ -6408,7 +6517,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2": +"bytes@npm:3.1.2, bytes@npm:^3.1.2, bytes@npm:~3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e @@ -6442,6 +6551,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 + languageName: node + linkType: hard + "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -6455,6 +6574,16 @@ __metadata: languageName: node linkType: hard +"call-bound@npm:^1.0.2": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10c0/f4796a6a0941e71c766aea672f63b72bc61234c4f4964dc6d7606e3664c307e7d77845328a8f3359ce39ddb377fed67318f9ee203dea1d47e46165dcf2917644 + languageName: node + linkType: hard + "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -6993,7 +7122,14 @@ __metadata: languageName: node linkType: hard -"content-type@npm:~1.0.4, content-type@npm:~1.0.5": +"content-disposition@npm:^1.0.0": + version: 1.1.0 + resolution: "content-disposition@npm:1.1.0" + checksum: 10c0/94e0aef65873e69330f5f187fbc44ebce593bdcb8013dd8a68b7d0f159ca089bd30db3f8095d829f81c341695b60a6085ee6e15e6d775c4a325b586cc8d91974 + languageName: node + linkType: hard + +"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af @@ -7021,6 +7157,13 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:^1.2.1": + version: 1.2.2 + resolution: "cookie-signature@npm:1.2.2" + checksum: 10c0/54e05df1a293b3ce81589b27dddc445f462f6fa6812147c033350cd3561a42bc14481674e05ed14c7bd0ce1e8bb3dc0e40851bad75415733711294ddce0b7bc6 + languageName: node + linkType: hard + "cookie@npm:0.6.0": version: 0.6.0 resolution: "cookie@npm:0.6.0" @@ -7028,6 +7171,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^0.7.1": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 + languageName: node + linkType: hard + "core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.36.1": version: 3.36.1 resolution: "core-js-compat@npm:3.36.1" @@ -7065,6 +7215,16 @@ __metadata: languageName: node linkType: hard +"cors@npm:^2.8.5": + version: 2.8.6 + resolution: "cors@npm:2.8.6" + dependencies: + object-assign: "npm:^4" + vary: "npm:^1" + checksum: 10c0/ab2bc57b8af8ef8476682a59647f7c55c1a7d406b559ac06119aa1c5f70b96d35036864d197b24cf86e228e4547231088f1f94ca05061dbb14d89cc0bc9d4cab + languageName: node + linkType: hard + "cosmiconfig@npm:^6.0.0": version: 6.0.0 resolution: "cosmiconfig@npm:6.0.0" @@ -7142,6 +7302,17 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^7.0.5": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 + languageName: node + linkType: hard + "crypto-random-string@npm:^2.0.0": version: 2.0.0 resolution: "crypto-random-string@npm:2.0.0" @@ -7631,6 +7802,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.0, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + "decamelize-keys@npm:^1.1.0": version: 1.1.1 resolution: "decamelize-keys@npm:1.1.1" @@ -7756,7 +7939,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0": +"depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c @@ -8003,6 +8186,17 @@ __metadata: languageName: node linkType: hard +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 + languageName: node + linkType: hard + "duplexer@npm:^0.1.2, duplexer@npm:~0.1.1": version: 0.1.2 resolution: "duplexer@npm:0.1.2" @@ -8094,6 +8288,13 @@ __metadata: languageName: node linkType: hard +"encodeurl@npm:^2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb + languageName: node + linkType: hard + "encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" @@ -8301,6 +8502,13 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c + languageName: node + linkType: hard + "es-errors@npm:^1.1.0, es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" @@ -8363,6 +8571,15 @@ __metadata: languageName: node linkType: hard +"es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10c0/65364812ca4daf48eb76e2a3b7a89b3f6a2e62a1c420766ce9f692665a29d94fe41fe88b65f24106f449859549711e4b40d9fb8002d862dfd7eb1c512d10be0c + languageName: node + linkType: hard + "es-set-tostringtag@npm:^2.0.3": version: 2.0.3 resolution: "es-set-tostringtag@npm:2.0.3" @@ -8481,7 +8698,7 @@ __metadata: languageName: node linkType: hard -"escape-html@npm:~1.0.3": +"escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 @@ -9021,7 +9238,7 @@ __metadata: languageName: node linkType: hard -"etag@npm:~1.8.1": +"etag@npm:^1.8.1, etag@npm:~1.8.1": version: 1.8.1 resolution: "etag@npm:1.8.1" checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84 @@ -9064,6 +9281,22 @@ __metadata: languageName: node linkType: hard +"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.1": + version: 3.0.8 + resolution: "eventsource-parser@npm:3.0.8" + checksum: 10c0/3a73eee85311f33b12fa558381a477c1bdcf8c024a429a9d48f87b043e328c26d24ed280fd7ca92e2fdd4c8c37f749b758420c1533778aaca2beabf895024efa + languageName: node + linkType: hard + +"eventsource@npm:^3.0.2": + version: 3.0.7 + resolution: "eventsource@npm:3.0.7" + dependencies: + eventsource-parser: "npm:^3.0.1" + checksum: 10c0/c48a73c38f300e33e9f11375d4ee969f25cbb0519608a12378a38068055ae8b55b6e0e8a49c3f91c784068434efe1d9f01eb49b6315b04b0da9157879ce2f67d + languageName: node + linkType: hard + "execa@npm:4.1.0": version: 4.1.0 resolution: "execa@npm:4.1.0" @@ -9146,6 +9379,17 @@ __metadata: languageName: node linkType: hard +"express-rate-limit@npm:^8.2.1": + version: 8.4.0 + resolution: "express-rate-limit@npm:8.4.0" + dependencies: + ip-address: "npm:10.1.0" + peerDependencies: + express: ">= 4.11" + checksum: 10c0/d391c45c40ebc0eedf4f51bf86f25212294a2df25b0b50dd7044940fc7afd98886e8e7bd6709f904d394606a912efdd90a6a7d1927f5208e4d153775f9a47531 + languageName: node + linkType: hard + "express@npm:^4.17.3": version: 4.19.1 resolution: "express@npm:4.19.1" @@ -9185,6 +9429,42 @@ __metadata: languageName: node linkType: hard +"express@npm:^5.2.1": + version: 5.2.1 + resolution: "express@npm:5.2.1" + dependencies: + accepts: "npm:^2.0.0" + body-parser: "npm:^2.2.1" + content-disposition: "npm:^1.0.0" + content-type: "npm:^1.0.5" + cookie: "npm:^0.7.1" + cookie-signature: "npm:^1.2.1" + debug: "npm:^4.4.0" + depd: "npm:^2.0.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + finalhandler: "npm:^2.1.0" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.0" + merge-descriptors: "npm:^2.0.0" + mime-types: "npm:^3.0.0" + on-finished: "npm:^2.4.1" + once: "npm:^1.4.0" + parseurl: "npm:^1.3.3" + proxy-addr: "npm:^2.0.7" + qs: "npm:^6.14.0" + range-parser: "npm:^1.2.1" + router: "npm:^2.2.0" + send: "npm:^1.1.0" + serve-static: "npm:^2.2.0" + statuses: "npm:^2.0.1" + type-is: "npm:^2.0.1" + vary: "npm:^1.1.2" + checksum: 10c0/45e8c841ad188a41402ddcd1294901e861ee0819f632fb494f2ed344ef9c43315d294d443fb48d594e6586a3b779785120f43321417adaef8567316a55072949 + languageName: node + linkType: hard + "extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -9289,6 +9569,13 @@ __metadata: languageName: node linkType: hard +"fast-uri@npm:^3.0.1": + version: 3.1.0 + resolution: "fast-uri@npm:3.1.0" + checksum: 10c0/44364adca566f70f40d1e9b772c923138d47efeac2ae9732a872baafd77061f26b097ba2f68f0892885ad177becd065520412b8ffeec34b16c99433c5b9e2de7 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" @@ -9402,6 +9689,20 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:^2.1.0": + version: 2.1.1 + resolution: "finalhandler@npm:2.1.1" + dependencies: + debug: "npm:^4.4.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + on-finished: "npm:^2.4.1" + parseurl: "npm:^1.3.3" + statuses: "npm:^2.0.1" + checksum: 10c0/6bd664e21b7b2e79efcaace7d1a427169f61cce048fae68eb56290e6934e676b78e55d89f5998c5508871345bc59a61f47002dc505dc7288be68cceac1b701e2 + languageName: node + linkType: hard + "find-cache-dir@npm:^3.3.1": version: 3.3.2 resolution: "find-cache-dir@npm:3.3.2" @@ -9611,6 +9912,13 @@ __metadata: languageName: node linkType: hard +"fresh@npm:^2.0.0": + version: 2.0.0 + resolution: "fresh@npm:2.0.0" + checksum: 10c0/0557548194cb9a809a435bf92bcfbc20c89e8b5eb38861b73ced36750437251e39a111fc3a18b98531be9dd91fe1411e4969f229dc579ec0251ce6c5d4900bbc + languageName: node + linkType: hard + "from@npm:~0": version: 0.1.7 resolution: "from@npm:0.1.7" @@ -9777,6 +10085,13 @@ __metadata: languageName: node linkType: hard +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8 + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -9804,6 +10119,27 @@ __metadata: languageName: node linkType: hard +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.3.0": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" + dependencies: + async-function: "npm:^1.0.0" + async-generator-function: "npm:^1.0.0" + call-bind-apply-helpers: "npm:^1.0.2" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" + function-bind: "npm:^1.1.2" + generator-function: "npm:^2.0.0" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d + languageName: node + linkType: hard + "get-own-enumerable-property-symbols@npm:^3.0.0": version: 3.0.2 resolution: "get-own-enumerable-property-symbols@npm:3.0.2" @@ -9818,6 +10154,16 @@ __metadata: languageName: node linkType: hard +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c + languageName: node + linkType: hard + "get-stream@npm:^5.0.0, get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -10019,6 +10365,13 @@ __metadata: languageName: node linkType: hard +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead + languageName: node + linkType: hard + "graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -10114,6 +10467,13 @@ __metadata: languageName: node linkType: hard +"has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e + languageName: node + linkType: hard + "has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.2": version: 1.0.2 resolution: "has-tostringtag@npm:1.0.2" @@ -10141,6 +10501,13 @@ __metadata: languageName: node linkType: hard +"hono@npm:^4.11.4": + version: 4.12.14 + resolution: "hono@npm:4.12.14" + checksum: 10c0/78de4c98a9a3da0f067e38dcc4bd27f0d82b45d146ac39f5ca688515ee482c0a2e704d2ac6c1ee91ad17596b7c52b3e4b9483acd9c238d42f6ebcb43414a71b6 + languageName: node + linkType: hard + "hoopy@npm:^0.1.4": version: 0.1.4 resolution: "hoopy@npm:0.1.4" @@ -10267,6 +10634,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:^2.0.0, http-errors@npm:^2.0.1, http-errors@npm:~2.0.1": + version: 2.0.1 + resolution: "http-errors@npm:2.0.1" + dependencies: + depd: "npm:~2.0.0" + inherits: "npm:~2.0.4" + setprototypeof: "npm:~1.2.0" + statuses: "npm:~2.0.2" + toidentifier: "npm:~1.0.1" + checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4 + languageName: node + linkType: hard + "http-errors@npm:~1.6.2": version: 1.6.3 resolution: "http-errors@npm:1.6.3" @@ -10426,6 +10806,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:^0.7.0, iconv-lite@npm:~0.7.0": + version: 0.7.2 + resolution: "iconv-lite@npm:0.7.2" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722 + languageName: node + linkType: hard + "icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0": version: 5.1.0 resolution: "icss-utils@npm:5.1.0" @@ -10525,7 +10914,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -10587,6 +10976,13 @@ __metadata: languageName: node linkType: hard +"ip-address@npm:10.1.0": + version: 10.1.0 + resolution: "ip-address@npm:10.1.0" + checksum: 10c0/0103516cfa93f6433b3bd7333fa876eb21263912329bfa47010af5e16934eeeff86f3d2ae700a3744a137839ddfad62b900c7a445607884a49b5d1e32a3d7566 + languageName: node + linkType: hard + "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -10899,6 +11295,13 @@ __metadata: languageName: node linkType: hard +"is-promise@npm:^4.0.0": + version: 4.0.0 + resolution: "is-promise@npm:4.0.0" + checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503 + languageName: node + linkType: hard + "is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -12155,6 +12558,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^6.1.3": + version: 6.2.2 + resolution: "jose@npm:6.2.2" + checksum: 10c0/201f4776d77eccd339de99fb3ba940fdf03db15e64be7a99b511e53c232e3f3818e3f21b95223d62f99315a2ab76b4251cedd94e067de56893e45273a8d2151b + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -12285,6 +12695,13 @@ __metadata: languageName: node linkType: hard +"json-schema-typed@npm:^8.0.2": + version: 8.0.2 + resolution: "json-schema-typed@npm:8.0.2" + checksum: 10c0/89f5e2fb1495483b705c027203c07277ee6bf2665165ad25a9cb55de5af7f72570326d13d32565180781e4083ad5c9688102f222baed7b353c2f39c1e02b0428 + languageName: node + linkType: hard + "json-schema@npm:0.4.0, json-schema@npm:^0.4.0": version: 0.4.0 resolution: "json-schema@npm:0.4.0" @@ -12900,6 +13317,13 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f + languageName: node + linkType: hard + "mdn-data@npm:2.0.14": version: 2.0.14 resolution: "mdn-data@npm:2.0.14" @@ -12921,6 +13345,13 @@ __metadata: languageName: node linkType: hard +"media-typer@npm:^1.1.0": + version: 1.1.0 + resolution: "media-typer@npm:1.1.0" + checksum: 10c0/7b4baa40b25964bb90e2121ee489ec38642127e48d0cc2b6baa442688d3fde6262bfdca86d6bbf6ba708784afcac168c06840c71facac70e390f5f759ac121b9 + languageName: node + linkType: hard + "memfs@npm:^3.1.2, memfs@npm:^3.4.3": version: 3.5.3 resolution: "memfs@npm:3.5.3" @@ -12956,6 +13387,13 @@ __metadata: languageName: node linkType: hard +"merge-descriptors@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-descriptors@npm:2.0.0" + checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -12994,6 +13432,13 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:^1.54.0": + version: 1.54.0 + resolution: "mime-db@npm:1.54.0" + checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284 + languageName: node + linkType: hard + "mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -13003,6 +13448,15 @@ __metadata: languageName: node linkType: hard +"mime-types@npm:^3.0.0, mime-types@npm:^3.0.2": + version: 3.0.2 + resolution: "mime-types@npm:3.0.2" + dependencies: + mime-db: "npm:^1.54.0" + checksum: 10c0/35a0dd1035d14d185664f346efcdb72e93ef7a9b6e9ae808bd1f6358227010267fab52657b37562c80fc888ff76becb2b2938deb5e730818b7983bf8bd359767 + languageName: node + linkType: hard + "mime@npm:1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -13240,7 +13694,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 @@ -13307,6 +13761,13 @@ __metadata: languageName: node linkType: hard +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b + languageName: node + linkType: hard + "neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" @@ -13466,7 +13927,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": +"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 @@ -13487,6 +13948,13 @@ __metadata: languageName: node linkType: hard +"object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 + languageName: node + linkType: hard + "object-is@npm:^1.1.5": version: 1.1.6 resolution: "object-is@npm:1.1.6" @@ -13593,7 +14061,7 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:2.4.1": +"on-finished@npm:2.4.1, on-finished@npm:^2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" dependencies: @@ -13852,7 +14320,7 @@ __metadata: languageName: node linkType: hard -"parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": +"parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 @@ -13931,6 +14399,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^8.0.0": + version: 8.4.2 + resolution: "path-to-regexp@npm:8.4.2" + checksum: 10c0/05b115c49b47ad252ce05faa32930f643f23769c68b8bcfe78ad833545140c48bbffb3266986d6c8d5db13a64cf12e07e0d72d9882cab830efeefa553533ebaf + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -14003,6 +14478,13 @@ __metadata: languageName: node linkType: hard +"pkce-challenge@npm:^5.0.0": + version: 5.0.1 + resolution: "pkce-challenge@npm:5.0.1" + checksum: 10c0/207f4cb976682f27e8324eb49cf71937c98fbb8341a0b8f6142bc6f664825b30e049a54a21b5c034e823ee3c3d412f10d74bd21de78e17452a6a496c2991f57c + languageName: node + linkType: hard + "pkg-dir@npm:^4.1.0, pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" @@ -15083,7 +15565,7 @@ __metadata: languageName: node linkType: hard -"proxy-addr@npm:~2.0.7": +"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" dependencies: @@ -15167,6 +15649,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.14.0, qs@npm:^6.14.1": + version: 6.15.1 + resolution: "qs@npm:6.15.1" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10c0/19ee504f0ebff72598503e38cd6d9bd7b52a8ab62ae18b1e6bee3d4db58469bd65871ef1893a881bafb0f80ef2f9ab586e1f255cf25cc8d816c0f5a704721d97 + languageName: node + linkType: hard + "query-registry@npm:^2.6.0": version: 2.6.0 resolution: "query-registry@npm:2.6.0" @@ -15238,6 +15729,18 @@ __metadata: languageName: node linkType: hard +"raw-body@npm:^3.0.0, raw-body@npm:^3.0.1": + version: 3.0.2 + resolution: "raw-body@npm:3.0.2" + dependencies: + bytes: "npm:~3.1.2" + http-errors: "npm:~2.0.1" + iconv-lite: "npm:~0.7.0" + unpipe: "npm:~1.0.0" + checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29 + languageName: node + linkType: hard + "react-app-polyfill@npm:^3.0.0": version: 3.0.0 resolution: "react-app-polyfill@npm:3.0.0" @@ -15739,6 +16242,7 @@ __metadata: version: 0.0.0-use.local resolution: "replayio@workspace:packages/replayio" dependencies: + "@modelcontextprotocol/sdk": "npm:^1.25.3" "@replay-cli/pkg-build": "workspace:^" "@replay-cli/shared": "workspace:^" "@replay-cli/tsconfig": "workspace:^" @@ -16077,6 +16581,19 @@ __metadata: languageName: node linkType: hard +"router@npm:^2.2.0": + version: 2.2.0 + resolution: "router@npm:2.2.0" + dependencies: + debug: "npm:^4.4.0" + depd: "npm:^2.0.0" + is-promise: "npm:^4.0.0" + parseurl: "npm:^1.3.3" + path-to-regexp: "npm:^8.0.0" + checksum: 10c0/3279de7450c8eae2f6e095e9edacbdeec0abb5cb7249c6e719faa0db2dba43574b4fff5892d9220631c9abaff52dd3cad648cfea2aaace845e1a071915ac8867 + languageName: node + linkType: hard + "run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -16332,6 +16849,25 @@ __metadata: languageName: node linkType: hard +"send@npm:^1.1.0, send@npm:^1.2.0": + version: 1.2.1 + resolution: "send@npm:1.2.1" + dependencies: + debug: "npm:^4.4.3" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.1" + mime-types: "npm:^3.0.2" + ms: "npm:^2.1.3" + on-finished: "npm:^2.4.1" + range-parser: "npm:^1.2.1" + statuses: "npm:^2.0.2" + checksum: 10c0/fbbbbdc902a913d65605274be23f3d604065cfc3ee3d78bf9fc8af1dc9fc82667c50d3d657f5e601ac657bac9b396b50ee97bd29cd55436320cf1cddebdcec72 + languageName: node + linkType: hard + "serialize-javascript@npm:^4.0.0": version: 4.0.0 resolution: "serialize-javascript@npm:4.0.0" @@ -16377,6 +16913,18 @@ __metadata: languageName: node linkType: hard +"serve-static@npm:^2.2.0": + version: 2.2.1 + resolution: "serve-static@npm:2.2.1" + dependencies: + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + parseurl: "npm:^1.3.3" + send: "npm:^1.2.0" + checksum: 10c0/37986096e8572e2dfaad35a3925fa8da0c0969f8814fd7788e84d4d388bc068cf0c06d1658509788e55bed942a6b6d040a8a267fa92bb9ffb1179f8bacde5fd7 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -16417,7 +16965,7 @@ __metadata: languageName: node linkType: hard -"setprototypeof@npm:1.2.0": +"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc @@ -16470,6 +17018,41 @@ __metadata: languageName: node linkType: hard +"side-channel-list@npm:^1.0.0": + version: 1.0.1 + resolution: "side-channel-list@npm:1.0.1" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.4" + checksum: 10c0/d346c787fd2f9f1c2fdea14f00e8250118db0e7596d85a6cb9faa75f105d31a73a8f7a341c93d7df2a2429098c3d37a77bd3be9e88c37094b8c01807bc77c7a2 + languageName: node + linkType: hard + +"side-channel-map@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-map@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672 + languageName: node + linkType: hard + +"side-channel-weakmap@npm:^1.0.2": + version: 1.0.2 + resolution: "side-channel-weakmap@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + side-channel-map: "npm:^1.0.1" + checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185 + languageName: node + linkType: hard + "side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": version: 1.0.6 resolution: "side-channel@npm:1.0.6" @@ -16482,6 +17065,19 @@ __metadata: languageName: node linkType: hard +"side-channel@npm:^1.1.0": + version: 1.1.0 + resolution: "side-channel@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + side-channel-list: "npm:^1.0.0" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -16912,6 +17508,13 @@ __metadata: languageName: node linkType: hard +"statuses@npm:^2.0.1, statuses@npm:^2.0.2, statuses@npm:~2.0.2": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.0.0": version: 1.0.0 resolution: "stop-iteration-iterator@npm:1.0.0" @@ -17618,7 +18221,7 @@ __metadata: languageName: node linkType: hard -"toidentifier@npm:1.0.1": +"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 @@ -17956,6 +18559,17 @@ __metadata: languageName: node linkType: hard +"type-is@npm:^2.0.1": + version: 2.0.1 + resolution: "type-is@npm:2.0.1" + dependencies: + content-type: "npm:^1.0.5" + media-typer: "npm:^1.1.0" + mime-types: "npm:^3.0.0" + checksum: 10c0/7f7ec0a060b16880bdad36824ab37c26019454b67d73e8a465ed5a3587440fbe158bc765f0da68344498235c877e7dbbb1600beccc94628ed05599d667951b99 + languageName: node + linkType: hard + "type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -18366,7 +18980,7 @@ __metadata: languageName: node linkType: hard -"vary@npm:~1.1.2": +"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: 10c0/f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f @@ -19368,3 +19982,19 @@ __metadata: checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f languageName: node linkType: hard + +"zod-to-json-schema@npm:^3.25.1": + version: 3.25.2 + resolution: "zod-to-json-schema@npm:3.25.2" + peerDependencies: + zod: ^3.25.28 || ^4 + checksum: 10c0/dd300554393903022487688af14fbda5c719ba8179702bb55b3aa86318830467f0f7beb7d654036975ac963dc4843b72e59636448bfff9a0608f277bb6a14939 + languageName: node + linkType: hard + +"zod@npm:^3.25 || ^4.0": + version: 4.3.6 + resolution: "zod@npm:4.3.6" + checksum: 10c0/860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307 + languageName: node + linkType: hard From a84f89d9ad6446df97fe03540c0181419e6249c0 Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Fri, 24 Apr 2026 01:51:48 -0400 Subject: [PATCH 2/7] Tie MCP OAuth cache to client ID --- packages/replayio/src/commands/mcp.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/replayio/src/commands/mcp.ts b/packages/replayio/src/commands/mcp.ts index dd0bb706b..798b4fea4 100644 --- a/packages/replayio/src/commands/mcp.ts +++ b/packages/replayio/src/commands/mcp.ts @@ -50,6 +50,7 @@ type NormalizedMcpOptions = Omit & { }; type CachedMcpOAuthDetails = { + clientId?: string; codeVerifier?: string; discoveryState?: OAuthDiscoveryState; state?: string; @@ -330,7 +331,12 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { } tokens() { - return this.readCache().tokens; + const cache = this.readCache(); + if (cache.clientId !== this.config.clientId) { + return undefined; + } + + return cache.tokens; } saveTokens(tokens: OAuthTokens) { @@ -480,6 +486,9 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { } private writeCache(cache: CachedMcpOAuthDetails) { - writeToCache(McpOAuthCachePath, cache); + writeToCache(McpOAuthCachePath, { + ...cache, + clientId: this.config.clientId, + }); } } From 816c756b96b629bcc91c4129265b44885631414d Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Fri, 24 Apr 2026 02:05:24 -0400 Subject: [PATCH 3/7] Use localhost MCP OAuth callback --- packages/replayio/README.md | 2 +- packages/replayio/src/commands/mcp.ts | 1 + packages/replayio/src/config.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/replayio/README.md b/packages/replayio/README.md index 80722bc39..442918ca2 100644 --- a/packages/replayio/README.md +++ b/packages/replayio/README.md @@ -49,7 +49,7 @@ stable pre-registered client ID and PKCE. The HTTP endpoint can be overridden with `REPLAY_MCP_SERVER` or `replayio mcp --url `. The OAuth client can be overridden with `REPLAY_MCP_OAUTH_CLIENT_ID`, and the loopback callback can be overridden with `REPLAY_MCP_OAUTH_REDIRECT_URL`. The default OAuth -callback is `http://127.0.0.1:42813/callback` and must be registered for the client. +callback is `http://localhost:42813/callback` and must be registered for the client. ## Contributing diff --git a/packages/replayio/src/commands/mcp.ts b/packages/replayio/src/commands/mcp.ts index 798b4fea4..f41bf5a48 100644 --- a/packages/replayio/src/commands/mcp.ts +++ b/packages/replayio/src/commands/mcp.ts @@ -378,6 +378,7 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { await this.startCallbackServer(); console.error("Replay MCP OAuth required. Opening browser for authorization."); + console.error(`Using OAuth callback URL: ${this.config.redirectUrl}`); console.error(`If the browser does not open, visit: ${authorizationUrl.toString()}`); try { diff --git a/packages/replayio/src/config.ts b/packages/replayio/src/config.ts index 2bc349291..74e54e828 100644 --- a/packages/replayio/src/config.ts +++ b/packages/replayio/src/config.ts @@ -8,7 +8,7 @@ export const replayMcpServer = export const replayMcpOAuthClientId = process.env.REPLAY_MCP_OAUTH_CLIENT_ID || "OIteqhJF3KieHSauCGduBqU8shNKzBuO"; export const replayMcpOAuthRedirectUrl = - process.env.REPLAY_MCP_OAUTH_REDIRECT_URL || "http://127.0.0.1:42813/callback"; + process.env.REPLAY_MCP_OAUTH_REDIRECT_URL || "http://localhost:42813/callback"; export const replayWsServer = process.env.RECORD_REPLAY_SERVER || process.env.REPLAY_SERVER || "wss://dispatch.replay.io"; From da9aac288caa11e572a1ae83cddd00301a373460 Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Fri, 24 Apr 2026 02:12:54 -0400 Subject: [PATCH 4/7] Start MCP stdio before remote auth --- packages/replayio/src/commands/mcp.ts | 149 ++++++++++++++++++-------- 1 file changed, 107 insertions(+), 42 deletions(-) diff --git a/packages/replayio/src/commands/mcp.ts b/packages/replayio/src/commands/mcp.ts index f41bf5a48..f5698f168 100644 --- a/packages/replayio/src/commands/mcp.ts +++ b/packages/replayio/src/commands/mcp.ts @@ -92,53 +92,89 @@ async function runMcp(options: McpOptions) { }; const { url } = normalizedOptions; const remoteUrl = new URL(url); - const remoteClient = await connectRemoteClient(remoteUrl, normalizedOptions); + let remoteClient: Client | undefined; + let remoteClientPromise: Promise | undefined; - const remoteCapabilities = remoteClient.getServerCapabilities() ?? {}; const server = new McpServer( { name: "replay", version: packageVersion, }, { - capabilities: getLocalCapabilities(remoteCapabilities), - instructions: remoteClient.getInstructions(), + capabilities: getLocalCapabilities(), + instructions: "Replay MCP stdio proxy. Remote Replay tools are loaded on demand.", } ); - server.setRequestHandler(ListToolsRequestSchema, (request, extra) => - remoteClient.listTools(request.params, { signal: extra.signal }) - ); - server.setRequestHandler(CallToolRequestSchema, (request, extra) => - remoteClient.callTool(request.params, undefined, { signal: extra.signal }) - ); + const getRemoteClient = async () => { + if (!remoteClientPromise) { + remoteClientPromise = connectRemoteClient(remoteUrl, normalizedOptions) + .then(client => { + remoteClient = client; + void server.sendToolListChanged().catch(() => {}); + void server.sendResourceListChanged().catch(() => {}); + void server.sendPromptListChanged().catch(() => {}); + return client; + }) + .catch(error => { + remoteClientPromise = undefined; + throw error; + }); + } - if (remoteCapabilities.resources) { - server.setRequestHandler(ListResourcesRequestSchema, (request, extra) => - remoteClient.listResources(request.params, { signal: extra.signal }) - ); - server.setRequestHandler(ReadResourceRequestSchema, (request, extra) => - remoteClient.readResource(request.params, { signal: extra.signal }) - ); - server.setRequestHandler(ListResourceTemplatesRequestSchema, (request, extra) => - remoteClient.listResourceTemplates(request.params, { signal: extra.signal }) - ); - } + return await remoteClientPromise; + }; - if (remoteCapabilities.prompts) { - server.setRequestHandler(ListPromptsRequestSchema, (request, extra) => - remoteClient.listPrompts(request.params, { signal: extra.signal }) - ); - server.setRequestHandler(GetPromptRequestSchema, (request, extra) => - remoteClient.getPrompt(request.params, { signal: extra.signal }) - ); - } + server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => { + const client = await getRemoteClient(); + return await client.listTools(request.params, { signal: extra.signal }); + }); + server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { + const client = await getRemoteClient(); + return await client.callTool(request.params, undefined, { signal: extra.signal }); + }); - if (remoteCapabilities.completions) { - server.setRequestHandler(CompleteRequestSchema, (request, extra) => - remoteClient.complete(request.params, { signal: extra.signal }) - ); - } + server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => { + const client = await getRemoteClient(); + if (!client.getServerCapabilities()?.resources) { + return { resources: [] }; + } + + return await client.listResources(request.params, { signal: extra.signal }); + }); + server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => { + const client = await getRemoteClient(); + return await client.readResource(request.params, { signal: extra.signal }); + }); + server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request, extra) => { + const client = await getRemoteClient(); + if (!client.getServerCapabilities()?.resources) { + return { resourceTemplates: [] }; + } + + return await client.listResourceTemplates(request.params, { signal: extra.signal }); + }); + + server.setRequestHandler(ListPromptsRequestSchema, async (request, extra) => { + const client = await getRemoteClient(); + if (!client.getServerCapabilities()?.prompts) { + return { prompts: [] }; + } + + return await client.listPrompts(request.params, { signal: extra.signal }); + }); + server.setRequestHandler(GetPromptRequestSchema, async (request, extra) => { + const client = await getRemoteClient(); + return await client.getPrompt(request.params, { signal: extra.signal }); + }); + server.setRequestHandler(CompleteRequestSchema, async (request, extra) => { + const client = await getRemoteClient(); + if (!client.getServerCapabilities()?.completions) { + return { completion: { values: [] } }; + } + + return await client.complete(request.params, { signal: extra.signal }); + }); server.onerror = error => { console.error(`Replay MCP stdio error: ${error.message}`); @@ -155,7 +191,8 @@ async function runMcp(options: McpOptions) { process.off("SIGTERM", handleSignal); process.stdin.off("end", handleStdinEnd); - await Promise.allSettled([server.close(), remoteClient.close()]); + const remoteClientToClose = remoteClient ?? (await remoteClientPromise?.catch(() => undefined)); + await Promise.allSettled([server.close(), remoteClientToClose?.close()]); if (typeof exitCode === "number") { process.exit(exitCode); @@ -286,13 +323,12 @@ function isAuthenticationError(error: unknown) { ); } -function getLocalCapabilities(remoteCapabilities: ServerCapabilities): ServerCapabilities { +function getLocalCapabilities(): ServerCapabilities { return { - completions: remoteCapabilities.completions, - experimental: remoteCapabilities.experimental, - prompts: remoteCapabilities.prompts, - resources: remoteCapabilities.resources, - tools: remoteCapabilities.tools ?? {}, + completions: {}, + prompts: { listChanged: true }, + resources: { listChanged: true }, + tools: { listChanged: true }, }; } @@ -300,6 +336,7 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { readonly clientMetadataUrl = undefined; private callbackServer: HttpServer | undefined; private callbackPromise: Promise | undefined; + private callbackServerReadyPromise: Promise | undefined; constructor( private readonly config: { @@ -417,6 +454,7 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { private async startCallbackServer() { if (this.callbackPromise) { + await this.callbackServerReadyPromise; return; } @@ -473,13 +511,40 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { }); this.callbackServer.once("error", reject); - this.callbackServer.listen(port, redirectUrl.hostname); }); + + this.callbackServerReadyPromise = new Promise((resolve, reject) => { + const server = this.callbackServer!; + const handleError = (error: Error) => { + server.off("listening", handleListening); + reject(error); + }; + const handleListening = () => { + server.off("error", handleError); + resolve(); + }; + + server.once("error", handleError); + if (redirectUrl.hostname === "localhost") { + server.listen(port, handleListening); + } else { + server.listen(port, redirectUrl.hostname, handleListening); + } + }); + + try { + await this.callbackServerReadyPromise; + } catch (error) { + this.closeCallbackServer(); + this.callbackPromise = undefined; + throw error; + } } private closeCallbackServer() { this.callbackServer?.close(); this.callbackServer = undefined; + this.callbackServerReadyPromise = undefined; } private readCache() { From 6230a787d552f2b2abab1bf9405db35c53e24ca4 Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Fri, 24 Apr 2026 02:17:13 -0400 Subject: [PATCH 5/7] Preconnect MCP after stdio initialization --- packages/replayio/src/commands/mcp.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/replayio/src/commands/mcp.ts b/packages/replayio/src/commands/mcp.ts index f5698f168..c47f62ecf 100644 --- a/packages/replayio/src/commands/mcp.ts +++ b/packages/replayio/src/commands/mcp.ts @@ -102,7 +102,7 @@ async function runMcp(options: McpOptions) { }, { capabilities: getLocalCapabilities(), - instructions: "Replay MCP stdio proxy. Remote Replay tools are loaded on demand.", + instructions: "Replay MCP stdio proxy for the hosted Replay MCP server.", } ); @@ -124,6 +124,15 @@ async function runMcp(options: McpOptions) { return await remoteClientPromise; }; + const startRemoteClientConnection = () => { + void getRemoteClient().catch(error => { + console.error( + `Replay MCP remote connection failed: ${ + error instanceof Error ? error.message : String(error) + }` + ); + }); + }; server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => { const client = await getRemoteClient(); @@ -208,12 +217,18 @@ async function runMcp(options: McpOptions) { server.onclose = () => { void cleanup(0); }; + server.oninitialized = startRemoteClientConnection; process.on("SIGINT", handleSignal); process.on("SIGTERM", handleSignal); process.stdin.on("end", handleStdinEnd); await server.connect(new StdioServerTransport()); + + if (process.stdin.isTTY) { + console.error("Replay MCP stdio server started. Starting Replay authentication."); + startRemoteClientConnection(); + } } async function connectRemoteClient(remoteUrl: URL, options: NormalizedMcpOptions): Promise { From 2f48012c1ee443b3087dc09c297cd7ae235ac3db Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Fri, 24 Apr 2026 02:22:46 -0400 Subject: [PATCH 6/7] Add Auth0 audience for MCP OAuth --- packages/replayio/src/commands/mcp.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/replayio/src/commands/mcp.ts b/packages/replayio/src/commands/mcp.ts index c47f62ecf..b2d0272fc 100644 --- a/packages/replayio/src/commands/mcp.ts +++ b/packages/replayio/src/commands/mcp.ts @@ -272,7 +272,7 @@ async function connectRemoteClientWithOAuth( remoteUrl: URL, options: NormalizedMcpOptions ): Promise { - const remoteClient = createRemoteClient(); + let remoteClient = createRemoteClient(); const oauthProvider = new ReplayMcpOAuthProvider({ clientId: options.oauthClientId, redirectUrl: options.oauthRedirectUrl, @@ -291,6 +291,7 @@ async function connectRemoteClientWithOAuth( await remoteTransport.finishAuth(authorizationCode); await remoteClient.close(); + remoteClient = createRemoteClient(); remoteTransport = createOAuthTransport(remoteUrl, oauthProvider); await remoteClient.connect(remoteTransport); return remoteClient; @@ -428,13 +429,14 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { async redirectToAuthorization(authorizationUrl: URL) { await this.startCallbackServer(); + const compatibleAuthorizationUrl = getCompatibleAuthorizationUrl(authorizationUrl); console.error("Replay MCP OAuth required. Opening browser for authorization."); console.error(`Using OAuth callback URL: ${this.config.redirectUrl}`); - console.error(`If the browser does not open, visit: ${authorizationUrl.toString()}`); + console.error(`If the browser does not open, visit: ${compatibleAuthorizationUrl.toString()}`); try { - await open(authorizationUrl.toString()); + await open(compatibleAuthorizationUrl.toString()); } catch (error) { console.error( `Failed to open browser automatically: ${ @@ -521,6 +523,7 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { response.writeHead(200, { "Content-Type": "text/html" }); response.end("

Replay MCP authorization complete

You can close this window.

"); + console.error("Replay MCP OAuth callback received. Exchanging authorization code."); this.closeCallbackServer(); resolve(code); }); @@ -573,3 +576,21 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { }); } } + +function getCompatibleAuthorizationUrl(authorizationUrl: URL) { + const compatibleAuthorizationUrl = new URL(authorizationUrl); + const resource = compatibleAuthorizationUrl.searchParams.get("resource"); + + // Auth0 issues API access tokens based on the non-standard `audience` + // authorization parameter. MCP sends RFC 8707 `resource`, so mirror it for + // Auth0 while preserving the spec-required parameter. + if ( + resource && + !compatibleAuthorizationUrl.searchParams.has("audience") && + compatibleAuthorizationUrl.hostname.endsWith(".auth0.com") + ) { + compatibleAuthorizationUrl.searchParams.set("audience", resource); + } + + return compatibleAuthorizationUrl; +} From 3714bc529474de86d56ded36c2c798ee3f96afdb Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Fri, 24 Apr 2026 02:26:24 -0400 Subject: [PATCH 7/7] Use deployed MCP OAuth audience --- packages/replayio/README.md | 7 ++-- packages/replayio/src/commands/mcp.ts | 47 +++++++++++++++++++++------ packages/replayio/src/config.ts | 1 + 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/replayio/README.md b/packages/replayio/README.md index 442918ca2..1b413f496 100644 --- a/packages/replayio/README.md +++ b/packages/replayio/README.md @@ -47,9 +47,10 @@ The command tries existing Replay CLI authentication first, using `replayio logi stable pre-registered client ID and PKCE. The HTTP endpoint can be overridden with `REPLAY_MCP_SERVER` or `replayio mcp --url `. -The OAuth client can be overridden with `REPLAY_MCP_OAUTH_CLIENT_ID`, and the loopback -callback can be overridden with `REPLAY_MCP_OAUTH_REDIRECT_URL`. The default OAuth -callback is `http://localhost:42813/callback` and must be registered for the client. +The OAuth client can be overridden with `REPLAY_MCP_OAUTH_CLIENT_ID`, the Auth0 +audience can be overridden with `REPLAY_MCP_OAUTH_AUDIENCE`, and the loopback callback +can be overridden with `REPLAY_MCP_OAUTH_REDIRECT_URL`. The default OAuth callback is +`http://localhost:42813/callback` and must be registered for the client. ## Contributing diff --git a/packages/replayio/src/commands/mcp.ts b/packages/replayio/src/commands/mcp.ts index b2d0272fc..5d5f518e9 100644 --- a/packages/replayio/src/commands/mcp.ts +++ b/packages/replayio/src/commands/mcp.ts @@ -34,22 +34,30 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import { program } from "commander"; import { name as packageName, version as packageVersion } from "../../package.json"; -import { replayMcpOAuthClientId, replayMcpOAuthRedirectUrl, replayMcpServer } from "../config"; +import { + replayMcpOAuthAudience, + replayMcpOAuthClientId, + replayMcpOAuthRedirectUrl, + replayMcpServer, +} from "../config"; type AuthMode = "auto" | "cli" | "oauth"; type McpOptions = { auth: string; + oauthAudience?: string; oauthClientId: string; oauthRedirectUrl: string; url: string; }; -type NormalizedMcpOptions = Omit & { +type NormalizedMcpOptions = Omit & { auth: AuthMode; + oauthAudience: string; }; type CachedMcpOAuthDetails = { + audience?: string; clientId?: string; codeVerifier?: string; discoveryState?: OAuthDiscoveryState; @@ -71,6 +79,11 @@ program .command("mcp") .description("Run the Replay MCP server over stdio") .option("--auth ", "Authentication mode: auto, cli, or oauth", "auto") + .option( + "--oauth-audience ", + "Auth0 audience to request for MCP OAuth", + replayMcpOAuthAudience + ) .option("--oauth-client-id ", "Pre-registered MCP OAuth client ID", replayMcpOAuthClientId) .option( "--oauth-redirect-url ", @@ -86,12 +99,12 @@ program }); async function runMcp(options: McpOptions) { + const remoteUrl = new URL(options.url); const normalizedOptions = { ...options, auth: parseAuthMode(options.auth), + oauthAudience: options.oauthAudience || getDefaultMcpOAuthAudience(remoteUrl), }; - const { url } = normalizedOptions; - const remoteUrl = new URL(url); let remoteClient: Client | undefined; let remoteClientPromise: Promise | undefined; @@ -274,6 +287,7 @@ async function connectRemoteClientWithOAuth( ): Promise { let remoteClient = createRemoteClient(); const oauthProvider = new ReplayMcpOAuthProvider({ + audience: options.oauthAudience, clientId: options.oauthClientId, redirectUrl: options.oauthRedirectUrl, }); @@ -356,6 +370,7 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { constructor( private readonly config: { + audience: string; clientId: string; redirectUrl: string; } @@ -385,7 +400,7 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { tokens() { const cache = this.readCache(); - if (cache.clientId !== this.config.clientId) { + if (cache.clientId !== this.config.clientId || cache.audience !== this.config.audience) { return undefined; } @@ -429,7 +444,10 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { async redirectToAuthorization(authorizationUrl: URL) { await this.startCallbackServer(); - const compatibleAuthorizationUrl = getCompatibleAuthorizationUrl(authorizationUrl); + const compatibleAuthorizationUrl = getCompatibleAuthorizationUrl( + authorizationUrl, + this.config.audience + ); console.error("Replay MCP OAuth required. Opening browser for authorization."); console.error(`Using OAuth callback URL: ${this.config.redirectUrl}`); @@ -572,24 +590,33 @@ class ReplayMcpOAuthProvider implements OAuthClientProvider { private writeCache(cache: CachedMcpOAuthDetails) { writeToCache(McpOAuthCachePath, { ...cache, + audience: this.config.audience, clientId: this.config.clientId, }); } } -function getCompatibleAuthorizationUrl(authorizationUrl: URL) { +function getDefaultMcpOAuthAudience(remoteUrl: URL) { + if (remoteUrl.origin === "https://dispatch.replay.io" && remoteUrl.pathname === "/mcp") { + return "https://dispatch.replay.io/nut/mcp"; + } + + const resourceUrl = new URL(remoteUrl); + resourceUrl.hash = ""; + return resourceUrl.toString().replace(/\/$/, ""); +} + +function getCompatibleAuthorizationUrl(authorizationUrl: URL, audience: string) { const compatibleAuthorizationUrl = new URL(authorizationUrl); - const resource = compatibleAuthorizationUrl.searchParams.get("resource"); // Auth0 issues API access tokens based on the non-standard `audience` // authorization parameter. MCP sends RFC 8707 `resource`, so mirror it for // Auth0 while preserving the spec-required parameter. if ( - resource && !compatibleAuthorizationUrl.searchParams.has("audience") && compatibleAuthorizationUrl.hostname.endsWith(".auth0.com") ) { - compatibleAuthorizationUrl.searchParams.set("audience", resource); + compatibleAuthorizationUrl.searchParams.set("audience", audience); } return compatibleAuthorizationUrl; diff --git a/packages/replayio/src/config.ts b/packages/replayio/src/config.ts index 74e54e828..9b10bf433 100644 --- a/packages/replayio/src/config.ts +++ b/packages/replayio/src/config.ts @@ -9,6 +9,7 @@ export const replayMcpOAuthClientId = process.env.REPLAY_MCP_OAUTH_CLIENT_ID || "OIteqhJF3KieHSauCGduBqU8shNKzBuO"; export const replayMcpOAuthRedirectUrl = process.env.REPLAY_MCP_OAUTH_REDIRECT_URL || "http://localhost:42813/callback"; +export const replayMcpOAuthAudience = process.env.REPLAY_MCP_OAUTH_AUDIENCE; export const replayWsServer = process.env.RECORD_REPLAY_SERVER || process.env.REPLAY_SERVER || "wss://dispatch.replay.io";