← 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 };
}