fix(mcp): retain changes during callback verification

Bound temporary eval tunnel setup retries before provider calls and record their outcomes. Keep production callback validation unchanged.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-01 12:24:15 -05:00
1 parent fed6d7bab8
commit 93056b8ede
5 files changed
+47 -11

No files matched your search

+12 -2
View File
@@ -334,6 +334,7 @@ describe.skipIf(!support.supported)("public MCP OAuth and tool boundary", () =>
let status = 204;
let cloudAllowed = true;
let goodChallenge = true;
let duringVerification: (() => Promise<void>) | undefined;
const fetcher: EventFetch = async (url, init) => {
if (cloudOrigin && url === cloudOrigin + "/mcp/paperclip") return cloudAllowed
? Response.json({ result: { structuredContent: { user: { id: f.actor.userId }, companyId: f.company.id, connectionId: principal.grant.id } } })
@@ -344,7 +345,7 @@ describe.skipIf(!support.supported)("public MCP OAuth and tool boundary", () =>
const expected = "v1," + createHmac("sha256", Buffer.from(secret.slice(6), "base64")).update(`${headers.get("webhook-id")}.${headers.get("webhook-timestamp")}.${String(init.body)}`).digest("base64");
// A rotation test may use another secret; initial deliveries must be independently verifiable.
if (!String(headers.get("webhook-signature")).includes(expected) && body.type !== "verification") throw new Error("Invalid signature");
if (body.type === "verification") return Response.json({ challenge: goodChallenge ? body.challenge : "wrong" });
if (body.type === "verification") { await duringVerification?.(); duringVerification = undefined; return Response.json({ challenge: goodChallenge ? body.challenge : "wrong" }); }
return new Response(null, { status });
};
const dispatch = async (p: typeof principal, _method: string, path: string) => {
@@ -361,7 +362,7 @@ describe.skipIf(!support.supported)("public MCP OAuth and tool boundary", () =>
return row!;
};
return { ...f, principal, task: task!, service, input, received, secret, activity, dispatch, options,
advance(ms: number) { clock += ms; }, setStatus(value: number) { status = value; }, denyCloud() { cloudAllowed = false; }, badChallenge() { goodChallenge = false; }, now: () => clock };
advance(ms: number) { clock += ms; }, setStatus(value: number) { status = value; }, denyCloud() { cloudAllowed = false; }, duringVerification(fn: () => Promise<void>) { duringVerification = fn; }, badChallenge() { goodChallenge = false; }, now: () => clock };
}
it("discovers MCP 2.0 events, validates metadata/headers, and preserves legacy tools", async () => {
@@ -446,6 +447,15 @@ describe.skipIf(!support.supported)("public MCP OAuth and tool boundary", () =>
}
});
it("retains changes occurring while callback ownership is being verified", async () => {
const f = await eventFixture();
f.duringVerification(async () => { await f.activity(); });
await f.service.subscribe(f.principal, f.input);
await f.service.tick();
expect(f.received.filter(r => r.body.eventId)).toHaveLength(1);
await f.service.unsubscribe(f.principal, f.input);
});
it("bounds lifetimes and retries without claiming unsupported replay", async () => {
const f = await eventFixture();
const short = await f.service.subscribe(f.principal, { ...f.input, ttlMs: 1 });
+2 -1
View File
@@ -75,6 +75,7 @@ export function createPublicMcpEvents(db: Db, oauth: PublicMcpOAuth, api: ApiDis
}
async function subscribe(principal: McpPrincipal, raw: unknown, cloud?: CloudEventAuthority) {
const requestedAt = new Date(now());
const input = subscribeSchema.parse(raw);
validate(input);
if (!input.delivery.secret) throw new McpEventError(-32602, "A webhook signing secret is required.");
@@ -109,7 +110,7 @@ export function createPublicMcpEvents(db: Db, oauth: PublicMcpOAuth, api: ApiDis
if (existing && !active) await tx.delete(deliveries).where(eq(deliveries.subscriptionId, id));
const value = { companyId: principal.grant.companyId, grantId: principal.grant.id, name: input.name, taskId: input.arguments.taskId,
arguments: input.arguments, deliveryMaterial: await encrypt(destination), expiresAt, stoppedAt: null,
verifiedAt: verify ? new Date(now()) : existing!.verifiedAt, startsAt: active ? existing.startsAt : new Date(now()), scannedAt: new Date(now()) };
verifiedAt: verify ? new Date(now()) : existing!.verifiedAt, startsAt: active ? existing.startsAt : requestedAt, scannedAt: new Date(now()) };
await tx.insert(subscriptions).values({ id, ...value }).onConflictDoUpdate({ target: subscriptions.id, set: value });
await logActivity(tx as unknown as Db, { companyId: principal.grant.companyId, actorType: "user", actorId: principal.grant.userId,
action: "mcp.event_subscribed", entityType: "mcp_subscription", entityId: id, details: { name: input.name, taskId: input.arguments.taskId, expiresAt: expiresAt.toISOString() } });