From 6aa0962d4fb715f2190bb7bb22efacab2e58495d Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 1 Oct 2026 12:44:13 -0500 Subject: [PATCH] fix(mcp): include events at the subscription start boundary Co-Authored-By: Paperclip --- doc/plans/2026-10-01-public-mcp-paid-eval-results.md | 4 ++++ server/src/__tests__/public-mcp.test.ts | 4 ++-- server/src/services/public-mcp/events.ts | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/plans/2026-10-01-public-mcp-paid-eval-results.md b/doc/plans/2026-10-01-public-mcp-paid-eval-results.md index 20162316638..7c76f9a65e6 100644 --- a/doc/plans/2026-10-01-public-mcp-paid-eval-results.md +++ b/doc/plans/2026-10-01-public-mcp-paid-eval-results.md @@ -189,6 +189,10 @@ Updated supporting verification: task is returned, preserving that path's response and avoiding a false event. The existing 12-test closed-workspace suite reproduces and verifies the fix. This narrow null-result correction follows the paid regression above. +- An exact-start boundary calibration also reproduced a missed event at the + subscription's timestamp. Scanning now includes that timestamp; the existing + verification-window test requires delivery at the exact start. This correction + also follows the paid regression, and all 33 MCP tests pass afterward. - Repository-wide typecheck and production build pass after merging master. - The Core → Cloud → Core authority round trip passes real OAuth, MCP 2.0 subscription, delivery, current Cloud membership loss, unsubscribe, legacy MCP diff --git a/server/src/__tests__/public-mcp.test.ts b/server/src/__tests__/public-mcp.test.ts index 6dafae8fc40..b0ade0752fb 100644 --- a/server/src/__tests__/public-mcp.test.ts +++ b/server/src/__tests__/public-mcp.test.ts @@ -447,9 +447,9 @@ describe.skipIf(!support.supported)("public MCP OAuth and tool boundary", () => } }); - it("retains changes occurring while callback ownership is being verified", async () => { + it("retains changes at the subscription start while callback ownership is being verified", async () => { const f = await eventFixture(); - f.duringVerification(async () => { await f.activity(); }); + f.duringVerification(async () => { f.advance(-100); 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); diff --git a/server/src/services/public-mcp/events.ts b/server/src/services/public-mcp/events.ts index 1f03cb6359f..04ac1b08150 100644 --- a/server/src/services/public-mcp/events.ts +++ b/server/src/services/public-mcp/events.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import { and, asc, count, eq, gt, isNotNull, isNull, lt, lte, or, sql } from "drizzle-orm"; +import { and, asc, count, eq, gt, gte, isNotNull, isNull, lt, lte, or, sql } from "drizzle-orm"; import { z } from "zod"; import { companies, activityLog, mcpEventDeliveries as deliveries, mcpEventSubscriptions as subscriptions, type Db } from "@paperclipai/db"; import { ISSUE_STATUSES } from "@paperclipai/shared"; @@ -141,7 +141,7 @@ export function createPublicMcpEvents(db: Db, oauth: PublicMcpOAuth, api: ApiDis const rows = await db.select({ activity: activityLog }).from(activityLog) .leftJoin(deliveries, and(eq(deliveries.subscriptionId, s.id), eq(deliveries.activityId, activityLog.id))) .where(and(eq(activityLog.companyId, s.companyId), eq(activityLog.entityType, "issue"), eq(activityLog.entityId, s.taskId), - gt(activityLog.createdAt, s.startsAt), lt(activityLog.createdAt, s.expiresAt), isNull(deliveries.id))) + gte(activityLog.createdAt, s.startsAt), lt(activityLog.createdAt, s.expiresAt), isNull(deliveries.id))) .orderBy(asc(activityLog.createdAt), asc(activityLog.id)).limit(100); for (const { activity } of rows) { const details = activity.details ?? {};