From 0f21c1b01bb5de600d5788ee00a03595f0bcea7d Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:48:27 +0100 Subject: [PATCH] =?UTF-8?q?fix(revenuecat):=20=F0=9F=90=9B=20pick=20result?= =?UTF-8?q?=20frame=20from=20MCP=20SSE=20stream;=20cover=20platform=20sync?= =?UTF-8?q?/keys=20in=20scope-403?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../external/revenueCat/misc/revenuecatMcp.ts | 32 ++++++++++---- .../integration/scopes/scope-403.test.ts | 44 +++++++++++++++++++ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/server/src/external/revenueCat/misc/revenuecatMcp.ts b/server/src/external/revenueCat/misc/revenuecatMcp.ts index 6d8d03944..1dad9fa74 100644 --- a/server/src/external/revenueCat/misc/revenuecatMcp.ts +++ b/server/src/external/revenueCat/misc/revenuecatMcp.ts @@ -39,19 +39,33 @@ export const callRcMcpTool = async ({ throw new Error(`RevenueCat MCP error (${response.status})`); } - // Streamable-HTTP MCP replies as SSE: pull the JSON out of the `data:` line. + // Streamable-HTTP MCP replies as SSE and may emit preamble frames (ping, + // progress) before the result — pick the frame that carries result/error. const text = await response.text(); - const dataLine = text + const candidates = text .split("\n") .map((line) => line.trim()) - .find((line) => line.startsWith("data:") || line.startsWith("{")); - const payload = dataLine?.replace(/^data:\s*/, "") ?? text.trim(); + .filter((line) => line.startsWith("data:") || line.startsWith("{")) + .map((line) => line.replace(/^data:\s*/, "")); - let parsed: JsonRpcResult; - try { - parsed = JSON.parse(payload) as JsonRpcResult; - } catch { - throw new Error(`RevenueCat MCP returned unparseable response: ${payload.slice(0, 200)}`); + let parsed: JsonRpcResult | undefined; + for (const candidate of candidates) { + let frame: JsonRpcResult; + try { + frame = JSON.parse(candidate) as JsonRpcResult; + } catch { + continue; + } + if (frame.result !== undefined || frame.error !== undefined) { + parsed = frame; + break; + } + } + + if (!parsed) { + throw new Error( + `RevenueCat MCP returned no result frame: ${text.slice(0, 200)}`, + ); } if (parsed.error) { diff --git a/server/tests/integration/scopes/scope-403.test.ts b/server/tests/integration/scopes/scope-403.test.ts index 5a0cba74a..577bdb35a 100644 --- a/server/tests/integration/scopes/scope-403.test.ts +++ b/server/tests/integration/scopes/scope-403.test.ts @@ -1370,6 +1370,36 @@ const ROUTES = [ needsScopes: true, isWebhookExempt: false, }, + { + handlerName: "handleSyncRevenueCat", + handlerFile: + "src/internal/platform/platformBeta/handlers/handleSyncRevenueCat.ts", + method: "POST", + path: "/v1/platform.sync_revenuecat", + style: "RPC", + group: "v1/platform", + mountChain: ["/v1", "", "", "/platform.sync_revenuecat"], + sourceRouterFile: + "src/internal/platform/platformBeta/platformRpcRouter.ts", + routeKind: "createRoute", + needsScopes: true, + isWebhookExempt: false, + }, + { + handlerName: "handleGetRevenueCatKeys", + handlerFile: + "src/internal/platform/platformBeta/handlers/handleGetRevenueCatKeys.ts", + method: "POST", + path: "/v1/platform.get_revenuecat_keys", + style: "RPC", + group: "v1/platform", + mountChain: ["/v1", "", "", "/platform.get_revenuecat_keys"], + sourceRouterFile: + "src/internal/platform/platformBeta/platformRpcRouter.ts", + routeKind: "createRoute", + needsScopes: true, + isWebhookExempt: false, + }, { handlerName: "handleCreateSchedule", handlerFile: "src/internal/billing/v2/handlers/handleCreateSchedule.ts", @@ -4269,6 +4299,20 @@ const SCOPE_DECISIONS: Record< note: "platform RPC route — write", decidedAt: "2026-06-01T00:00:00.000Z", }, + "POST|/v1/platform.sync_revenuecat|handleSyncRevenueCat": { + decision: "decided", + scopes: ["platform:write"], + shape: "array", + note: "platform RPC route — write", + decidedAt: "2026-06-01T00:00:00.000Z", + }, + "POST|/v1/platform.get_revenuecat_keys|handleGetRevenueCatKeys": { + decision: "decided", + scopes: ["platform:write"], + shape: "array", + note: "platform RPC route — write", + decidedAt: "2026-06-01T00:00:00.000Z", + }, "POST|/v1/billing.create_schedule|handleCreateSchedule": { decision: "decided", scopes: ["billing:write"],