Corsair needs five tables. Run this once, then you’re done.
SQLite
PostgreSQL
Install the driver, then run the migration:
npm install better-sqlite3
npm install --save-dev @types/better-sqlite3
Install the driver, then run the migration:
npm install pg
npm install --save-dev @types/pg
SQLite
PostgreSQL
View migration SQL
migration.sql
CREATE TABLE IF NOT EXISTS corsair_integrations ( id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, name TEXT NOT NULL, config TEXT NOT NULL DEFAULT '{}', dek TEXT NULL);CREATE TABLE IF NOT EXISTS corsair_accounts ( id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, tenant_id TEXT NOT NULL, integration_id TEXT NOT NULL, config TEXT NOT NULL DEFAULT '{}', dek TEXT NULL, FOREIGN KEY (integration_id) REFERENCES corsair_integrations(id));CREATE TABLE IF NOT EXISTS corsair_entities ( id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, account_id TEXT NOT NULL, entity_id TEXT NOT NULL, entity_type TEXT NOT NULL, version TEXT NOT NULL, data TEXT NOT NULL DEFAULT '{}', FOREIGN KEY (account_id) REFERENCES corsair_accounts(id));CREATE TABLE IF NOT EXISTS corsair_events ( id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, account_id TEXT NOT NULL, event_type TEXT NOT NULL, payload TEXT NOT NULL DEFAULT '{}', status TEXT, FOREIGN KEY (account_id) REFERENCES corsair_accounts(id));
sqlite3 corsair.db < migration.sql
SQL
Drizzle
Prisma
View migration SQL
migration.sql
CREATE TABLE IF NOT EXISTS corsair_integrations ( id TEXT PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), name TEXT NOT NULL, config JSONB NOT NULL DEFAULT '{}', dek TEXT NULL);CREATE TABLE IF NOT EXISTS corsair_accounts ( id TEXT PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), tenant_id TEXT NOT NULL, integration_id TEXT NOT NULL REFERENCES corsair_integrations(id), config JSONB NOT NULL DEFAULT '{}', dek TEXT NULL);CREATE TABLE IF NOT EXISTS corsair_entities ( id TEXT PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), account_id TEXT NOT NULL REFERENCES corsair_accounts(id), entity_id TEXT NOT NULL, entity_type TEXT NOT NULL, version TEXT NOT NULL, data JSONB NOT NULL DEFAULT '{}');CREATE TABLE IF NOT EXISTS corsair_events ( id TEXT PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), account_id TEXT NOT NULL REFERENCES corsair_accounts(id), event_type TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}', status TEXT);
This is the one file that wires everything together. Create it at src/server/corsair.ts:
SQLite
PostgreSQL
src/server/corsair.ts
import 'dotenv/config';import Database from 'better-sqlite3';import { createCorsair } from 'corsair';import { github } from '@corsair-dev/github';const db = new Database('corsair.db');export const corsair = createCorsair({ plugins: [github()], database: db, kek: process.env.CORSAIR_KEK!,});
SQL
Drizzle
Prisma
src/server/corsair.ts
import 'dotenv/config';import { Pool } from 'pg';import { createCorsair } from 'corsair';import { github } from '@corsair-dev/github';const db = new Pool({ connectionString: process.env.DATABASE_URL });export const corsair = createCorsair({ plugins: [github()], database: db, kek: process.env.CORSAIR_KEK!,});
src/server/corsair.ts
import 'dotenv/config';import { Pool } from 'pg';import { drizzle } from 'drizzle-orm/node-postgres';import { createCorsair } from 'corsair';import { github } from '@corsair-dev/github';const pool = new Pool({ connectionString: process.env.DATABASE_URL });const db = drizzle(pool);export const corsair = createCorsair({ plugins: [github()], database: pool, kek: process.env.CORSAIR_KEK!,});
src/server/corsair.ts
import 'dotenv/config';import { PrismaClient } from '@prisma/client';import { createCorsair } from 'corsair';import { github } from '@corsair-dev/github';const db = new PrismaClient();export const corsair = createCorsair({ plugins: [github()], database: pool, kek: process.env.CORSAIR_KEK!,});
You’ll add more plugins here later — slack(), linear(), gmail() — same pattern, just append to the array.
CORSAIR_KEK not loading?
If process.env.CORSAIR_KEK is undefined at runtime, your .env file isn’t being picked up. Install dotenv and add one line at the top of src/server/corsair.ts:
npm install dotenv
src/server/corsair.ts
import 'dotenv/config'; // add this lineimport Database from 'better-sqlite3';// ...
Then re-run pnpm install and retry the setup command.
Corsair stores your token, creates the database rows, and fetches your GitHub data locally. Your agent can now query it instantly — no API call needed every time.
There’s multiple ways to use Corsair. Here’s three easy ways:
MCP / Agent
CLI
Direct (Hardcode)
Let the agent discover and call endpoints on its own. You prompt in plain English — Corsair handles the rest.Install the MCP adapter alongside your preferred framework:
Anthropic SDK
npm install @corsair-dev/mcp @anthropic-ai/sdk
agent.ts
import Anthropic from '@anthropic-ai/sdk';import { AnthropicProvider } from '@corsair-dev/mcp';import { corsair } from './src/server/corsair';async function main() { const provider = new AnthropicProvider(); const tools = provider.build({ corsair }); const client = new Anthropic(); const message = await client.beta.messages.toolRunner({ model: 'claude-sonnet-4-6', max_tokens: 4096, tools, messages: [{ role: 'user', content: 'Use Corsair to list my GitHub repos with the most open issues.', }], }); for (const block of message.content) { if (block.type === 'text') console.log(block.text); }}main().catch(console.error);
import { createSdkMcpServer, query } from '@anthropic-ai/claude-agent-sdk';import { ClaudeProvider } from '@corsair-dev/mcp';import { corsair } from './src/server/corsair';async function main() { const provider = new ClaudeProvider(); const tools = await provider.build({ corsair }); const server = createSdkMcpServer({ name: 'corsair', tools }); const stream = query({ prompt: 'Use Corsair to list my GitHub repos with the most open issues.', options: { model: 'claude-opus-4-6', mcpServers: { corsair: server }, allowedTools: ['mcp__corsair__corsair_setup', 'mcp__corsair__list_operations', 'mcp__corsair__get_schema', 'mcp__corsair__run_script'], }, }); for await (const event of stream) { if ('result' in event) process.stdout.write(event.result); }}main().catch(console.error);
OpenAI Agents
npm install @corsair-dev/mcp @openai/agents
agent.ts
import { OpenAIAgentsProvider } from '@corsair-dev/mcp';import { Agent, run, tool } from '@openai/agents';import { corsair } from './src/server/corsair';async function main() { const provider = new OpenAIAgentsProvider(); const tools = provider.build({ corsair, tool }); const agent = new Agent({ name: 'corsair-agent', model: 'gpt-4.1', instructions: 'You have access to Corsair tools. Use list_operations to discover ' + 'available APIs, get_schema to understand arguments, and run_script ' + 'to execute them.', tools, }); const result = await run(agent, 'Use Corsair. List my GitHub repos with the most open issues.'); console.log(result.finalOutput);}main().catch(console.error);
Your agent gets four tools automatically — regardless of which framework you use:
Tool
What it does
corsair_setup
Check auth and get credential instructions
list_operations
Discover every available API endpoint
get_schema
Inspect parameters for a specific endpoint
run_script
Execute a JS snippet with corsair in scope
See MCP Adapters for Vercel AI SDK, Mastra, and more.
Call any endpoint from plain TypeScript — no agent, no framework required.The pattern is always: corsair.[plugin].[api | db].[group].[method]()API — live calls to GitHub:
// Get a specific repoconst repo = await corsair.github.api.repositories.get({ owner: 'octocat', repo: 'hello-world',});console.log(repo.name); // "hello-world"
DB — query what’s already cached locally:After backfill runs (or after any API call), data lives in your database. Query it without touching the GitHub API:
// Every repo you've fetched — instant, no networkconst repos = await corsair.github.db.repositories.search({});// Filter in JS using camelCase data fieldsconst withIssues = repos.filter((r) => Number(r.data?.openIssuesCount ?? 0) > 0);for (const repo of withIssues) { console.log(`${repo.data?.name} — ${repo.data?.openIssuesCount} open issues`);}