Skip to content

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.

A provider is a small module that answers four questions:

  1. How do users install it? OAuth flow or credential paste.
  2. How do incoming webhooks map to sessions? Webhook payload → session dispatch.
  3. What can the agent do? A list of MCP tool descriptors + their executors.
  4. 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[];
};
}

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 provider

Webhook 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.

  1. Add a folder under apps/integrations/src/routes/<your-integration>/.

  2. Implement IntegrationProvider. Start with id, displayName, the OAuth flow, and one MCP tool — get a session dispatched end-to-end before adding more.

  3. Register it. apps/integrations/src/index.ts has a registry; add your provider to the list. The gateway exposes /install/<id> and /webhook/<id> automatically.

  4. Add UI install flow (optional but recommended). The Console has an Integrations page that picks up registered providers from /v1/integrations.

  5. Test locally. pnpm dev:integrations runs the gateway against your local main worker. Use a tunneling tool (cloudflared tunnel) to receive webhooks from the third-party service.

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.

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.

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.