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"],