Skip to content

v1.3 Plan 2: Workflows

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.

Goal: Add built-in workflows (onboard_domain, migrate_dns, apply_protection) that compose tools across providers based on roles, exposed as tools on the orchestrator's MCP server.

Architecture: A WorkflowEngine resolves capabilities to actual tool names at startup using pattern matching, then executes multi-step workflows by calling tools through the proxy. Workflows are registered as additional tools on the orchestrator's MCP server.

Tech Stack: TypeScript, vitest, @modelcontextprotocol/sdk


File Structure

packages/
  orchestrator/
    src/
      capability-resolver.ts   # Maps capability names to actual provider tool names
      workflows/
        engine.ts              # WorkflowEngine — registers and runs workflows
        onboard.ts             # onboard_domain workflow definition
        migrate.ts             # migrate_dns workflow definition
        protect.ts             # apply_protection workflow definition
      server.ts                # Modified — register workflow tools alongside proxied tools
    tests/
      capability-resolver.test.ts
      workflows/
        engine.test.ts
        onboard.test.ts
        migrate.test.ts
        protect.test.ts

Task 1: Capability Resolver

Files: - Create: packages/orchestrator/src/capability-resolver.ts - Create: packages/orchestrator/tests/capability-resolver.test.ts

The capability resolver maps abstract capability names (like create_zone) to actual provider tool names (like cloudflare_create_zone) by scanning tools registered for each role.

  • [ ] Step 1: Write the tests
// packages/orchestrator/tests/capability-resolver.test.ts

import { describe, it, expect } from "vitest";
import { CapabilityResolver } from "../src/capability-resolver.js";
import { McpToolDefinition } from "@infrastructure-mcp/shared";

function tool(name: string): McpToolDefinition {
  return { name, description: name, inputSchema: { type: "object" } };
}

describe("CapabilityResolver", () => {
  it("resolves a capability to matching tool name", () => {
    const resolver = new CapabilityResolver();
    resolver.registerProvider("cloudflare", ["dns_host"], [
      tool("cloudflare_list_zones"),
      tool("cloudflare_create_zone"),
      tool("cloudflare_get_dns"),
      tool("cloudflare_create_dns_record"),
    ]);

    expect(resolver.resolve("dns_host", "*create_zone*")).toBe("cloudflare.cloudflare_create_zone");
    expect(resolver.resolve("dns_host", "*list_zones*")).toBe("cloudflare.cloudflare_list_zones");
  });

  it("returns null when no tool matches the capability", () => {
    const resolver = new CapabilityResolver();
    resolver.registerProvider("cf", ["dns_host"], [tool("list_zones")]);

    expect(resolver.resolve("dns_host", "*create_zone*")).toBeNull();
  });

  it("returns null for unfilled role", () => {
    const resolver = new CapabilityResolver();

    expect(resolver.resolve("dns_host", "*list_zones*")).toBeNull();
  });

  it("resolves across multiple providers", () => {
    const resolver = new CapabilityResolver();
    resolver.registerProvider("cloudflare", ["dns_host"], [
      tool("list_zones"),
      tool("create_zone"),
    ]);
    resolver.registerProvider("namecheap", ["dns_registrar"], [
      tool("list_domains"),
      tool("get_nameservers"),
      tool("set_nameservers"),
    ]);

    expect(resolver.resolve("dns_host", "*create_zone*")).toBe("cloudflare.create_zone");
    expect(resolver.resolve("dns_registrar", "*set_nameservers*")).toBe("namecheap.set_nameservers");
  });

  it("checks if all required capabilities are available", () => {
    const resolver = new CapabilityResolver();
    resolver.registerProvider("cf", ["dns_host"], [
      tool("list_zones"),
      tool("create_zone"),
    ]);

    const required = [
      { role: "dns_host" as const, pattern: "*create_zone*" },
      { role: "dns_host" as const, pattern: "*list_zones*" },
    ];

    expect(resolver.hasAll(required)).toBe(true);
  });

  it("returns false when a required capability is missing", () => {
    const resolver = new CapabilityResolver();
    resolver.registerProvider("cf", ["dns_host"], [tool("list_zones")]);

    const required = [
      { role: "dns_host" as const, pattern: "*create_zone*" },
      { role: "dns_host" as const, pattern: "*list_zones*" },
    ];

    expect(resolver.hasAll(required)).toBe(false);
  });

  it("returns missing capabilities list", () => {
    const resolver = new CapabilityResolver();
    resolver.registerProvider("cf", ["dns_host"], [tool("list_zones")]);

    const required = [
      { role: "dns_host" as const, pattern: "*create_zone*" },
      { role: "dns_host" as const, pattern: "*list_zones*" },
      { role: "dns_registrar" as const, pattern: "*get_dns*" },
    ];

    const missing = resolver.getMissing(required);

    expect(missing).toHaveLength(2);
    expect(missing).toContainEqual({ role: "dns_host", pattern: "*create_zone*" });
    expect(missing).toContainEqual({ role: "dns_registrar", pattern: "*get_dns*" });
  });
});
  • [ ] Step 2: Run tests to verify they fail

Run: cd packages/orchestrator && npx vitest run tests/capability-resolver.test.ts

  • [ ] Step 3: Implement capability-resolver.ts
// packages/orchestrator/src/capability-resolver.ts

import { McpToolDefinition, Role } from "@infrastructure-mcp/shared";
import { matchesPattern } from "@infrastructure-mcp/shared";

export interface CapabilityRequirement {
  role: Role;
  pattern: string;
}

export class CapabilityResolver {
  /** role -> { providerName, tools[] } */
  private providers = new Map<string, { providerName: string; tools: McpToolDefinition[] }>();

  registerProvider(providerName: string, roles: Role[], tools: McpToolDefinition[]): void {
    for (const role of roles) {
      this.providers.set(role, { providerName, tools });
    }
  }

  /**
   * Resolve a capability pattern for a role to a namespaced tool name.
   * Returns "providerName.toolName" or null if not found.
   */
  resolve(role: string, pattern: string): string | null {
    const entry = this.providers.get(role);
    if (!entry) return null;

    for (const tool of entry.tools) {
      if (matchesPattern(tool.name, pattern)) {
        return `${entry.providerName}.${tool.name}`;
      }
    }

    return null;
  }

  /** Check if all required capabilities are resolvable. */
  hasAll(requirements: CapabilityRequirement[]): boolean {
    return requirements.every((req) => this.resolve(req.role, req.pattern) !== null);
  }

  /** Return the subset of requirements that cannot be resolved. */
  getMissing(requirements: CapabilityRequirement[]): CapabilityRequirement[] {
    return requirements.filter((req) => this.resolve(req.role, req.pattern) === null);
  }
}
  • [ ] Step 4: Run tests

Run: cd packages/orchestrator && npx vitest run tests/capability-resolver.test.ts Expected: All pass.

  • [ ] Step 5: Commit
git add packages/orchestrator/src/capability-resolver.ts packages/orchestrator/tests/capability-resolver.test.ts
git commit -m "feat(orchestrator): add capability resolver for workflow tool mapping"

Task 2: Workflow Engine

Files: - Create: packages/orchestrator/src/workflows/engine.ts - Create: packages/orchestrator/tests/workflows/engine.test.ts

The engine holds workflow definitions, checks which are enabled based on config + capability availability, and executes them by calling tools through the proxy.

  • [ ] Step 1: Write the tests
// packages/orchestrator/tests/workflows/engine.test.ts

import { describe, it, expect, vi } from "vitest";
import { WorkflowEngine, WorkflowDefinition } from "../../src/workflows/engine.js";
import { CapabilityResolver } from "../../src/capability-resolver.js";
import { ProviderProxy } from "../../src/proxy.js";

function makeResolver(): CapabilityResolver {
  const resolver = new CapabilityResolver();
  resolver.registerProvider("cf", ["dns_host", "cdn", "security"], [
    { name: "create_zone", inputSchema: { type: "object" } },
    { name: "get_dns", inputSchema: { type: "object" } },
    { name: "create_dns_record", inputSchema: { type: "object" } },
    { name: "apply_protection", inputSchema: { type: "object" } },
    { name: "apply_cache_settings", inputSchema: { type: "object" } },
  ]);
  resolver.registerProvider("nc", ["dns_registrar"], [
    { name: "get_dns_hosts", inputSchema: { type: "object" } },
    { name: "set_nameservers", inputSchema: { type: "object" } },
  ]);
  return resolver;
}

function makeProxy(): ProviderProxy {
  return {
    callTool: vi.fn().mockResolvedValue({ content: '{"ok":true}', isError: false }),
    getAllTools: vi.fn().mockReturnValue([]),
    startAll: vi.fn(),
    shutdownAll: vi.fn(),
    getStatuses: vi.fn().mockReturnValue([]),
    getProviderForRole: vi.fn(),
  } as unknown as ProviderProxy;
}

describe("WorkflowEngine", () => {
  it("registers a workflow and lists it as a tool", () => {
    const engine = new WorkflowEngine(makeResolver(), makeProxy());
    const workflow: WorkflowDefinition = {
      name: "test_workflow",
      description: "A test workflow",
      parameters: { domain: { type: "string", description: "Domain" } },
      requiredParams: ["domain"],
      requirements: [{ role: "dns_host", pattern: "*create_zone*" }],
      execute: vi.fn(),
    };

    engine.register(workflow);
    const tools = engine.getTools();

    expect(tools).toHaveLength(1);
    expect(tools[0].name).toBe("test_workflow");
  });

  it("skips workflows with unmet requirements", () => {
    const resolver = new CapabilityResolver(); // empty — no providers
    const engine = new WorkflowEngine(resolver, makeProxy());
    const workflow: WorkflowDefinition = {
      name: "test_workflow",
      description: "A test",
      parameters: {},
      requiredParams: [],
      requirements: [{ role: "dns_host", pattern: "*create_zone*" }],
      execute: vi.fn(),
    };

    engine.register(workflow);
    const tools = engine.getTools();

    expect(tools).toHaveLength(0);
  });

  it("executes a workflow by calling its execute function", async () => {
    const resolver = makeResolver();
    const proxy = makeProxy();
    const executeFn = vi.fn().mockResolvedValue({ domain: "test.com", success: true });

    const engine = new WorkflowEngine(resolver, proxy);
    engine.register({
      name: "test_workflow",
      description: "Test",
      parameters: { domain: { type: "string", description: "Domain" } },
      requiredParams: ["domain"],
      requirements: [{ role: "dns_host", pattern: "*create_zone*" }],
      execute: executeFn,
    });

    const result = await engine.execute("test_workflow", { domain: "test.com" });

    expect(result.isError).toBe(false);
    expect(executeFn).toHaveBeenCalledWith(
      { domain: "test.com" },
      expect.objectContaining({ proxy, resolver })
    );
  });

  it("returns error for unknown workflow", async () => {
    const engine = new WorkflowEngine(makeResolver(), makeProxy());
    const result = await engine.execute("nonexistent", {});

    expect(result.isError).toBe(true);
    expect(result.content).toContain("nonexistent");
  });

  it("returns error when execute throws", async () => {
    const resolver = makeResolver();
    const engine = new WorkflowEngine(resolver, makeProxy());
    engine.register({
      name: "failing",
      description: "Fails",
      parameters: {},
      requiredParams: [],
      requirements: [],
      execute: vi.fn().mockRejectedValue(new Error("boom")),
    });

    const result = await engine.execute("failing", {});

    expect(result.isError).toBe(true);
    expect(result.content).toContain("boom");
  });
});
  • [ ] Step 2: Run tests to verify they fail

  • [ ] Step 3: Implement engine.ts

// packages/orchestrator/src/workflows/engine.ts

import { McpToolDefinition, ToolResult } from "@infrastructure-mcp/shared";
import { CapabilityResolver, CapabilityRequirement } from "../capability-resolver.js";
import { ProviderProxy } from "../proxy.js";

export interface WorkflowContext {
  proxy: ProviderProxy;
  resolver: CapabilityResolver;
}

export interface WorkflowDefinition {
  name: string;
  description: string;
  parameters: Record<string, { type: string; description: string }>;
  requiredParams: string[];
  requirements: CapabilityRequirement[];
  execute: (
    args: Record<string, unknown>,
    ctx: WorkflowContext,
  ) => Promise<Record<string, unknown>>;
}

export class WorkflowEngine {
  private workflows = new Map<string, WorkflowDefinition>();
  private enabled = new Set<string>();
  private resolver: CapabilityResolver;
  private proxy: ProviderProxy;

  constructor(resolver: CapabilityResolver, proxy: ProviderProxy) {
    this.resolver = resolver;
    this.proxy = proxy;
  }

  register(workflow: WorkflowDefinition): void {
    this.workflows.set(workflow.name, workflow);

    if (this.resolver.hasAll(workflow.requirements)) {
      this.enabled.add(workflow.name);
    } else {
      const missing = this.resolver.getMissing(workflow.requirements);
      console.warn(
        `Workflow '${workflow.name}' disabled: missing capabilities: ${missing.map((m) => `${m.role}:${m.pattern}`).join(", ")}`
      );
    }
  }

  getTools(): McpToolDefinition[] {
    const tools: McpToolDefinition[] = [];

    for (const [name, workflow] of this.workflows) {
      if (!this.enabled.has(name)) continue;

      tools.push({
        name: workflow.name,
        description: workflow.description,
        inputSchema: {
          type: "object",
          properties: workflow.parameters,
          required: workflow.requiredParams,
        },
        annotations: { destructiveHint: true },
      });
    }

    return tools;
  }

  async execute(name: string, args: Record<string, unknown>): Promise<ToolResult> {
    const workflow = this.workflows.get(name);

    if (!workflow || !this.enabled.has(name)) {
      return {
        content: `Unknown or disabled workflow: ${name}`,
        isError: true,
      };
    }

    try {
      const result = await workflow.execute(args, {
        proxy: this.proxy,
        resolver: this.resolver,
      });

      return { content: JSON.stringify(result, null, 2), isError: false };
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      return { content: `Workflow '${name}' failed: ${msg}`, isError: true };
    }
  }
}
  • [ ] Step 4: Run tests

  • [ ] Step 5: Commit

git add packages/orchestrator/src/workflows/engine.ts packages/orchestrator/tests/workflows/engine.test.ts
git commit -m "feat(orchestrator): add workflow engine with capability checking"

Task 3: Workflow Definitions

Files: - Create: packages/orchestrator/src/workflows/onboard.ts - Create: packages/orchestrator/src/workflows/migrate.ts - Create: packages/orchestrator/src/workflows/protect.ts - Create: packages/orchestrator/src/workflows/index.ts - Create: packages/orchestrator/tests/workflows/onboard.test.ts

Each workflow definition specifies requirements, parameters, and an execute function that calls tools through the proxy.

  • [ ] Step 1: Write onboard test
// packages/orchestrator/tests/workflows/onboard.test.ts

import { describe, it, expect, vi } from "vitest";
import { onboardWorkflow } from "../../src/workflows/onboard.js";
import { CapabilityResolver } from "../../src/capability-resolver.js";
import { ProviderProxy } from "../../src/proxy.js";

function makeResolver(): CapabilityResolver {
  const resolver = new CapabilityResolver();
  resolver.registerProvider("cf", ["dns_host", "cdn", "security"], [
    { name: "create_zone", inputSchema: { type: "object" } },
    { name: "get_dns_records", inputSchema: { type: "object" } },
    { name: "create_dns_record", inputSchema: { type: "object" } },
    { name: "apply_protection", inputSchema: { type: "object" } },
    { name: "apply_cache_settings", inputSchema: { type: "object" } },
  ]);
  resolver.registerProvider("nc", ["dns_registrar"], [
    { name: "get_dns_hosts", inputSchema: { type: "object" } },
    { name: "set_nameservers", inputSchema: { type: "object" } },
    { name: "list_domains", inputSchema: { type: "object" } },
  ]);
  return resolver;
}

describe("onboardWorkflow", () => {
  it("has correct name and requirements", () => {
    expect(onboardWorkflow.name).toBe("onboard_domain");
    expect(onboardWorkflow.requirements.length).toBeGreaterThan(0);
    expect(onboardWorkflow.requiredParams).toContain("domain");
  });

  it("calls create_zone, get_dns, create_dns, set_nameservers, and protection tools", async () => {
    const resolver = makeResolver();
    const callTool = vi.fn()
      .mockResolvedValueOnce({ content: '{"id":"zone1","nameServers":["ns1.cf.com","ns2.cf.com"]}', isError: false }) // create_zone
      .mockResolvedValueOnce({ content: '[{"type":"A","name":"@","content":"1.2.3.4"}]', isError: false }) // get_dns
      .mockResolvedValueOnce({ content: '{"id":"rec1"}', isError: false }) // create_dns_record
      .mockResolvedValueOnce({ content: '{"success":true}', isError: false }) // set_nameservers
      .mockResolvedValueOnce({ content: '{"applied":5}', isError: false }) // apply_protection
      .mockResolvedValueOnce({ content: '{"applied":3}', isError: false }); // apply_cache

    const proxy = { callTool } as unknown as ProviderProxy;

    const result = await onboardWorkflow.execute(
      { domain: "example.com" },
      { proxy, resolver },
    );

    expect(result.domain).toBe("example.com");
    expect(callTool).toHaveBeenCalledWith("cf.create_zone", expect.objectContaining({ domain: "example.com" }));
  });

  it("stops and returns error if create_zone fails", async () => {
    const resolver = makeResolver();
    const callTool = vi.fn()
      .mockResolvedValueOnce({ content: "Zone already exists", isError: true });

    const proxy = { callTool } as unknown as ProviderProxy;

    await expect(
      onboardWorkflow.execute({ domain: "example.com" }, { proxy, resolver })
    ).rejects.toThrow();
  });
});
  • [ ] Step 2: Run tests to verify they fail

  • [ ] Step 3: Implement workflow definitions

// packages/orchestrator/src/workflows/onboard.ts

import { WorkflowDefinition, WorkflowContext } from "./engine.js";

async function callAndParse(
  ctx: WorkflowContext,
  toolName: string,
  args: Record<string, unknown>,
): Promise<unknown> {
  const result = await ctx.proxy.callTool(toolName, args);
  if (result.isError) {
    throw new Error(`${toolName} failed: ${result.content}`);
  }
  try {
    return JSON.parse(result.content);
  } catch {
    return result.content;
  }
}

function resolveTool(ctx: WorkflowContext, role: string, pattern: string): string {
  const tool = ctx.resolver.resolve(role, pattern);
  if (!tool) throw new Error(`No tool found for ${role}:${pattern}`);
  return tool;
}

export const onboardWorkflow: WorkflowDefinition = {
  name: "onboard_domain",
  description: "Onboard a domain: create zone at DNS host, migrate DNS from registrar, update nameservers, apply security and CDN settings",
  parameters: {
    domain: { type: "string", description: "Domain to onboard (e.g. example.com)" },
    migrateRecords: { type: "boolean", description: "Migrate DNS records from registrar (default true)" },
    applyProtection: { type: "boolean", description: "Apply security and CDN settings (default true)" },
  },
  requiredParams: ["domain"],
  requirements: [
    { role: "dns_host", pattern: "*create_zone*" },
    { role: "dns_registrar", pattern: "*get_dns*" },
    { role: "dns_registrar", pattern: "*set_nameservers*" },
    { role: "security", pattern: "*protection*" },
  ],
  async execute(args, ctx): Promise<Record<string, unknown>> {
    const domain = args.domain as string;
    const migrateRecords = args.migrateRecords !== false;
    const applyProtection = args.applyProtection !== false;

    const summary: Record<string, unknown> = { domain };

    // 1. Create zone at DNS host
    const createZoneTool = resolveTool(ctx, "dns_host", "*create_zone*");
    const zone = await callAndParse(ctx, createZoneTool, { domain }) as Record<string, unknown>;
    summary.zoneId = zone.id;
    summary.nameServers = zone.nameServers;

    // 2. Migrate DNS records
    if (migrateRecords) {
      const getDnsTool = resolveTool(ctx, "dns_registrar", "*get_dns*");
      const records = await callAndParse(ctx, getDnsTool, { domain }) as unknown[];
      summary.recordsFound = Array.isArray(records) ? records.length : 0;

      if (Array.isArray(records)) {
        const createDnsTool = resolveTool(ctx, "dns_host", "*create_dns*");
        let created = 0;
        const errors: string[] = [];

        for (const record of records) {
          try {
            await callAndParse(ctx, createDnsTool, {
              zoneId: zone.id,
              ...(typeof record === "object" && record !== null ? record : {}),
            });
            created++;
          } catch (err) {
            errors.push(err instanceof Error ? err.message : String(err));
          }
        }

        summary.recordsMigrated = created;
        if (errors.length > 0) summary.migrationErrors = errors;
      }

      // 3. Update nameservers at registrar
      const nameServers = zone.nameServers;
      if (Array.isArray(nameServers) && nameServers.length > 0) {
        const setNsTool = resolveTool(ctx, "dns_registrar", "*set_nameservers*");
        try {
          await callAndParse(ctx, setNsTool, { domain, nameservers: nameServers });
          summary.nameserversUpdated = true;
        } catch (err) {
          summary.nameserversUpdated = false;
          summary.nameserverError = err instanceof Error ? err.message : String(err);
        }
      }
    }

    // 4. Apply protection
    if (applyProtection) {
      const protectionTool = resolveTool(ctx, "security", "*protection*");
      try {
        const protResult = await callAndParse(ctx, protectionTool, { domain }) as Record<string, unknown>;
        summary.protectionApplied = protResult;
      } catch (err) {
        summary.protectionError = err instanceof Error ? err.message : String(err);
      }

      // CDN settings (optional — may not have a matching tool)
      const cdnTool = ctx.resolver.resolve("cdn", "*cache*");
      if (cdnTool) {
        try {
          const cdnResult = await callAndParse(ctx, cdnTool, { domain }) as Record<string, unknown>;
          summary.cdnApplied = cdnResult;
        } catch (err) {
          summary.cdnError = err instanceof Error ? err.message : String(err);
        }
      }
    }

    return summary;
  },
};
// packages/orchestrator/src/workflows/migrate.ts

import { WorkflowDefinition, WorkflowContext } from "./engine.js";

async function callAndParse(
  ctx: WorkflowContext,
  toolName: string,
  args: Record<string, unknown>,
): Promise<unknown> {
  const result = await ctx.proxy.callTool(toolName, args);
  if (result.isError) {
    throw new Error(`${toolName} failed: ${result.content}`);
  }
  try {
    return JSON.parse(result.content);
  } catch {
    return result.content;
  }
}

function resolveTool(ctx: WorkflowContext, role: string, pattern: string): string {
  const tool = ctx.resolver.resolve(role, pattern);
  if (!tool) throw new Error(`No tool found for ${role}:${pattern}`);
  return tool;
}

export const migrateWorkflow: WorkflowDefinition = {
  name: "migrate_dns",
  description: "Migrate DNS records from registrar to DNS host",
  parameters: {
    domain: { type: "string", description: "Domain to migrate DNS for" },
  },
  requiredParams: ["domain"],
  requirements: [
    { role: "dns_registrar", pattern: "*get_dns*" },
    { role: "dns_host", pattern: "*create_dns*" },
  ],
  async execute(args, ctx): Promise<Record<string, unknown>> {
    const domain = args.domain as string;
    const summary: Record<string, unknown> = { domain };

    const getDnsTool = resolveTool(ctx, "dns_registrar", "*get_dns*");
    const records = await callAndParse(ctx, getDnsTool, { domain }) as unknown[];
    summary.recordsFound = Array.isArray(records) ? records.length : 0;

    if (Array.isArray(records)) {
      const createDnsTool = resolveTool(ctx, "dns_host", "*create_dns*");
      let created = 0;
      const errors: string[] = [];

      for (const record of records) {
        try {
          await callAndParse(ctx, createDnsTool, {
            domain,
            ...(typeof record === "object" && record !== null ? record : {}),
          });
          created++;
        } catch (err) {
          errors.push(err instanceof Error ? err.message : String(err));
        }
      }

      summary.recordsMigrated = created;
      if (errors.length > 0) summary.errors = errors;
    }

    return summary;
  },
};
// packages/orchestrator/src/workflows/protect.ts

import { WorkflowDefinition, WorkflowContext } from "./engine.js";

async function callAndParse(
  ctx: WorkflowContext,
  toolName: string,
  args: Record<string, unknown>,
): Promise<unknown> {
  const result = await ctx.proxy.callTool(toolName, args);
  if (result.isError) {
    throw new Error(`${toolName} failed: ${result.content}`);
  }
  try {
    return JSON.parse(result.content);
  } catch {
    return result.content;
  }
}

export const protectWorkflow: WorkflowDefinition = {
  name: "apply_protection",
  description: "Apply security and CDN settings to a domain",
  parameters: {
    domain: { type: "string", description: "Domain to apply protection to" },
  },
  requiredParams: ["domain"],
  requirements: [
    { role: "security", pattern: "*protection*" },
  ],
  async execute(args, ctx): Promise<Record<string, unknown>> {
    const domain = args.domain as string;
    const summary: Record<string, unknown> = { domain };

    const protectionTool = ctx.resolver.resolve("security", "*protection*");
    if (protectionTool) {
      const result = await callAndParse(ctx, protectionTool, { domain }) as Record<string, unknown>;
      summary.security = result;
    }

    const cdnTool = ctx.resolver.resolve("cdn", "*cache*");
    if (cdnTool) {
      const result = await callAndParse(ctx, cdnTool, { domain }) as Record<string, unknown>;
      summary.cdn = result;
    }

    return summary;
  },
};
// packages/orchestrator/src/workflows/index.ts

export { WorkflowEngine } from "./engine.js";
export type { WorkflowDefinition, WorkflowContext } from "./engine.js";
export { onboardWorkflow } from "./onboard.js";
export { migrateWorkflow } from "./migrate.js";
export { protectWorkflow } from "./protect.js";
  • [ ] Step 4: Run all tests

  • [ ] Step 5: Commit

git add packages/orchestrator/src/workflows/ packages/orchestrator/tests/workflows/
git commit -m "feat(orchestrator): add onboard, migrate, and protect workflow definitions"

Task 4: Wire Workflows into Server + Proxy

Files: - Modify: packages/orchestrator/src/proxy.ts — add CapabilityResolver, WorkflowEngine - Modify: packages/orchestrator/src/server.ts — register workflow tools - Modify: packages/orchestrator/src/index.ts — export new modules

The proxy creates the CapabilityResolver alongside the ToolRouter during startup. The WorkflowEngine registers workflows. The server includes workflow tools in tools/list and routes workflow tool calls through the engine.

  • [ ] Step 1: Update proxy.ts

Add a getCapabilityResolver() method and populate it during startAll():

// In proxy.ts — add CapabilityResolver import and usage
private capabilityResolver = new CapabilityResolver();

// In startAll(), after registering in router:
this.capabilityResolver.registerProvider(
  providerConfig.name,
  providerConfig.roles as Role[],
  tools,
);

// New public method:
getCapabilityResolver(): CapabilityResolver {
  return this.capabilityResolver;
}
  • [ ] Step 2: Update server.ts

After starting the proxy, create WorkflowEngine, register workflows, then merge workflow tools into tools/list and handle workflow tool calls:

// After proxy.startAll():
const resolver = proxy.getCapabilityResolver();
const workflowEngine = new WorkflowEngine(resolver, proxy);

// Register built-in workflows (only those enabled in config)
const workflowDefs = [onboardWorkflow, migrateWorkflow, protectWorkflow];
for (const def of workflowDefs) {
  const wfConfig = config.workflows[def.name];
  if (wfConfig && wfConfig.enabled === false) continue;
  workflowEngine.register(def);
}

const providerTools = proxy.getAllTools();
const workflowTools = workflowEngine.getTools();
const allTools = [...providerTools, ...workflowTools];

// In CallToolRequestSchema handler, check workflow engine first:
const workflowNames = new Set(workflowTools.map(t => t.name));
if (workflowNames.has(name)) {
  const result = await workflowEngine.execute(name, args ?? {});
  return { content: [{ type: "text", text: result.content }], isError: result.isError };
}
// Otherwise route to proxy as before
  • [ ] Step 3: Update index.ts

Add exports for new modules.

  • [ ] Step 4: Run all tests

Run: cd packages/orchestrator && npx vitest run Run: cd packages/orchestrator && npx tsc --noEmit

  • [ ] Step 5: Commit
git add packages/orchestrator/src/proxy.ts packages/orchestrator/src/server.ts packages/orchestrator/src/index.ts
git commit -m "feat(orchestrator): wire workflows into server and proxy"

Task 5: Full Test Suite Verification

  • [ ] Step 1: Run all shared tests

Run: cd packages/shared && npx vitest run

  • [ ] Step 2: Run all orchestrator tests

Run: cd packages/orchestrator && npx vitest run

  • [ ] Step 3: Build both packages

Run: cd packages/shared && npx tsc && cd ../orchestrator && npx tsc

  • [ ] Step 4: Smoke test

Run: cd packages/orchestrator && npx tsx bin/cli.ts --validate --config /tmp/test-config.json with a valid config.