@codemcp/ade-harnesses 0.0.2 → 0.1.1

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.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-format.log +1 -1
  3. package/.turbo/turbo-lint.log +1 -1
  4. package/.turbo/turbo-test.log +15 -12
  5. package/.turbo/turbo-typecheck.log +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/permission-policy.d.ts +7 -0
  9. package/dist/permission-policy.js +152 -0
  10. package/dist/util.d.ts +1 -1
  11. package/dist/util.js +16 -2
  12. package/dist/writers/claude-code.js +51 -20
  13. package/dist/writers/cline.js +2 -2
  14. package/dist/writers/copilot.js +61 -8
  15. package/dist/writers/cursor.js +48 -2
  16. package/dist/writers/kiro.js +54 -38
  17. package/dist/writers/opencode.js +26 -23
  18. package/dist/writers/roo-code.js +38 -2
  19. package/dist/writers/universal.js +41 -3
  20. package/dist/writers/windsurf.js +43 -1
  21. package/package.json +2 -2
  22. package/src/index.ts +1 -0
  23. package/src/permission-policy.ts +173 -0
  24. package/src/util.ts +20 -7
  25. package/src/writers/claude-code.spec.ts +163 -5
  26. package/src/writers/claude-code.ts +63 -20
  27. package/src/writers/cline.spec.ts +146 -3
  28. package/src/writers/cline.ts +2 -2
  29. package/src/writers/copilot.spec.ts +157 -1
  30. package/src/writers/copilot.ts +76 -9
  31. package/src/writers/cursor.spec.ts +104 -1
  32. package/src/writers/cursor.ts +65 -3
  33. package/src/writers/kiro.spec.ts +228 -0
  34. package/src/writers/kiro.ts +77 -40
  35. package/src/writers/opencode.spec.ts +258 -0
  36. package/src/writers/opencode.ts +40 -27
  37. package/src/writers/roo-code.spec.ts +129 -1
  38. package/src/writers/roo-code.ts +49 -2
  39. package/src/writers/universal.spec.ts +134 -0
  40. package/src/writers/universal.ts +57 -4
  41. package/src/writers/windsurf.spec.ts +111 -3
  42. package/src/writers/windsurf.ts +64 -2
  43. package/tsconfig.tsbuildinfo +1 -1
@@ -6,9 +6,9 @@ import {
6
6
  writeJson,
7
7
  writeMcpServers,
8
8
  writeAgentMd,
9
- writeInlineSkills,
10
9
  writeGitHooks
11
10
  } from "../util.js";
11
+ import { allowsCapability, keepsWebOnAsk } from "../permission-policy.js";
12
12
 
13
13
  export const claudeCodeWriter: HarnessWriter = {
14
14
  id: "claude-code",
@@ -26,7 +26,6 @@ export const claudeCodeWriter: HarnessWriter = {
26
26
  });
27
27
 
28
28
  await writeClaudeSettings(config, projectRoot);
29
- await writeInlineSkills(config, projectRoot);
30
29
  await writeGitHooks(config.git_hooks, projectRoot);
31
30
  }
32
31
  };
@@ -35,30 +34,74 @@ async function writeClaudeSettings(
35
34
  config: LogicalConfig,
36
35
  projectRoot: string
37
36
  ): Promise<void> {
38
- const servers = config.mcp_servers;
39
- if (servers.length === 0) return;
40
-
41
37
  const settingsPath = join(projectRoot, ".claude", "settings.json");
42
38
  const existing = await readJsonOrEmpty(settingsPath);
39
+ const existingPerms = (existing.permissions as Record<string, unknown>) ?? {};
40
+ const existingAllow = asStringArray(existingPerms.allow);
41
+ const existingAsk = asStringArray(existingPerms.ask);
43
42
 
44
- const allowRules: string[] = [];
45
- for (const server of servers) {
46
- const allowed = server.allowedTools ?? ["*"];
47
- if (allowed.includes("*")) {
48
- allowRules.push(`MCP(${server.ref}:*)`);
49
- } else {
50
- for (const tool of allowed) {
51
- allowRules.push(`MCP(${server.ref}:${tool})`);
52
- }
53
- }
54
- }
43
+ const autonomyRules = getClaudeAutonomyRules(config);
44
+ const mcpRules = getClaudeMcpAllowRules(config);
45
+ const allowRules = [
46
+ ...new Set([...existingAllow, ...autonomyRules.allow, ...mcpRules])
47
+ ];
48
+ const askRules = [...new Set([...existingAsk, ...autonomyRules.ask])];
55
49
 
56
- const existingPerms = (existing.permissions as Record<string, unknown>) ?? {};
57
- const existingAllow = (existingPerms.allow as string[]) ?? [];
58
- const mergedAllow = [...new Set([...existingAllow, ...allowRules])];
50
+ if (
51
+ allowRules.length === 0 &&
52
+ askRules.length === 0 &&
53
+ config.mcp_servers.length === 0
54
+ ) {
55
+ return;
56
+ }
59
57
 
60
58
  await writeJson(settingsPath, {
61
59
  ...existing,
62
- permissions: { ...existingPerms, allow: mergedAllow }
60
+ permissions: {
61
+ ...existingPerms,
62
+ ...(allowRules.length > 0 ? { allow: allowRules } : {}),
63
+ ...(askRules.length > 0 ? { ask: askRules } : {})
64
+ }
63
65
  });
64
66
  }
67
+
68
+ function asStringArray(value: unknown): string[] {
69
+ return Array.isArray(value)
70
+ ? value.filter((entry): entry is string => typeof entry === "string")
71
+ : [];
72
+ }
73
+
74
+ function getClaudeMcpAllowRules(config: LogicalConfig): string[] {
75
+ const allowRules: string[] = [];
76
+
77
+ for (const server of config.mcp_servers) {
78
+ const allowedTools = server.allowedTools;
79
+ if (!allowedTools || allowedTools.includes("*")) {
80
+ continue;
81
+ }
82
+
83
+ for (const tool of allowedTools) {
84
+ allowRules.push(`mcp__${server.ref}__${tool}`);
85
+ }
86
+ }
87
+
88
+ return allowRules;
89
+ }
90
+
91
+ function getClaudeAutonomyRules(config: LogicalConfig): {
92
+ allow: string[];
93
+ ask: string[];
94
+ } {
95
+ const ask = keepsWebOnAsk(config) ? ["WebFetch", "WebSearch"] : [];
96
+
97
+ return {
98
+ allow: [
99
+ ...(allowsCapability(config, "read") ? ["Read"] : []),
100
+ ...(allowsCapability(config, "edit_write") ? ["Edit"] : []),
101
+ ...(allowsCapability(config, "search_list") ? ["Glob", "Grep"] : []),
102
+ ...(allowsCapability(config, "bash_unsafe") ? ["Bash"] : []),
103
+ ...(allowsCapability(config, "task_agent") ? ["TodoWrite"] : [])
104
+ ],
105
+ ask
106
+ };
107
+ }
@@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { mkdtemp, rm, readFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import type { LogicalConfig } from "@codemcp/ade-core";
5
+ import type {
6
+ AutonomyProfile,
7
+ LogicalConfig,
8
+ PermissionPolicy
9
+ } from "@codemcp/ade-core";
6
10
  import { clineWriter } from "./cline.js";
7
11
 
12
+ function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy {
13
+ switch (profile) {
14
+ case "rigid":
15
+ return {
16
+ profile,
17
+ capabilities: {
18
+ read: "ask",
19
+ edit_write: "ask",
20
+ search_list: "ask",
21
+ bash_safe: "ask",
22
+ bash_unsafe: "ask",
23
+ web: "ask",
24
+ task_agent: "ask"
25
+ }
26
+ };
27
+ case "sensible-defaults":
28
+ return {
29
+ profile,
30
+ capabilities: {
31
+ read: "allow",
32
+ edit_write: "allow",
33
+ search_list: "allow",
34
+ bash_safe: "allow",
35
+ bash_unsafe: "ask",
36
+ web: "ask",
37
+ task_agent: "allow"
38
+ }
39
+ };
40
+ case "max-autonomy":
41
+ return {
42
+ profile,
43
+ capabilities: {
44
+ read: "allow",
45
+ edit_write: "allow",
46
+ search_list: "allow",
47
+ bash_safe: "allow",
48
+ bash_unsafe: "allow",
49
+ web: "ask",
50
+ task_agent: "allow"
51
+ }
52
+ };
53
+ }
54
+ }
55
+
8
56
  describe("clineWriter", () => {
9
57
  let dir: string;
10
58
 
@@ -21,7 +69,7 @@ describe("clineWriter", () => {
21
69
  expect(clineWriter.label).toBe("Cline");
22
70
  });
23
71
 
24
- it("writes .cline/mcp.json with MCP servers", async () => {
72
+ it("writes cline_mcp_settings.json with MCP servers", async () => {
25
73
  const config: LogicalConfig = {
26
74
  mcp_servers: [
27
75
  {
@@ -41,7 +89,7 @@ describe("clineWriter", () => {
41
89
 
42
90
  await clineWriter.install(config, dir);
43
91
 
44
- const raw = await readFile(join(dir, ".cline", "mcp.json"), "utf-8");
92
+ const raw = await readFile(join(dir, "cline_mcp_settings.json"), "utf-8");
45
93
  const parsed = JSON.parse(raw);
46
94
  expect(parsed.mcpServers["workflows"]).toEqual({
47
95
  command: "npx",
@@ -50,6 +98,36 @@ describe("clineWriter", () => {
50
98
  });
51
99
  });
52
100
 
101
+ it("forwards explicit MCP approvals unchanged from provisioning", async () => {
102
+ const config: LogicalConfig = {
103
+ mcp_servers: [
104
+ {
105
+ ref: "workflows",
106
+ command: "npx",
107
+ args: ["-y", "@codemcp/workflows"],
108
+ env: {},
109
+ allowedTools: ["whats_next", "proceed_to_phase"]
110
+ }
111
+ ],
112
+ instructions: [],
113
+ cli_actions: [],
114
+ knowledge_sources: [],
115
+ skills: [],
116
+ git_hooks: [],
117
+ setup_notes: []
118
+ };
119
+
120
+ await clineWriter.install(config, dir);
121
+
122
+ const raw = await readFile(join(dir, "cline_mcp_settings.json"), "utf-8");
123
+ const parsed = JSON.parse(raw);
124
+ expect(parsed.mcpServers["workflows"]).toEqual({
125
+ command: "npx",
126
+ args: ["-y", "@codemcp/workflows"],
127
+ alwaysAllow: ["whats_next", "proceed_to_phase"]
128
+ });
129
+ });
130
+
53
131
  it("writes .clinerules with instructions", async () => {
54
132
  const config: LogicalConfig = {
55
133
  mcp_servers: [],
@@ -66,4 +144,69 @@ describe("clineWriter", () => {
66
144
  const content = await readFile(join(dir, ".clinerules"), "utf-8");
67
145
  expect(content).toContain("Follow TDD.");
68
146
  });
147
+
148
+ it("does not invent built-in auto-approval settings for autonomy profiles", async () => {
149
+ const rigidRoot = join(dir, "rigid");
150
+ const sensibleRoot = join(dir, "sensible");
151
+ const maxRoot = join(dir, "max");
152
+
153
+ const rigidConfig: LogicalConfig = {
154
+ mcp_servers: [
155
+ {
156
+ ref: "workflows",
157
+ command: "npx",
158
+ args: ["-y", "@codemcp/workflows"],
159
+ env: {}
160
+ }
161
+ ],
162
+ instructions: ["Use approvals for risky actions."],
163
+ cli_actions: [],
164
+ knowledge_sources: [],
165
+ skills: [],
166
+ git_hooks: [],
167
+ setup_notes: [],
168
+ permission_policy: autonomyPolicy("rigid")
169
+ };
170
+
171
+ const sensibleConfig: LogicalConfig = {
172
+ ...rigidConfig,
173
+ permission_policy: autonomyPolicy("sensible-defaults")
174
+ };
175
+
176
+ const maxConfig: LogicalConfig = {
177
+ ...rigidConfig,
178
+ permission_policy: autonomyPolicy("max-autonomy")
179
+ };
180
+
181
+ await clineWriter.install(rigidConfig, rigidRoot);
182
+ await clineWriter.install(sensibleConfig, sensibleRoot);
183
+ await clineWriter.install(maxConfig, maxRoot);
184
+
185
+ const rigidSettings = JSON.parse(
186
+ await readFile(join(rigidRoot, "cline_mcp_settings.json"), "utf-8")
187
+ );
188
+ const sensibleSettings = JSON.parse(
189
+ await readFile(join(sensibleRoot, "cline_mcp_settings.json"), "utf-8")
190
+ );
191
+ const maxSettings = JSON.parse(
192
+ await readFile(join(maxRoot, "cline_mcp_settings.json"), "utf-8")
193
+ );
194
+ const maxRules = await readFile(join(maxRoot, ".clinerules"), "utf-8");
195
+
196
+ expect(rigidSettings).toEqual(sensibleSettings);
197
+ expect(sensibleSettings).toEqual(maxSettings);
198
+ expect(maxSettings).toEqual({
199
+ mcpServers: {
200
+ workflows: {
201
+ command: "npx",
202
+ args: ["-y", "@codemcp/workflows"],
203
+ alwaysAllow: ["*"]
204
+ }
205
+ }
206
+ });
207
+ expect(maxRules).toContain("Use approvals for risky actions.");
208
+ expect(maxRules).not.toContain("browser_action");
209
+ expect(maxRules).not.toContain("execute_command");
210
+ expect(maxRules).not.toContain("web");
211
+ });
69
212
  });
@@ -11,10 +11,10 @@ import {
11
11
  export const clineWriter: HarnessWriter = {
12
12
  id: "cline",
13
13
  label: "Cline",
14
- description: "VS Code AI agent — .cline/mcp.json + .clinerules",
14
+ description: "VS Code AI agent — cline_mcp_settings.json + .clinerules",
15
15
  async install(config: LogicalConfig, projectRoot: string) {
16
16
  await writeMcpServers(config.mcp_servers, {
17
- path: join(projectRoot, ".cline", "mcp.json"),
17
+ path: join(projectRoot, "cline_mcp_settings.json"),
18
18
  transform: alwaysAllowEntry
19
19
  });
20
20
 
@@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { mkdtemp, rm, readFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import type { LogicalConfig } from "@codemcp/ade-core";
5
+ import type {
6
+ AutonomyProfile,
7
+ LogicalConfig,
8
+ PermissionPolicy
9
+ } from "@codemcp/ade-core";
6
10
  import { copilotWriter } from "./copilot.js";
7
11
 
12
+ function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy {
13
+ switch (profile) {
14
+ case "rigid":
15
+ return {
16
+ profile,
17
+ capabilities: {
18
+ read: "ask",
19
+ edit_write: "ask",
20
+ search_list: "ask",
21
+ bash_safe: "ask",
22
+ bash_unsafe: "ask",
23
+ web: "ask",
24
+ task_agent: "ask"
25
+ }
26
+ };
27
+ case "sensible-defaults":
28
+ return {
29
+ profile,
30
+ capabilities: {
31
+ read: "allow",
32
+ edit_write: "allow",
33
+ search_list: "allow",
34
+ bash_safe: "allow",
35
+ bash_unsafe: "ask",
36
+ web: "ask",
37
+ task_agent: "allow"
38
+ }
39
+ };
40
+ case "max-autonomy":
41
+ return {
42
+ profile,
43
+ capabilities: {
44
+ read: "allow",
45
+ edit_write: "allow",
46
+ search_list: "allow",
47
+ bash_safe: "allow",
48
+ bash_unsafe: "allow",
49
+ web: "ask",
50
+ task_agent: "allow"
51
+ }
52
+ };
53
+ }
54
+ }
55
+
8
56
  describe("copilotWriter", () => {
9
57
  let dir: string;
10
58
 
@@ -96,7 +144,115 @@ describe("copilotWriter", () => {
96
144
  expect(content).toContain("name: ade");
97
145
  expect(content).toContain("tools:");
98
146
  expect(content).toContain(" - workflows/*");
147
+ expect(content).toContain("mcp-servers:");
148
+ expect(content).toContain(" workflows:");
149
+ expect(content).toContain(" type: stdio");
150
+ expect(content).toContain(' command: "npx"');
151
+ expect(content).toContain(' args: ["-y","@codemcp/workflows"]');
152
+ expect(content).toContain(' tools: ["*"]');
153
+ expect(content).toContain(" - read");
99
154
  expect(content).toContain(" - edit");
155
+ expect(content).toContain(" - search");
156
+ expect(content).toContain(" - execute");
157
+ expect(content).toContain(" - agent");
158
+ expect(content).toContain(" - web");
159
+ expect(content).not.toContain("runCommands");
160
+ expect(content).not.toContain("runTasks");
161
+ expect(content).not.toContain("fetch");
162
+ expect(content).not.toContain("githubRepo");
100
163
  expect(content).toContain("Follow TDD.");
101
164
  });
165
+
166
+ it("derives the tools allowlist from autonomy while keeping web access approval-gated", async () => {
167
+ const rigidRoot = join(dir, "rigid");
168
+ const sensibleRoot = join(dir, "sensible");
169
+ const maxRoot = join(dir, "max");
170
+
171
+ const rigidConfig: LogicalConfig = {
172
+ mcp_servers: [
173
+ {
174
+ ref: "workflows",
175
+ command: "npx",
176
+ args: ["-y", "@codemcp/workflows"],
177
+ env: {}
178
+ }
179
+ ],
180
+ instructions: [],
181
+ cli_actions: [],
182
+ knowledge_sources: [],
183
+ skills: [],
184
+ git_hooks: [],
185
+ setup_notes: [],
186
+ permission_policy: autonomyPolicy("rigid")
187
+ };
188
+
189
+ const sensibleConfig: LogicalConfig = {
190
+ ...rigidConfig,
191
+ mcp_servers: [
192
+ {
193
+ ref: "workflows",
194
+ command: "npx",
195
+ args: ["-y", "@codemcp/workflows"],
196
+ env: {},
197
+ allowedTools: ["whats_next", "proceed_to_phase"]
198
+ }
199
+ ],
200
+ permission_policy: autonomyPolicy("sensible-defaults")
201
+ };
202
+
203
+ const maxConfig: LogicalConfig = {
204
+ ...rigidConfig,
205
+ permission_policy: autonomyPolicy("max-autonomy")
206
+ };
207
+
208
+ await copilotWriter.install(rigidConfig, rigidRoot);
209
+ await copilotWriter.install(sensibleConfig, sensibleRoot);
210
+ await copilotWriter.install(maxConfig, maxRoot);
211
+
212
+ const rigidAgent = await readFile(
213
+ join(rigidRoot, ".github", "agents", "ade.agent.md"),
214
+ "utf-8"
215
+ );
216
+ const sensibleAgent = await readFile(
217
+ join(sensibleRoot, ".github", "agents", "ade.agent.md"),
218
+ "utf-8"
219
+ );
220
+ const maxAgent = await readFile(
221
+ join(maxRoot, ".github", "agents", "ade.agent.md"),
222
+ "utf-8"
223
+ );
224
+
225
+ expect(rigidAgent).not.toContain(" - server/workflows/*");
226
+ expect(rigidAgent).toContain(" - workflows/*");
227
+ expect(rigidAgent).not.toContain(" - read");
228
+ expect(rigidAgent).not.toContain(" - edit");
229
+ expect(rigidAgent).not.toContain(" - search");
230
+ expect(rigidAgent).not.toContain(" - execute");
231
+ expect(rigidAgent).not.toContain(" - agent");
232
+ expect(rigidAgent).not.toContain(" - web");
233
+
234
+ expect(sensibleAgent).toContain(" - read");
235
+ expect(sensibleAgent).toContain(" - edit");
236
+ expect(sensibleAgent).toContain(" - search");
237
+ expect(sensibleAgent).toContain(" - agent");
238
+ expect(sensibleAgent).not.toContain(" - execute");
239
+ expect(sensibleAgent).not.toContain(" - todo");
240
+ expect(sensibleAgent).not.toContain(" - web");
241
+ expect(sensibleAgent).toContain(" - workflows/whats_next");
242
+ expect(sensibleAgent).toContain(" - workflows/proceed_to_phase");
243
+ expect(sensibleAgent).not.toContain(" - workflows/*");
244
+ expect(sensibleAgent).toContain(
245
+ ' tools: ["whats_next","proceed_to_phase"]'
246
+ );
247
+
248
+ expect(maxAgent).toContain(" - read");
249
+ expect(maxAgent).toContain(" - edit");
250
+ expect(maxAgent).toContain(" - search");
251
+ expect(maxAgent).toContain(" - execute");
252
+ expect(maxAgent).toContain(" - agent");
253
+ expect(maxAgent).toContain(" - todo");
254
+ expect(maxAgent).not.toContain(" - web");
255
+ expect(maxAgent).toContain(" - workflows/*");
256
+ expect(maxAgent).toContain("mcp-servers:");
257
+ });
102
258
  });
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import type { LogicalConfig } from "@codemcp/ade-core";
2
+ import type { LogicalConfig, McpServerEntry } from "@codemcp/ade-core";
3
3
  import type { HarnessWriter } from "../types.js";
4
4
  import {
5
5
  writeMcpServers,
@@ -7,6 +7,11 @@ import {
7
7
  writeAgentMd,
8
8
  writeGitHooks
9
9
  } from "../util.js";
10
+ import {
11
+ allowsCapability,
12
+ hasPermissionPolicy,
13
+ keepsWebOnAsk
14
+ } from "../permission-policy.js";
10
15
 
11
16
  export const copilotWriter: HarnessWriter = {
12
17
  id: "copilot",
@@ -20,19 +25,81 @@ export const copilotWriter: HarnessWriter = {
20
25
  });
21
26
 
22
27
  const tools = [
23
- "edit",
24
- "search",
25
- "runCommands",
26
- "runTasks",
27
- "fetch",
28
- "githubRepo",
29
- ...config.mcp_servers.map((s) => `${s.ref}/*`)
28
+ ...getBuiltInTools(config),
29
+ ...getForwardedMcpTools(config.mcp_servers)
30
30
  ];
31
31
 
32
32
  await writeAgentMd(config, {
33
33
  path: join(projectRoot, ".github", "agents", "ade.agent.md"),
34
- extraFrontmatter: ["tools:", ...tools.map((t) => ` - ${t}`)]
34
+ extraFrontmatter: [
35
+ "tools:",
36
+ ...tools.map((t) => ` - ${t}`),
37
+ ...renderCopilotAgentMcpServers(config.mcp_servers)
38
+ ]
35
39
  });
36
40
  await writeGitHooks(config.git_hooks, projectRoot);
37
41
  }
38
42
  };
43
+
44
+ function getBuiltInTools(config: LogicalConfig): string[] {
45
+ if (!hasPermissionPolicy(config)) {
46
+ return ["read", "edit", "search", "execute", "agent", "web"];
47
+ }
48
+
49
+ return [
50
+ ...(allowsCapability(config, "read") ? ["read"] : []),
51
+ ...(allowsCapability(config, "edit_write") ? ["edit"] : []),
52
+ ...(allowsCapability(config, "search_list") ? ["search"] : []),
53
+ ...(allowsCapability(config, "bash_unsafe") ? ["execute"] : []),
54
+ ...(allowsCapability(config, "task_agent") ? ["agent"] : []),
55
+ ...(allowsCapability(config, "task_agent") &&
56
+ allowsCapability(config, "bash_unsafe")
57
+ ? ["todo"]
58
+ : []),
59
+ ...(!keepsWebOnAsk(config) && allowsCapability(config, "web")
60
+ ? ["web"]
61
+ : [])
62
+ ];
63
+ }
64
+
65
+ function getForwardedMcpTools(servers: McpServerEntry[]): string[] {
66
+ return servers.flatMap((server) => {
67
+ const allowedTools = server.allowedTools ?? ["*"];
68
+ if (allowedTools.includes("*")) {
69
+ return [`${server.ref}/*`];
70
+ }
71
+
72
+ return allowedTools.map((tool) => `${server.ref}/${tool}`);
73
+ });
74
+ }
75
+
76
+ function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] {
77
+ if (servers.length === 0) {
78
+ return [];
79
+ }
80
+
81
+ const lines = ["mcp-servers:"];
82
+
83
+ for (const server of servers) {
84
+ lines.push(` ${formatYamlKey(server.ref)}:`);
85
+ lines.push(" type: stdio");
86
+ lines.push(` command: ${JSON.stringify(server.command)}`);
87
+ lines.push(` args: ${JSON.stringify(server.args)}`);
88
+ lines.push(` tools: ${JSON.stringify(server.allowedTools ?? ["*"])}`);
89
+
90
+ if (Object.keys(server.env).length > 0) {
91
+ lines.push(" env:");
92
+ for (const [key, value] of Object.entries(server.env)) {
93
+ lines.push(` ${formatYamlKey(key)}: ${JSON.stringify(value)}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ return lines;
99
+ }
100
+
101
+ function formatYamlKey(value: string): string {
102
+ return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value)
103
+ ? value
104
+ : JSON.stringify(value);
105
+ }