@codemcp/ade 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemcp/ade",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "ADE CLI — Agentic Development Environment setup and configuration tool",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -11805,14 +11805,34 @@ var instructionWriter = {
11805
11805
  var workflowsWriter = {
11806
11806
  id: "workflows",
11807
11807
  async write(config) {
11808
- const { package: pkg, ref, env: env2 } = config;
11808
+ const { package: pkg, ref, env: env2, allowedTools } = config;
11809
11809
  return {
11810
11810
  mcp_servers: [
11811
11811
  {
11812
11812
  ref: ref ?? pkg,
11813
11813
  command: "npx",
11814
11814
  args: [pkg],
11815
- env: env2 ?? {}
11815
+ env: env2 ?? {},
11816
+ ...allowedTools !== void 0 ? { allowedTools } : {}
11817
+ }
11818
+ ]
11819
+ };
11820
+ }
11821
+ };
11822
+
11823
+ // ../core/dist/writers/mcp-server.js
11824
+ var mcpServerWriter = {
11825
+ id: "mcp-server",
11826
+ async write(config) {
11827
+ const { ref, command: command2, args: args2, env: env2, allowedTools } = config;
11828
+ return {
11829
+ mcp_servers: [
11830
+ {
11831
+ ref,
11832
+ command: command2,
11833
+ args: args2,
11834
+ env: env2 ?? {},
11835
+ ...allowedTools !== void 0 ? { allowedTools } : {}
11816
11836
  }
11817
11837
  ]
11818
11838
  };
@@ -11882,12 +11902,13 @@ function createDefaultRegistry() {
11882
11902
  const registry2 = createRegistry();
11883
11903
  registerProvisionWriter(registry2, instructionWriter);
11884
11904
  registerProvisionWriter(registry2, workflowsWriter);
11905
+ registerProvisionWriter(registry2, mcpServerWriter);
11885
11906
  registerProvisionWriter(registry2, skillsWriter);
11886
11907
  registerProvisionWriter(registry2, knowledgeWriter);
11887
11908
  registerProvisionWriter(registry2, gitHooksWriter);
11888
11909
  registerProvisionWriter(registry2, setupNoteWriter);
11889
11910
  registerProvisionWriter(registry2, permissionPolicyWriter);
11890
- for (const id of ["mcp-server", "installable"]) {
11911
+ for (const id of ["installable"]) {
11891
11912
  registerProvisionWriter(registry2, {
11892
11913
  id,
11893
11914
  write: async () => ({})
@@ -11912,7 +11933,16 @@ var processFacet = {
11912
11933
  writer: "workflows",
11913
11934
  config: {
11914
11935
  package: "@codemcp/workflows-server@latest",
11915
- ref: "workflows"
11936
+ ref: "workflows",
11937
+ env: {
11938
+ VIBE_WORKFLOW_DOMAINS: "skilled"
11939
+ },
11940
+ allowedTools: [
11941
+ "whats_next",
11942
+ "conduct_review",
11943
+ "list_workflows",
11944
+ "get_tool_info"
11945
+ ]
11916
11946
  }
11917
11947
  },
11918
11948
  {
@@ -22295,13 +22325,7 @@ function getBuiltInTools(profile) {
22295
22325
  }
22296
22326
  }
22297
22327
  function getForwardedMcpTools(servers) {
22298
- return servers.flatMap((server) => {
22299
- const allowedTools = server.allowedTools ?? ["*"];
22300
- if (allowedTools.includes("*")) {
22301
- return [`${server.ref}/*`];
22302
- }
22303
- return allowedTools.map((tool) => `${server.ref}/${tool}`);
22304
- });
22328
+ return servers.map((server) => `${server.ref}/*`);
22305
22329
  }
22306
22330
  function renderCopilotAgentMcpServers(servers) {
22307
22331
  if (servers.length === 0) {
@@ -22313,7 +22337,7 @@ function renderCopilotAgentMcpServers(servers) {
22313
22337
  lines.push(" type: stdio");
22314
22338
  lines.push(` command: ${JSON.stringify(server.command)}`);
22315
22339
  lines.push(` args: ${JSON.stringify(server.args)}`);
22316
- lines.push(` tools: ${JSON.stringify(server.allowedTools ?? ["*"])}`);
22340
+ lines.push(` tools: ${JSON.stringify(["*"])}`);
22317
22341
  if (Object.keys(server.env).length > 0) {
22318
22342
  lines.push(" env:");
22319
22343
  for (const [key, value] of Object.entries(server.env)) {
@@ -22460,20 +22484,21 @@ var kiroWriter = {
22460
22484
  })
22461
22485
  });
22462
22486
  const tools = getKiroTools(getAutonomyProfile(config), config.mcp_servers);
22487
+ const allowedTools = getKiroAllowedTools(getAutonomyProfile(config), config.mcp_servers);
22463
22488
  await writeJson(join17(projectRoot, ".kiro", "agents", "ade.json"), {
22464
22489
  name: "ade",
22465
22490
  description: "ADE \u2014 Agentic Development Environment agent with project conventions and tools.",
22466
22491
  prompt: config.instructions.join("\n\n") || "ADE \u2014 Agentic Development Environment agent.",
22467
22492
  mcpServers: getKiroAgentMcpServers(config.mcp_servers),
22468
22493
  tools,
22469
- allowedTools: tools,
22494
+ allowedTools,
22470
22495
  useLegacyMcpJson: true
22471
22496
  });
22472
22497
  await writeGitHooks(config.git_hooks, projectRoot);
22473
22498
  }
22474
22499
  };
22475
22500
  function getKiroTools(profile, servers) {
22476
- const mcpTools = getKiroForwardedMcpTools(servers);
22501
+ const mcpTools = servers.map((server) => `@${server.ref}/*`);
22477
22502
  switch (profile) {
22478
22503
  case "rigid":
22479
22504
  return ["read", "shell", "spec", ...mcpTools];
@@ -22485,14 +22510,24 @@ function getKiroTools(profile, servers) {
22485
22510
  return ["read", "write", "shell", "spec", ...mcpTools];
22486
22511
  }
22487
22512
  }
22488
- function getKiroForwardedMcpTools(servers) {
22489
- return servers.flatMap((server) => {
22513
+ function getKiroAllowedTools(profile, servers) {
22514
+ const mcpAllowedTools = servers.flatMap((server) => {
22490
22515
  const allowedTools = server.allowedTools ?? ["*"];
22491
22516
  if (allowedTools.includes("*")) {
22492
22517
  return [`@${server.ref}/*`];
22493
22518
  }
22494
22519
  return allowedTools.map((tool) => `@${server.ref}/${tool}`);
22495
22520
  });
22521
+ switch (profile) {
22522
+ case "rigid":
22523
+ return ["read", "shell", "spec", ...mcpAllowedTools];
22524
+ case "sensible-defaults":
22525
+ return ["read", "write", "shell", "spec", ...mcpAllowedTools];
22526
+ case "max-autonomy":
22527
+ return ["read", "write", "shell(*)", "spec", ...mcpAllowedTools];
22528
+ default:
22529
+ return ["read", "write", "shell", "spec", ...mcpAllowedTools];
22530
+ }
22496
22531
  }
22497
22532
  function getKiroAgentMcpServers(servers) {
22498
22533
  return Object.fromEntries(servers.map((server) => [
@@ -22630,6 +22665,19 @@ var MAX_AUTONOMY_RULES = {
22630
22665
  codesearch: "ask",
22631
22666
  doom_loop: "deny"
22632
22667
  };
22668
+ function getMcpPermissions(servers) {
22669
+ const entries = servers.flatMap((server) => {
22670
+ const allowedTools = server.allowedTools ?? ["*"];
22671
+ if (allowedTools.includes("*")) {
22672
+ return [[`${server.ref}*`, "allow"]];
22673
+ }
22674
+ return [
22675
+ [`${server.ref}*`, "ask"],
22676
+ ...allowedTools.map((tool) => [`${server.ref}_${tool}`, "allow"])
22677
+ ];
22678
+ });
22679
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
22680
+ }
22633
22681
  function getPermissionRules(profile) {
22634
22682
  switch (profile) {
22635
22683
  case "rigid":
@@ -22658,9 +22706,11 @@ var opencodeWriter = {
22658
22706
  defaults: { $schema: "https://opencode.ai/config.json" }
22659
22707
  });
22660
22708
  const permission = getPermissionRules(getAutonomyProfile(config));
22709
+ const mcpPermissions = getMcpPermissions(config.mcp_servers);
22710
+ const mergedPermission = permission || mcpPermissions ? { ...mcpPermissions ?? {}, ...permission ?? {} } : void 0;
22661
22711
  await writeAgentMd(config, {
22662
22712
  path: join18(projectRoot, ".opencode", "agents", "ade.md"),
22663
- extraFrontmatter: permission ? renderYamlMapping("permission", permission) : void 0,
22713
+ extraFrontmatter: mergedPermission ? renderYamlMapping("permission", mergedPermission) : void 0,
22664
22714
  fallbackBody: "ADE \u2014 Agentic Development Environment agent with project conventions and tools."
22665
22715
  });
22666
22716
  await writeGitHooks(config.git_hooks, projectRoot);
@@ -39,5 +39,5 @@
39
39
  "typescript": "catalog:",
40
40
  "vitest": "catalog:"
41
41
  },
42
- "version": "0.4.0"
42
+ "version": "0.5.0"
43
43
  }
@@ -38,5 +38,5 @@
38
38
  "typescript": "catalog:",
39
39
  "vitest": "catalog:"
40
40
  },
41
- "version": "0.4.0"
41
+ "version": "0.5.0"
42
42
  }
@@ -16,7 +16,16 @@ export const processFacet: Facet = {
16
16
  writer: "workflows",
17
17
  config: {
18
18
  package: "@codemcp/workflows-server@latest",
19
- ref: "workflows"
19
+ ref: "workflows",
20
+ env: {
21
+ VIBE_WORKFLOW_DOMAINS: "skilled"
22
+ },
23
+ allowedTools: [
24
+ "whats_next",
25
+ "conduct_review",
26
+ "list_workflows",
27
+ "get_tool_info"
28
+ ]
20
29
  }
21
30
  },
22
31
  {
@@ -5,6 +5,7 @@ import type {
5
5
  } from "./types.js";
6
6
  import { instructionWriter } from "./writers/instruction.js";
7
7
  import { workflowsWriter } from "./writers/workflows.js";
8
+ import { mcpServerWriter } from "./writers/mcp-server.js";
8
9
  import { skillsWriter } from "./writers/skills.js";
9
10
  import { knowledgeWriter } from "./writers/knowledge.js";
10
11
  import { gitHooksWriter } from "./writers/git-hooks.js";
@@ -51,15 +52,15 @@ export function createDefaultRegistry(): WriterRegistry {
51
52
 
52
53
  registerProvisionWriter(registry, instructionWriter);
53
54
  registerProvisionWriter(registry, workflowsWriter);
55
+ registerProvisionWriter(registry, mcpServerWriter);
54
56
  registerProvisionWriter(registry, skillsWriter);
55
-
56
57
  registerProvisionWriter(registry, knowledgeWriter);
57
58
  registerProvisionWriter(registry, gitHooksWriter);
58
59
  registerProvisionWriter(registry, setupNoteWriter);
59
60
  registerProvisionWriter(registry, permissionPolicyWriter);
60
61
 
61
62
  // Stub writers for types not yet implemented
62
- for (const id of ["mcp-server", "installable"]) {
63
+ for (const id of ["installable"]) {
63
64
  registerProvisionWriter(registry, {
64
65
  id,
65
66
  write: async () => ({})
@@ -120,6 +120,35 @@ describe("resolve", () => {
120
120
  expect(result.mcp_servers.length).toBeGreaterThanOrEqual(1);
121
121
  expect(result.instructions).toContain("Extra instruction");
122
122
  });
123
+
124
+ it("env set in the catalog option config is forwarded to the resolved mcp_server entry", async () => {
125
+ const userConfig: UserConfig = {
126
+ choices: { process: "codemcp-workflows" }
127
+ };
128
+
129
+ // Patch the catalog option's provision config to include env
130
+ const processFacet = catalog.facets.find((f) => f.id === "process")!;
131
+ const option = processFacet.options.find(
132
+ (o) => o.id === "codemcp-workflows"
133
+ )!;
134
+ const workflowsProvision = option.recipe.find(
135
+ (p) => p.writer === "workflows"
136
+ )!;
137
+ workflowsProvision.config = {
138
+ ...workflowsProvision.config,
139
+ env: { VIBE_WORKFLOWS_DOMAIN: "skilled" }
140
+ };
141
+
142
+ const result = await resolve(userConfig, catalog, registry);
143
+
144
+ const workflowsServer = result.mcp_servers.find(
145
+ (s) => s.ref === "workflows"
146
+ );
147
+ expect(workflowsServer).toBeDefined();
148
+ expect(workflowsServer!.env).toEqual({
149
+ VIBE_WORKFLOWS_DOMAIN: "skilled"
150
+ });
151
+ });
123
152
  });
124
153
 
125
154
  describe("unknown facet in choices", () => {
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mcpServerWriter } from "./mcp-server.js";
3
+ import type { ResolutionContext } from "../types.js";
4
+
5
+ describe("mcpServerWriter", () => {
6
+ const context: ResolutionContext = { resolved: {} };
7
+
8
+ it("has id 'mcp-server'", () => {
9
+ expect(mcpServerWriter.id).toBe("mcp-server");
10
+ });
11
+
12
+ it("returns mcp_servers with correct ref, command, args, and env", async () => {
13
+ const result = await mcpServerWriter.write(
14
+ {
15
+ ref: "my-server",
16
+ command: "npx",
17
+ args: ["my-mcp-package"],
18
+ env: { KEY: "value" }
19
+ },
20
+ context
21
+ );
22
+ expect(result).toEqual({
23
+ mcp_servers: [
24
+ {
25
+ ref: "my-server",
26
+ command: "npx",
27
+ args: ["my-mcp-package"],
28
+ env: { KEY: "value" }
29
+ }
30
+ ]
31
+ });
32
+ });
33
+
34
+ it("defaults env to an empty object when not specified", async () => {
35
+ const result = await mcpServerWriter.write(
36
+ { ref: "my-server", command: "npx", args: ["my-mcp-package"] },
37
+ context
38
+ );
39
+ expect(result.mcp_servers![0].env).toEqual({});
40
+ });
41
+
42
+ it("includes allowedTools when specified", async () => {
43
+ const result = await mcpServerWriter.write(
44
+ {
45
+ ref: "my-server",
46
+ command: "npx",
47
+ args: ["my-mcp-package"],
48
+ allowedTools: ["tool_a", "tool_b"]
49
+ },
50
+ context
51
+ );
52
+ expect(result.mcp_servers![0].allowedTools).toEqual(["tool_a", "tool_b"]);
53
+ });
54
+
55
+ it("omits allowedTools from entry when not specified", async () => {
56
+ const result = await mcpServerWriter.write(
57
+ { ref: "my-server", command: "npx", args: ["my-mcp-package"] },
58
+ context
59
+ );
60
+ expect(result.mcp_servers![0]).not.toHaveProperty("allowedTools");
61
+ });
62
+ });
@@ -0,0 +1,25 @@
1
+ import type { ProvisionWriterDef } from "../types.js";
2
+
3
+ export const mcpServerWriter: ProvisionWriterDef = {
4
+ id: "mcp-server",
5
+ async write(config) {
6
+ const { ref, command, args, env, allowedTools } = config as {
7
+ ref: string;
8
+ command: string;
9
+ args: string[];
10
+ env?: Record<string, string>;
11
+ allowedTools?: string[];
12
+ };
13
+ return {
14
+ mcp_servers: [
15
+ {
16
+ ref,
17
+ command,
18
+ args,
19
+ env: env ?? {},
20
+ ...(allowedTools !== undefined ? { allowedTools } : {})
21
+ }
22
+ ]
23
+ };
24
+ }
25
+ };
@@ -59,6 +59,28 @@ describe("workflowsWriter", () => {
59
59
  expect(result.mcp_servers![0].env).toEqual({});
60
60
  });
61
61
 
62
+ it("includes allowedTools in the entry when specified", async () => {
63
+ const result = await workflowsWriter.write(
64
+ {
65
+ package: "@codemcp/workflows-server",
66
+ allowedTools: ["whats_next", "conduct_review"]
67
+ },
68
+ context
69
+ );
70
+ expect(result.mcp_servers![0].allowedTools).toEqual([
71
+ "whats_next",
72
+ "conduct_review"
73
+ ]);
74
+ });
75
+
76
+ it("omits allowedTools from entry when not specified", async () => {
77
+ const result = await workflowsWriter.write(
78
+ { package: "@codemcp/workflows-server" },
79
+ context
80
+ );
81
+ expect(result.mcp_servers![0]).not.toHaveProperty("allowedTools");
82
+ });
83
+
62
84
  it("only returns mcp_servers, not other LogicalConfig keys", async () => {
63
85
  const result = await workflowsWriter.write(
64
86
  { package: "@codemcp/workflows-server" },
@@ -6,11 +6,13 @@ export const workflowsWriter: ProvisionWriterDef = {
6
6
  const {
7
7
  package: pkg,
8
8
  ref,
9
- env
9
+ env,
10
+ allowedTools
10
11
  } = config as {
11
12
  package: string;
12
13
  ref?: string;
13
14
  env?: Record<string, string>;
15
+ allowedTools?: string[];
14
16
  };
15
17
  return {
16
18
  mcp_servers: [
@@ -18,7 +20,8 @@ export const workflowsWriter: ProvisionWriterDef = {
18
20
  ref: ref ?? pkg,
19
21
  command: "npx",
20
22
  args: [pkg],
21
- env: env ?? {}
23
+ env: env ?? {},
24
+ ...(allowedTools !== undefined ? { allowedTools } : {})
22
25
  }
23
26
  ]
24
27
  };
@@ -40,5 +40,5 @@
40
40
  "typescript": "catalog:",
41
41
  "vitest": "catalog:"
42
42
  },
43
- "version": "0.4.0"
43
+ "version": "0.5.0"
44
44
  }
@@ -198,12 +198,8 @@ describe("copilotWriter", () => {
198
198
  expect(sensibleAgent).not.toContain(" - execute");
199
199
  expect(sensibleAgent).not.toContain(" - todo");
200
200
  expect(sensibleAgent).not.toContain(" - web");
201
- expect(sensibleAgent).toContain(" - workflows/whats_next");
202
- expect(sensibleAgent).toContain(" - workflows/proceed_to_phase");
203
- expect(sensibleAgent).not.toContain(" - workflows/*");
204
- expect(sensibleAgent).toContain(
205
- ' tools: ["whats_next","proceed_to_phase"]'
206
- );
201
+ expect(sensibleAgent).toContain(" - workflows/*");
202
+ expect(sensibleAgent).toContain(' tools: ["*"]');
207
203
 
208
204
  expect(maxAgent).toContain(" - read");
209
205
  expect(maxAgent).toContain(" - edit");
@@ -56,14 +56,7 @@ function getBuiltInTools(profile: AutonomyProfile | undefined): string[] {
56
56
  }
57
57
 
58
58
  function getForwardedMcpTools(servers: McpServerEntry[]): string[] {
59
- return servers.flatMap((server) => {
60
- const allowedTools = server.allowedTools ?? ["*"];
61
- if (allowedTools.includes("*")) {
62
- return [`${server.ref}/*`];
63
- }
64
-
65
- return allowedTools.map((tool) => `${server.ref}/${tool}`);
66
- });
59
+ return servers.map((server) => `${server.ref}/*`);
67
60
  }
68
61
 
69
62
  function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] {
@@ -78,7 +71,7 @@ function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] {
78
71
  lines.push(" type: stdio");
79
72
  lines.push(` command: ${JSON.stringify(server.command)}`);
80
73
  lines.push(` args: ${JSON.stringify(server.args)}`);
81
- lines.push(` tools: ${JSON.stringify(server.allowedTools ?? ["*"])}`);
74
+ lines.push(` tools: ${JSON.stringify(["*"])}`);
82
75
 
83
76
  if (Object.keys(server.env).length > 0) {
84
77
  lines.push(" env:");
@@ -186,4 +186,36 @@ describe("kiroWriter", () => {
186
186
  expect(rigidMcp.mcpServers.workflows.autoApprove).toEqual(["*"]);
187
187
  expect(maxMcp.mcpServers.workflows.autoApprove).toEqual(["*"]);
188
188
  });
189
+
190
+ it("uses wildcard in tools but restricted names in allowedTools when allowedTools is set", async () => {
191
+ const config: LogicalConfig = {
192
+ mcp_servers: [
193
+ {
194
+ ref: "workflows",
195
+ command: "npx",
196
+ args: ["-y", "@codemcp/workflows"],
197
+ env: {},
198
+ allowedTools: ["whats_next", "conduct_review"]
199
+ }
200
+ ],
201
+ instructions: [],
202
+ cli_actions: [],
203
+ knowledge_sources: [],
204
+ skills: [],
205
+ git_hooks: [],
206
+ setup_notes: []
207
+ };
208
+
209
+ await kiroWriter.install(config, dir);
210
+
211
+ const agent = JSON.parse(
212
+ await readFile(join(dir, ".kiro", "agents", "ade.json"), "utf-8")
213
+ );
214
+
215
+ expect(agent.tools).toContain("@workflows/*");
216
+ expect(agent.tools).not.toContain("@workflows/whats_next");
217
+ expect(agent.allowedTools).toContain("@workflows/whats_next");
218
+ expect(agent.allowedTools).toContain("@workflows/conduct_review");
219
+ expect(agent.allowedTools).not.toContain("@workflows/*");
220
+ });
189
221
  });
@@ -27,6 +27,10 @@ export const kiroWriter: HarnessWriter = {
27
27
  });
28
28
 
29
29
  const tools = getKiroTools(getAutonomyProfile(config), config.mcp_servers);
30
+ const allowedTools = getKiroAllowedTools(
31
+ getAutonomyProfile(config),
32
+ config.mcp_servers
33
+ );
30
34
  await writeJson(join(projectRoot, ".kiro", "agents", "ade.json"), {
31
35
  name: "ade",
32
36
  description:
@@ -36,7 +40,7 @@ export const kiroWriter: HarnessWriter = {
36
40
  "ADE — Agentic Development Environment agent.",
37
41
  mcpServers: getKiroAgentMcpServers(config.mcp_servers),
38
42
  tools,
39
- allowedTools: tools,
43
+ allowedTools,
40
44
  useLegacyMcpJson: true
41
45
  });
42
46
 
@@ -48,7 +52,7 @@ function getKiroTools(
48
52
  profile: AutonomyProfile | undefined,
49
53
  servers: McpServerEntry[]
50
54
  ): string[] {
51
- const mcpTools = getKiroForwardedMcpTools(servers);
55
+ const mcpTools = servers.map((server) => `@${server.ref}/*`);
52
56
 
53
57
  switch (profile) {
54
58
  case "rigid":
@@ -62,15 +66,28 @@ function getKiroTools(
62
66
  }
63
67
  }
64
68
 
65
- function getKiroForwardedMcpTools(servers: McpServerEntry[]): string[] {
66
- return servers.flatMap((server) => {
69
+ function getKiroAllowedTools(
70
+ profile: AutonomyProfile | undefined,
71
+ servers: McpServerEntry[]
72
+ ): string[] {
73
+ const mcpAllowedTools = servers.flatMap((server) => {
67
74
  const allowedTools = server.allowedTools ?? ["*"];
68
75
  if (allowedTools.includes("*")) {
69
76
  return [`@${server.ref}/*`];
70
77
  }
71
-
72
78
  return allowedTools.map((tool) => `@${server.ref}/${tool}`);
73
79
  });
80
+
81
+ switch (profile) {
82
+ case "rigid":
83
+ return ["read", "shell", "spec", ...mcpAllowedTools];
84
+ case "sensible-defaults":
85
+ return ["read", "write", "shell", "spec", ...mcpAllowedTools];
86
+ case "max-autonomy":
87
+ return ["read", "write", "shell(*)", "spec", ...mcpAllowedTools];
88
+ default:
89
+ return ["read", "write", "shell", "spec", ...mcpAllowedTools];
90
+ }
74
91
  }
75
92
 
76
93
  function getKiroAgentMcpServers(
@@ -148,6 +148,72 @@ describe("opencodeWriter", () => {
148
148
  expect(rigidAgent).not.toContain("tools:");
149
149
  });
150
150
 
151
+ it("writes allowed MCP tools into the permission block of the agent frontmatter", async () => {
152
+ const projectRoot = join(dir, "mcp-tools");
153
+ const config: LogicalConfig = {
154
+ mcp_servers: [
155
+ {
156
+ ref: "workflows",
157
+ command: "npx",
158
+ args: ["@codemcp/workflows-server@latest"],
159
+ env: {},
160
+ allowedTools: ["whats_next", "conduct_review"]
161
+ }
162
+ ],
163
+ instructions: ["Follow project rules."],
164
+ cli_actions: [],
165
+ knowledge_sources: [],
166
+ skills: [],
167
+ git_hooks: [],
168
+ setup_notes: []
169
+ };
170
+
171
+ await opencodeWriter.install(config, projectRoot);
172
+
173
+ const agent = await readFile(
174
+ join(projectRoot, ".opencode", "agents", "ade.md"),
175
+ "utf-8"
176
+ );
177
+ const frontmatter = parseFrontmatter(agent);
178
+ const permission = frontmatter.permission as Record<string, string>;
179
+
180
+ expect(permission["workflows*"]).toBe("ask");
181
+ expect(permission["workflows_whats_next"]).toBe("allow");
182
+ expect(permission["workflows_conduct_review"]).toBe("allow");
183
+ expect(agent).not.toContain("tools:");
184
+ });
185
+
186
+ it("writes wildcard MCP permission when allowedTools is not restricted", async () => {
187
+ const projectRoot = join(dir, "mcp-wildcard");
188
+ const config: LogicalConfig = {
189
+ mcp_servers: [
190
+ {
191
+ ref: "workflows",
192
+ command: "npx",
193
+ args: ["@codemcp/workflows-server@latest"],
194
+ env: {}
195
+ }
196
+ ],
197
+ instructions: ["Follow project rules."],
198
+ cli_actions: [],
199
+ knowledge_sources: [],
200
+ skills: [],
201
+ git_hooks: [],
202
+ setup_notes: []
203
+ };
204
+
205
+ await opencodeWriter.install(config, projectRoot);
206
+
207
+ const agent = await readFile(
208
+ join(projectRoot, ".opencode", "agents", "ade.md"),
209
+ "utf-8"
210
+ );
211
+ const frontmatter = parseFrontmatter(agent);
212
+ const permission = frontmatter.permission as Record<string, string>;
213
+
214
+ expect(permission["workflows*"]).toBe("allow");
215
+ });
216
+
151
217
  it("keeps MCP servers in project config and writes documented environment fields", async () => {
152
218
  const projectRoot = join(dir, "mcp");
153
219
  const config = {
@@ -1,5 +1,9 @@
1
1
  import { join } from "node:path";
2
- import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core";
2
+ import type {
3
+ AutonomyProfile,
4
+ LogicalConfig,
5
+ McpServerEntry
6
+ } from "@codemcp/ade-core";
3
7
  import type { HarnessWriter } from "../types.js";
4
8
  import {
5
9
  writeAgentMd,
@@ -138,6 +142,24 @@ const MAX_AUTONOMY_RULES: Record<string, PermissionRule> = {
138
142
  doom_loop: "deny"
139
143
  };
140
144
 
145
+ function getMcpPermissions(
146
+ servers: McpServerEntry[]
147
+ ): Record<string, PermissionRule> | undefined {
148
+ const entries: [string, PermissionRule][] = servers.flatMap((server) => {
149
+ const allowedTools = server.allowedTools ?? ["*"];
150
+ if (allowedTools.includes("*")) {
151
+ return [[`${server.ref}*`, "allow"]] as [string, PermissionRule][];
152
+ }
153
+ return [
154
+ [`${server.ref}*`, "ask"] as [string, PermissionRule],
155
+ ...allowedTools.map(
156
+ (tool) => [`${server.ref}_${tool}`, "allow"] as [string, PermissionRule]
157
+ )
158
+ ];
159
+ });
160
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
161
+ }
162
+
141
163
  function getPermissionRules(
142
164
  profile: AutonomyProfile | undefined
143
165
  ): Record<string, PermissionRule> | undefined {
@@ -170,11 +192,16 @@ export const opencodeWriter: HarnessWriter = {
170
192
  });
171
193
 
172
194
  const permission = getPermissionRules(getAutonomyProfile(config));
195
+ const mcpPermissions = getMcpPermissions(config.mcp_servers);
196
+ const mergedPermission =
197
+ permission || mcpPermissions
198
+ ? { ...(mcpPermissions ?? {}), ...(permission ?? {}) }
199
+ : undefined;
173
200
 
174
201
  await writeAgentMd(config, {
175
202
  path: join(projectRoot, ".opencode", "agents", "ade.md"),
176
- extraFrontmatter: permission
177
- ? renderYamlMapping("permission", permission)
203
+ extraFrontmatter: mergedPermission
204
+ ? renderYamlMapping("permission", mergedPermission)
178
205
  : undefined,
179
206
  fallbackBody:
180
207
  "ADE — Agentic Development Environment agent with project conventions and tools."