#199

feat: new mcp events api with both sep and chatgpt compat

Draftblurrahwants to merge intofrom

Stacked on #196.

Adds mcp-handler/events, aiming for SEP-3415's "as simple as implementing tools":

// lib/events.ts
import {
  connect,
  createMcpEvents,
  defineEvent,
  redisStore,
  vercelQueue,
} from "mcp-handler/events";
import { send } from "@vercel/queue";
import { createClient } from "redis";
import { z } from "zod";
import { canReadProject } from "./linear";

const redis = await createClient({ url: process.env.REDIS_URL }).connect();

export const issueCreated = defineEvent({
  name: "issue.created",
  input: z.strictObject({ projectId: z.string() }),
  payload: z.object({ id: z.string(), projectId: z.string() }),
  // Checked at subscribe time and before every delivery, with the
  // subscriber's Linear token from Vercel Connect.
  authorize: connect("linear/acme", (token, { projectId }) =>
    canReadProject(token, projectId),
  ),
});

export const events = createMcpEvents({
  events: [issueCreated],
  principal: (auth) => String(auth.extra?.sub),
  store: redisStore({ client: redis }),
  queue: vercelQueue({ topic: "mcp-events", send }),
});
// app/mcp/route.ts
import { createMcpHandler, withMcpAuth } from "mcp-handler";
import { events } from "@/lib/events";
import { registerTools, verifyToken } from "@/lib/mcp";

const handler = createMcpHandler(registerTools, { events });
export const POST = withMcpAuth(handler, verifyToken, { required: true });
// app/api/issues/route.ts (wherever changes happen)
import { events, issueCreated } from "@/lib/events";

export async function POST(request: Request) {
  const issue = await request.json();
  await events.emit(issueCreated, issue, { id: issue.id });
  return new Response(null, { status: 202 });
}
// app/api/mcp-events/route.ts (queue consumer)
import { handleCallback } from "@vercel/queue";
import { vercelQueueConsumer } from "mcp-handler/events";
import { events } from "@/lib/events";

export const POST = vercelQueueConsumer(events, handleCallback);
// vercel.json
{
  "functions": {
    "app/api/mcp-events/route.ts": {
      "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "mcp-events" }]
    }
  }
}

The engine stores subscriptions, verifies callbacks, signs and retries deliveries, and drops subscriptions once authorize fails.

Webhook delivery only. Speaks SEP-3415, and falls back to ChatGPT's dialect for clients that don't declare the extension. #196's helpers are now internal.