Custom Integrations
The three shipped integrations (Linear, GitHub, Slack) are concrete implementations of a single interface: IntegrationProvider, defined in packages/integrations-core/src/provider.ts. Implement that interface and your integration plugs into the same install / webhook / MCP-tools / publication shape.
What an integration provides
Section titled “What an integration provides”A provider is a small module that answers four questions:
- How do users install it? OAuth flow or credential paste.
- How do incoming webhooks map to sessions? Webhook payload → session dispatch.
- What can the agent do? A list of MCP tool descriptors + their executors.
- How is publication scoped? Per-workspace, per-issue, per-user, etc.
// packages/integrations-core/src/provider.ts (sketch)export interface IntegrationProvider { id: string; displayName: string;
install: { type: 'oauth' | 'credentials'; // ... };
parseWebhook(req: Request): WebhookEvent | null;
dispatch(event: WebhookEvent, ctx: ProviderContext): Promise<SessionDispatch>;
tools: McpToolDescriptor[]; executeTool(name: string, input: unknown, ctx: PublicationContext): Promise<unknown>;
publication: { scopes: PublicationScope[]; };}Anatomy of an existing provider
Section titled “Anatomy of an existing provider”Look at packages/linear/ (provider logic) + apps/integrations/src/routes/linear/publications.ts (thin CF wrapper) for a complete reference. The split is the same for GitHub and Slack:
packages/<provider>/src/├── provider.ts // implements IntegrationProvider — OAuth/PAT install, publish flow, MCP wiring├── webhook/parse.ts // verify signature, parse event → typed dispatch├── config.ts // capabilities (ALL_*_CAPABILITIES + DEFAULT_*_CAPABILITIES) + MCP URL└── oauth/ // OAuth client (where applicable)
apps/integrations/src/routes/<provider>/└── publications.ts // thin CF route that delegates into the providerWebhook entry mounting lives in packages/http-routes/src/integrations/gateway.ts (shared by both CF and Node hosts). The interesting design work is in which capabilities you expose and how you map webhooks to dispatches, not in plumbing.
Build a new provider
Section titled “Build a new provider”-
Add a folder under
apps/integrations/src/routes/<your-integration>/. -
Implement
IntegrationProvider. Start withid,displayName, the OAuth flow, and one MCP tool — get a session dispatched end-to-end before adding more. -
Register it.
apps/integrations/src/index.tshas a registry; add your provider to the list. The gateway exposes/install/<id>and/webhook/<id>automatically. -
Add UI install flow (optional but recommended). The Console has an
Integrationspage that picks up registered providers from/v1/integrations. -
Test locally.
pnpm dev:integrationsruns the gateway against your local main worker. Use a tunneling tool (cloudflared tunnel) to receive webhooks from the third-party service.
Persistence
Section titled “Persistence”Use the abstract repos in packages/integrations-adapters-cf/. They give you typed CRUD on installations, oauth_state, publications, and webhook_events against the shared MAIN_DB D1 instance. Don’t roll your own — the adapters are the same ones Linear/GitHub/Slack use, and they handle multi-tenant scoping for you.
Webhook security
Section titled “Webhook security”The base provider verifies webhook signatures using PLATFORM_ROOT_SECRET for outbound delivery and per-provider signing keys for inbound. Don’t bypass this. If the third-party service has its own signature scheme, validate it inside parseWebhook and return null on failure.
MCP tool design
Section titled “MCP tool design”Each tool is one capability — favor many small tools over few mega-tools. Names are namespaced as mcp__<provider>__<tool> (double underscore). Input schemas are JSON Schema.
{ name: 'mcp__jira__update_issue_status', description: 'Move a Jira issue to a new status.', inputSchema: { type: 'object', properties: { issue_key: { type: 'string', description: 'e.g. PROJ-123' }, status: { type: 'string', enum: ['Open', 'In Progress', 'Done'] }, }, required: ['issue_key', 'status'], },}The executor receives a PublicationContext with the OAuth token (already validated against the publication’s scopes) and the originating session metadata.