← back to Ventura Claw
app/lib/mcp/framework.ts
100 lines
// VenturaClaw — Connector Framework (Zapier-for-business)
// Every connector is a Zap-style bundle: Auth + Triggers + Actions + (optional) MCP server
export type AuthMethod = "oauth2" | "api_key" | "hmac" | "basic" | "smtp" | "none";
export interface AuthConfig {
method: AuthMethod;
scopes?: string[];
envKeys: string[]; // env var names that must be present
oauthAuthUrl?: string;
oauthTokenUrl?: string;
docsUrl?: string;
}
export interface FieldSpec {
key: string;
label: string;
type: "string" | "number" | "boolean" | "enum" | "datetime" | "json";
required?: boolean;
enum?: string[];
help?: string;
}
export interface Trigger<TIn = unknown, TOut = unknown> {
id: string; // e.g. "shopify.order.created"
label: string;
kind: "polling" | "webhook" | "subscription";
cronHint?: string;
inputs: FieldSpec[];
sample: TOut;
poll?: (ctx: ConnectorCtx, input: TIn) => Promise<TOut[]>;
webhookPath?: string;
}
export interface Action<TIn = unknown, TOut = unknown> {
id: string; // e.g. "shopify.product.update"
label: string;
sensitive: boolean; // true → must go through approval gate
inputs: FieldSpec[];
sample: TOut;
run: (ctx: ConnectorCtx, input: TIn) => Promise<TOut>;
}
export interface ConnectorCtx {
userId: number;
env: Record<string, string>;
fetch: typeof fetch;
log: (msg: string, level?: "info"|"warn"|"error") => void;
approval: <T>(actionId: string, payload: unknown, fn: () => Promise<T>) => Promise<T | { queued: true; approvalId: string }>;
}
export interface ConnectorMeta {
id: string;
name: string;
category: string;
homepage: string;
docsUrl: string;
difficulty: "easy" | "medium" | "hard";
priority: "v1" | "v2" | "later";
mcpServer?: { kind: "npm" | "git" | "local"; ref: string } | null;
rateLimit?: string;
webhookSupport?: boolean;
appReviewRequired?: boolean;
}
export interface Connector {
meta: ConnectorMeta;
auth: AuthConfig;
triggers: Trigger[];
actions: Action[];
}
export const REGISTRY = new Map<string, Connector>();
export function register(c: Connector) { REGISTRY.set(c.meta.id, c); return c; }
// ---------- Zap engine ----------
export interface Zap {
id: string;
name: string;
ownerId: number;
trigger: { connector: string; trigger: string; input: unknown };
steps: { connector: string; action: string; input: unknown }[];
active: boolean;
}
export async function runZap(zap: Zap, payload: unknown, ctx: ConnectorCtx): Promise<{ ok: boolean; results: unknown[] }> {
const results: unknown[] = [payload];
for (const step of zap.steps) {
const c = REGISTRY.get(step.connector);
if (!c) throw new Error(`unknown connector: ${step.connector}`);
const a = c.actions.find(x => x.id === step.action);
if (!a) throw new Error(`unknown action: ${step.action}`);
const out = a.sensitive
? await ctx.approval(a.id, step.input, () => a.run(ctx, step.input))
: await a.run(ctx, step.input);
results.push(out);
}
return { ok: true, results };
}