Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/core/src/catalog/facets/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ export const processFacet: Facet = {
writer: "workflows",
config: {
package: "@codemcp/workflows-server@latest",
ref: "workflows"
ref: "workflows",
env: {
VIBE_WORKFLOW_DOMAINS: "skilled"
},
allowedTools: [
"whats_next",
"conduct_review",
"list_workflows",
"get_tool_info"
]
}
},
{
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
} from "./types.js";
import { instructionWriter } from "./writers/instruction.js";
import { workflowsWriter } from "./writers/workflows.js";
import { mcpServerWriter } from "./writers/mcp-server.js";
import { skillsWriter } from "./writers/skills.js";
import { knowledgeWriter } from "./writers/knowledge.js";
import { gitHooksWriter } from "./writers/git-hooks.js";
Expand Down Expand Up @@ -51,15 +52,15 @@ export function createDefaultRegistry(): WriterRegistry {

registerProvisionWriter(registry, instructionWriter);
registerProvisionWriter(registry, workflowsWriter);
registerProvisionWriter(registry, mcpServerWriter);
registerProvisionWriter(registry, skillsWriter);

registerProvisionWriter(registry, knowledgeWriter);
registerProvisionWriter(registry, gitHooksWriter);
registerProvisionWriter(registry, setupNoteWriter);
registerProvisionWriter(registry, permissionPolicyWriter);

// Stub writers for types not yet implemented
for (const id of ["mcp-server", "installable"]) {
for (const id of ["installable"]) {
registerProvisionWriter(registry, {
id,
write: async () => ({})
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,35 @@ describe("resolve", () => {
expect(result.mcp_servers.length).toBeGreaterThanOrEqual(1);
expect(result.instructions).toContain("Extra instruction");
});

it("env set in the catalog option config is forwarded to the resolved mcp_server entry", async () => {
const userConfig: UserConfig = {
choices: { process: "codemcp-workflows" }
};

// Patch the catalog option's provision config to include env
const processFacet = catalog.facets.find((f) => f.id === "process")!;
const option = processFacet.options.find(
(o) => o.id === "codemcp-workflows"
)!;
const workflowsProvision = option.recipe.find(
(p) => p.writer === "workflows"
)!;
workflowsProvision.config = {
...workflowsProvision.config,
env: { VIBE_WORKFLOWS_DOMAIN: "skilled" }
};

const result = await resolve(userConfig, catalog, registry);

const workflowsServer = result.mcp_servers.find(
(s) => s.ref === "workflows"
);
expect(workflowsServer).toBeDefined();
expect(workflowsServer!.env).toEqual({
VIBE_WORKFLOWS_DOMAIN: "skilled"
});
});
});

describe("unknown facet in choices", () => {
Expand Down
62 changes: 62 additions & 0 deletions packages/core/src/writers/mcp-server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, it, expect } from "vitest";
import { mcpServerWriter } from "./mcp-server.js";
import type { ResolutionContext } from "../types.js";

describe("mcpServerWriter", () => {
const context: ResolutionContext = { resolved: {} };

it("has id 'mcp-server'", () => {
expect(mcpServerWriter.id).toBe("mcp-server");
});

it("returns mcp_servers with correct ref, command, args, and env", async () => {
const result = await mcpServerWriter.write(
{
ref: "my-server",
command: "npx",
args: ["my-mcp-package"],
env: { KEY: "value" }
},
context
);
expect(result).toEqual({
mcp_servers: [
{
ref: "my-server",
command: "npx",
args: ["my-mcp-package"],
env: { KEY: "value" }
}
]
});
});

it("defaults env to an empty object when not specified", async () => {
const result = await mcpServerWriter.write(
{ ref: "my-server", command: "npx", args: ["my-mcp-package"] },
context
);
expect(result.mcp_servers![0].env).toEqual({});
});

it("includes allowedTools when specified", async () => {
const result = await mcpServerWriter.write(
{
ref: "my-server",
command: "npx",
args: ["my-mcp-package"],
allowedTools: ["tool_a", "tool_b"]
},
context
);
expect(result.mcp_servers![0].allowedTools).toEqual(["tool_a", "tool_b"]);
});

it("omits allowedTools from entry when not specified", async () => {
const result = await mcpServerWriter.write(
{ ref: "my-server", command: "npx", args: ["my-mcp-package"] },
context
);
expect(result.mcp_servers![0]).not.toHaveProperty("allowedTools");
});
});
25 changes: 25 additions & 0 deletions packages/core/src/writers/mcp-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ProvisionWriterDef } from "../types.js";

export const mcpServerWriter: ProvisionWriterDef = {
id: "mcp-server",
async write(config) {
const { ref, command, args, env, allowedTools } = config as {
ref: string;
command: string;
args: string[];
env?: Record<string, string>;
allowedTools?: string[];
};
return {
mcp_servers: [
{
ref,
command,
args,
env: env ?? {},
...(allowedTools !== undefined ? { allowedTools } : {})
}
]
};
}
};
22 changes: 22 additions & 0 deletions packages/core/src/writers/workflows.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,28 @@ describe("workflowsWriter", () => {
expect(result.mcp_servers![0].env).toEqual({});
});

it("includes allowedTools in the entry when specified", async () => {
const result = await workflowsWriter.write(
{
package: "@codemcp/workflows-server",
allowedTools: ["whats_next", "conduct_review"]
},
context
);
expect(result.mcp_servers![0].allowedTools).toEqual([
"whats_next",
"conduct_review"
]);
});

it("omits allowedTools from entry when not specified", async () => {
const result = await workflowsWriter.write(
{ package: "@codemcp/workflows-server" },
context
);
expect(result.mcp_servers![0]).not.toHaveProperty("allowedTools");
});

it("only returns mcp_servers, not other LogicalConfig keys", async () => {
const result = await workflowsWriter.write(
{ package: "@codemcp/workflows-server" },
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/writers/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ export const workflowsWriter: ProvisionWriterDef = {
const {
package: pkg,
ref,
env
env,
allowedTools
} = config as {
package: string;
ref?: string;
env?: Record<string, string>;
allowedTools?: string[];
};
return {
mcp_servers: [
{
ref: ref ?? pkg,
command: "npx",
args: [pkg],
env: env ?? {}
env: env ?? {},
...(allowedTools !== undefined ? { allowedTools } : {})
}
]
};
Expand Down
8 changes: 2 additions & 6 deletions packages/harnesses/src/writers/copilot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,8 @@ describe("copilotWriter", () => {
expect(sensibleAgent).not.toContain(" - execute");
expect(sensibleAgent).not.toContain(" - todo");
expect(sensibleAgent).not.toContain(" - web");
expect(sensibleAgent).toContain(" - workflows/whats_next");
expect(sensibleAgent).toContain(" - workflows/proceed_to_phase");
expect(sensibleAgent).not.toContain(" - workflows/*");
expect(sensibleAgent).toContain(
' tools: ["whats_next","proceed_to_phase"]'
);
expect(sensibleAgent).toContain(" - workflows/*");
expect(sensibleAgent).toContain(' tools: ["*"]');

expect(maxAgent).toContain(" - read");
expect(maxAgent).toContain(" - edit");
Expand Down
11 changes: 2 additions & 9 deletions packages/harnesses/src/writers/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,7 @@ function getBuiltInTools(profile: AutonomyProfile | undefined): string[] {
}

function getForwardedMcpTools(servers: McpServerEntry[]): string[] {
return servers.flatMap((server) => {
const allowedTools = server.allowedTools ?? ["*"];
if (allowedTools.includes("*")) {
return [`${server.ref}/*`];
}

return allowedTools.map((tool) => `${server.ref}/${tool}`);
});
return servers.map((server) => `${server.ref}/*`);
}

function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] {
Expand All @@ -78,7 +71,7 @@ function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] {
lines.push(" type: stdio");
lines.push(` command: ${JSON.stringify(server.command)}`);
lines.push(` args: ${JSON.stringify(server.args)}`);
lines.push(` tools: ${JSON.stringify(server.allowedTools ?? ["*"])}`);
lines.push(` tools: ${JSON.stringify(["*"])}`);

if (Object.keys(server.env).length > 0) {
lines.push(" env:");
Expand Down
32 changes: 32 additions & 0 deletions packages/harnesses/src/writers/kiro.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,36 @@ describe("kiroWriter", () => {
expect(rigidMcp.mcpServers.workflows.autoApprove).toEqual(["*"]);
expect(maxMcp.mcpServers.workflows.autoApprove).toEqual(["*"]);
});

it("uses wildcard in tools but restricted names in allowedTools when allowedTools is set", async () => {
const config: LogicalConfig = {
mcp_servers: [
{
ref: "workflows",
command: "npx",
args: ["-y", "@codemcp/workflows"],
env: {},
allowedTools: ["whats_next", "conduct_review"]
}
],
instructions: [],
cli_actions: [],
knowledge_sources: [],
skills: [],
git_hooks: [],
setup_notes: []
};

await kiroWriter.install(config, dir);

const agent = JSON.parse(
await readFile(join(dir, ".kiro", "agents", "ade.json"), "utf-8")
);

expect(agent.tools).toContain("@workflows/*");
expect(agent.tools).not.toContain("@workflows/whats_next");
expect(agent.allowedTools).toContain("@workflows/whats_next");
expect(agent.allowedTools).toContain("@workflows/conduct_review");
expect(agent.allowedTools).not.toContain("@workflows/*");
});
});
27 changes: 22 additions & 5 deletions packages/harnesses/src/writers/kiro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export const kiroWriter: HarnessWriter = {
});

const tools = getKiroTools(getAutonomyProfile(config), config.mcp_servers);
const allowedTools = getKiroAllowedTools(
getAutonomyProfile(config),
config.mcp_servers
);
await writeJson(join(projectRoot, ".kiro", "agents", "ade.json"), {
name: "ade",
description:
Expand All @@ -36,7 +40,7 @@ export const kiroWriter: HarnessWriter = {
"ADE — Agentic Development Environment agent.",
mcpServers: getKiroAgentMcpServers(config.mcp_servers),
tools,
allowedTools: tools,
allowedTools,
useLegacyMcpJson: true
});

Expand All @@ -48,7 +52,7 @@ function getKiroTools(
profile: AutonomyProfile | undefined,
servers: McpServerEntry[]
): string[] {
const mcpTools = getKiroForwardedMcpTools(servers);
const mcpTools = servers.map((server) => `@${server.ref}/*`);

switch (profile) {
case "rigid":
Expand All @@ -62,15 +66,28 @@ function getKiroTools(
}
}

function getKiroForwardedMcpTools(servers: McpServerEntry[]): string[] {
return servers.flatMap((server) => {
function getKiroAllowedTools(
profile: AutonomyProfile | undefined,
servers: McpServerEntry[]
): string[] {
const mcpAllowedTools = servers.flatMap((server) => {
const allowedTools = server.allowedTools ?? ["*"];
if (allowedTools.includes("*")) {
return [`@${server.ref}/*`];
}

return allowedTools.map((tool) => `@${server.ref}/${tool}`);
});

switch (profile) {
case "rigid":
return ["read", "shell", "spec", ...mcpAllowedTools];
case "sensible-defaults":
return ["read", "write", "shell", "spec", ...mcpAllowedTools];
case "max-autonomy":
return ["read", "write", "shell(*)", "spec", ...mcpAllowedTools];
default:
return ["read", "write", "shell", "spec", ...mcpAllowedTools];
}
}

function getKiroAgentMcpServers(
Expand Down
Loading