Skip to content

Commit 322523e

Browse files
Marina-L-StoyanovaCopilot
authored andcommitted
feat: update MCP configuration to support multiple coding assistants and enhance related prompts
Co-authored-by: Copilot <copilot@github.com>
1 parent 25318e0 commit 322523e

5 files changed

Lines changed: 85 additions & 52 deletions

File tree

packages/cli/lib/commands/ai-config.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { addMcpServers, AI_AGENT_LABELS, AI_AGENT_CHOICES, AIAgentTarget, copyAgentInstructionFiles, copyAISkillsToProject, GoogleAnalytics, InquirerWrapper, Util, AiCodingAssistant, AI_ASSISTANT_MCP_CONFIGS } from "@igniteui/cli-core";
22
import { ArgumentsCamelCase, CommandModule } from "yargs";
33

4-
export function configureMCP(assistant: AiCodingAssistant = "vscode"): void {
5-
const { mcpFilePath } = AI_ASSISTANT_MCP_CONFIGS[assistant];
6-
const modified = addMcpServers(assistant);
7-
8-
if (!modified) {
9-
Util.log(` Ignite UI MCP servers already configured in ${mcpFilePath}`);
10-
return;
4+
export function configureMCP(assistants: AiCodingAssistant[] = ["vscode"]): void {
5+
for (const assistant of assistants) {
6+
const { mcpFilePath } = AI_ASSISTANT_MCP_CONFIGS[assistant];
7+
const modified = addMcpServers(assistant);
8+
9+
if (!modified) {
10+
Util.log(` Ignite UI MCP servers already configured in ${mcpFilePath}`);
11+
} else {
12+
Util.log(Util.greenCheck() + ` MCP servers configured in ${mcpFilePath}`);
13+
}
1114
}
12-
Util.log(Util.greenCheck() + ` MCP servers configured in ${mcpFilePath}`);
1315
}
1416

1517
export function configureSkills(agents: AIAgentTarget[]): void {
@@ -27,12 +29,12 @@ export function configureSkills(agents: AIAgentTarget[]): void {
2729
}
2830
}
2931

30-
export async function configure(agents?: AIAgentTarget[], skills = true, assistant: AiCodingAssistant = "vscode"): Promise<void> {
32+
export async function configure(agents?: AIAgentTarget[], skills = true, assistants: AiCodingAssistant[] = ["vscode"]): Promise<void> {
3133
if (!agents?.length) {
3234
agents = await promptForAgents();
3335
}
3436
if (!agents.length) return;
35-
configureMCP(assistant);
37+
configureMCP(assistants);
3638
if (skills) {
3739
configureSkills(agents);
3840
}
@@ -59,6 +61,15 @@ const AI_AGENT_CHECKBOX_CHOICES = [
5961
}))
6062
];
6163

64+
const AI_ASSISTANT_CHECKBOX_CHOICES = [
65+
{ value: "none", name: "None (skip MCP configuration)" },
66+
...AI_ASSISTANT_CHOICES.map(a => ({
67+
value: a,
68+
name: AI_ASSISTANT_LABELS[a],
69+
checked: a === "vscode"
70+
}))
71+
];
72+
6273
export async function promptForAgents(): Promise<AIAgentTarget[]> {
6374
let selected: AIAgentTarget[] = AI_AGENT_CHECKBOX_DEFAULTS;
6475
if (Util.canPrompt()) {
@@ -72,12 +83,14 @@ export async function promptForAgents(): Promise<AIAgentTarget[]> {
7283
return selected;
7384
}
7485

75-
export async function promptForAssistant(): Promise<AiCodingAssistant> {
76-
const selected = await InquirerWrapper.select({
77-
message: "Which coding assistant should MCP servers be configured for?",
78-
choices: AI_ASSISTANT_CHOICES.map(a => ({ value: a, name: AI_ASSISTANT_LABELS[a] }))
86+
export async function promptForAssistant(): Promise<AiCodingAssistant[]> {
87+
// TODO: check Util.canPrompt() and assign defaults
88+
const selected = await InquirerWrapper.checkbox({
89+
message: "Which coding assistants should MCP servers be configured for?",
90+
required: true,
91+
choices: AI_ASSISTANT_CHECKBOX_CHOICES
7992
});
80-
return selected as AiCodingAssistant;
93+
return selected.includes("none") ? [] : selected as AiCodingAssistant[];
8194
}
8295

8396
const command: CommandModule = {
@@ -92,21 +105,21 @@ const command: CommandModule = {
92105
type: "array"
93106
})
94107
.option("assistant", {
95-
describe: "Coding assistant to configure MCP servers for",
108+
describe: "Coding assistant(s) to configure MCP servers for",
96109
choices: AI_ASSISTANT_CHOICES,
97-
type: "string"
110+
type: "array"
98111
}),
99112
async handler(argv: ArgumentsCamelCase) {
100113
let agents = argv.agent as AIAgentTarget[] | undefined;
101-
let assistant = argv.assistant as AiCodingAssistant | undefined;
114+
let assistants = argv.assistant as AiCodingAssistant[] | undefined;
102115

103116
GoogleAnalytics.post({
104117
t: "screenview",
105118
cd: "Ai Config"
106119
});
107120

108-
if (!assistant) {
109-
assistant = await promptForAssistant();
121+
if (!assistants?.length) {
122+
assistants = await promptForAssistant();
110123
}
111124
if (!agents?.length) {
112125
agents = await promptForAgents();
@@ -118,7 +131,9 @@ const command: CommandModule = {
118131
ea: `agent: ${agents.join(", ") || "none"}`
119132
});
120133

121-
configureMCP(assistant);
134+
if (assistants.length) {
135+
configureMCP(assistants);
136+
}
122137

123138
if (!agents.length) {
124139
Util.log("No AI configuration selected. Skipping.");

packages/ng-schematics/src/cli-config/ai-config-schema.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,19 @@
3232
}
3333
},
3434
"assistant": {
35-
"type": "string",
36-
"description": "Coding assistant to configure MCP servers for.",
37-
"default": "vscode",
38-
"enum": ["vscode", "cursor", "claude-code", "gemini", "junie"],
35+
"type": "array",
36+
"description": "Coding assistant(s) to configure MCP servers for.",
37+
"default": ["vscode"],
38+
"items": {
39+
"type": "string",
40+
"enum": ["none", "vscode", "cursor", "claude-code", "gemini", "junie"]
41+
},
3942
"x-prompt": {
40-
"message": "Which coding assistant should MCP servers be configured for?",
43+
"message": "Which coding assistants should MCP servers be configured for?",
4144
"type": "list",
45+
"multiselect": true,
4246
"items": [
47+
{ "value": "none", "label": "None (skip MCP configuration)" },
4348
{ "value": "vscode", "label": "VS Code (GitHub Copilot)" },
4449
{ "value": "cursor", "label": "Cursor" },
4550
{ "value": "claude-code", "label": "Claude Code" },

packages/ng-schematics/src/cli-config/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ function appInit(tree: Tree) {
127127
setVirtual(tree);
128128
}
129129

130-
function aiConfig({ init, agents, assistant = "vscode" }: { init: boolean; agents: AIAgentTarget[]; assistant?: AiCodingAssistant }): Rule {
130+
function aiConfig({ init, agents, assistants = ["vscode"] }: { init: boolean; agents: AIAgentTarget[]; assistants?: AiCodingAssistant[] }): Rule {
131131
return (tree: Tree) => {
132132
if (init) {
133133
appInit(tree);
@@ -142,15 +142,19 @@ function aiConfig({ init, agents, assistant = "vscode" }: { init: boolean; agent
142142
}
143143
};
144144

145-
addMcpServers(assistant, angularCliServer);
145+
for (const assistant of assistants) {
146+
addMcpServers(assistant, angularCliServer);
147+
}
146148
};
147149
}
148150

149151
/** Standalone `ai-config` schematic entry */
150-
export function addAIConfig(options: { agents?: AIAgentTarget[]; assistant?: AiCodingAssistant } = {}): Rule {
152+
export function addAIConfig(options: { agents?: AIAgentTarget[]; /* TODO: assistants */ assistant?: AiCodingAssistant[] } = {}): Rule {
151153
const selected = options.agents?.length ? options.agents : [] as AIAgentTarget[];
152154
const agents = selected.includes("none" as any) ? [] : selected;
153-
return aiConfig({ init: true, agents, assistant: options.assistant });
155+
const selectedAssistants = options.assistant?.length ? options.assistant : ["vscode"] as AiCodingAssistant[];
156+
const assistants = selectedAssistants.includes("none" as any) ? [] : selectedAssistants;
157+
return aiConfig({ init: true, agents, assistants });
154158
}
155159

156160
export default function (): Rule {

packages/ng-schematics/src/cli-config/index_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ export const appConfig: ApplicationConfig = {
444444
});
445445

446446
it("should write to .cursor/mcp.json with mcpServers key when assistant is cursor", async () => {
447-
await runner.runSchematic("ai-config", { assistant: "cursor" }, tree);
447+
await runner.runSchematic("ai-config", { assistant: ["cursor"] }, tree);
448448

449449
const mcpFilePath = "/.cursor/mcp.json";
450450
expect(tree.exists(mcpFilePath)).toBeTruthy();
@@ -456,7 +456,7 @@ export const appConfig: ApplicationConfig = {
456456
});
457457

458458
it("should write to .mcp.json when assistant is claude-code", async () => {
459-
await runner.runSchematic("ai-config", { assistant: "claude-code" }, tree);
459+
await runner.runSchematic("ai-config", { assistant: ["claude-code"] }, tree);
460460

461461
const mcpFilePath = "/.mcp.json";
462462
expect(tree.exists(mcpFilePath)).toBeTruthy();

spec/unit/ai-config-spec.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe("Unit - ai-config command", () => {
5454
const mockFs = createMockFs();
5555
App.container.set(FS_TOKEN, mockFs);
5656

57-
configureMCP("cursor");
57+
configureMCP(["cursor"]);
5858

5959
expect(mockFs.writeFile).toHaveBeenCalledWith(".cursor/mcp.json", jasmine.any(String));
6060
const config = writtenConfig(mockFs);
@@ -277,8 +277,10 @@ describe("Unit - ai-config command", () => {
277277
it("prompts for agents when --agent is not provided", async () => {
278278
App.container.set(FS_TOKEN, createMockFs());
279279
spyOn(Util, "canPrompt").and.returnValue(true);
280-
spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["claude"]));
281-
spyOn(InquirerWrapper, "select").and.returnValue(Promise.resolve("vscode"));
280+
spyOn(InquirerWrapper, "checkbox").and.returnValues(
281+
Promise.resolve(["vscode"]),
282+
Promise.resolve(["claude"])
283+
);
282284

283285
await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" });
284286

@@ -317,8 +319,10 @@ describe("Unit - ai-config command", () => {
317319
it("still configures MCP when none is selected for skills", async () => {
318320
const mockFs = createMockFs();
319321
App.container.set(FS_TOKEN, mockFs);
320-
spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["none"]));
321-
spyOn(InquirerWrapper, "select").and.returnValue(Promise.resolve("vscode"));
322+
spyOn(InquirerWrapper, "checkbox").and.returnValues(
323+
Promise.resolve(["vscode"]),
324+
Promise.resolve(["none"])
325+
);
322326

323327
await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" });
324328

@@ -331,8 +335,10 @@ describe("Unit - ai-config command", () => {
331335
it("configures multiple agents when selected interactively", async () => {
332336
App.container.set(FS_TOKEN, createMockFs());
333337
spyOn(Util, "canPrompt").and.returnValue(true);
334-
spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["claude", "cursor"]));
335-
spyOn(InquirerWrapper, "select").and.returnValue(Promise.resolve("vscode"));
338+
spyOn(InquirerWrapper, "checkbox").and.returnValues(
339+
Promise.resolve(["vscode"]),
340+
Promise.resolve(["claude", "cursor"])
341+
);
336342

337343
await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" });
338344

@@ -344,43 +350,46 @@ describe("Unit - ai-config command", () => {
344350

345351
it("skips prompt when --agent is provided", async () => {
346352
App.container.set(FS_TOKEN, createMockFs());
347-
spyOn(InquirerWrapper, "checkbox");
348-
spyOn(InquirerWrapper, "select").and.returnValue(Promise.resolve("vscode"));
353+
spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["vscode"]));
349354

350355
await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", agent: ["cursor"] });
351356

352-
expect(InquirerWrapper.checkbox).not.toHaveBeenCalled();
357+
expect(InquirerWrapper.checkbox).not.toHaveBeenCalledWith(jasmine.objectContaining({
358+
message: "Which AI agents do you want to generate skills and instructions for?"
359+
}));
353360
expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ ea: "agent: cursor" }));
354361
});
355362

356363
it("skips assistant prompt when --assistant is provided", async () => {
357364
App.container.set(FS_TOKEN, createMockFs());
358365
spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["claude"]));
359-
spyOn(InquirerWrapper, "select");
360366

361-
await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", assistant: "cursor" });
367+
await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", assistant: ["cursor"] });
362368

363-
expect(InquirerWrapper.select).not.toHaveBeenCalled();
364-
expect((createMockFs().writeFile as jasmine.Spy).calls || true).toBeTruthy();
369+
expect(InquirerWrapper.checkbox).toHaveBeenCalledTimes(1);
365370
});
366371

367372
it("prompts for assistant with correct message", async () => {
368373
App.container.set(FS_TOKEN, createMockFs());
369-
spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["claude"]));
370-
spyOn(InquirerWrapper, "select").and.returnValue(Promise.resolve("cursor"));
374+
spyOn(InquirerWrapper, "checkbox").and.returnValues(
375+
Promise.resolve(["cursor"]),
376+
Promise.resolve(["claude"])
377+
);
371378

372379
await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" });
373380

374-
expect(InquirerWrapper.select).toHaveBeenCalledWith(jasmine.objectContaining({
375-
message: "Which coding assistant should MCP servers be configured for?"
381+
expect(InquirerWrapper.checkbox).toHaveBeenCalledWith(jasmine.objectContaining({
382+
message: "Which coding assistants should MCP servers be configured for?"
376383
}));
377384
});
378385

379386
it("writes to correct config path for selected assistant", async () => {
380387
const mockFs = createMockFs();
381388
App.container.set(FS_TOKEN, mockFs);
382-
spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["none"]));
383-
spyOn(InquirerWrapper, "select").and.returnValue(Promise.resolve("claude-code"));
389+
spyOn(InquirerWrapper, "checkbox").and.returnValues(
390+
Promise.resolve(["claude-code"]),
391+
Promise.resolve(["none"])
392+
);
384393

385394
await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" });
386395

0 commit comments

Comments
 (0)