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.