Skip to content
Open
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
7 changes: 3 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,11 @@
"program": "${workspaceRoot}/packages/cli/bin/execute.js",
"console": "externalTerminal",
"preLaunchTask": "build",
"outFiles": [ "${workspaceRoot}/lib/**/*.js",
"${workspaceRoot}/spec/**/*.js" ],
"args": [
"new",
"angularproj",
"--framework=angular"
"--framework=angular",
"--skip-install"
]
},
{
Expand Down Expand Up @@ -298,7 +297,7 @@
"preLaunchTask": "build",
"outFiles": ["${workspaceFolder}/**/*.js"],
"args": [
"list"
"ai-config"
]
}, {
"type": "node",
Expand Down
76 changes: 62 additions & 14 deletions packages/cli/lib/commands/ai-config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { addMcpServers, AI_AGENT_LABELS, AI_AGENT_CHOICES, AIAgentTarget, copyAgentInstructionFiles, copyAISkillsToProject, GoogleAnalytics, InquirerWrapper, Util, VS_CODE_MCP_PATH } from "@igniteui/cli-core";
import { addMcpServers, AI_AGENT_LABELS, AI_AGENT_CHOICES, AIAgentTarget, copyAgentInstructionFiles, copyAISkillsToProject, GoogleAnalytics, InquirerWrapper, Util, AiCodingAssistant, AI_ASSISTANT_MCP_CONFIGS, AI_ASSISTANT_CHOICES, AI_ASSISTANT_LABELS } from "@igniteui/cli-core";
import { ArgumentsCamelCase, CommandModule } from "yargs";

export function configureMCP(): void {
const modified = addMcpServers(VS_CODE_MCP_PATH);
export function configureMCP(assistants: AiCodingAssistant[] = ["vscode"]): void {
for (const assistant of assistants) {
const { mcpFilePath } = AI_ASSISTANT_MCP_CONFIGS[assistant];
const modified = addMcpServers(assistant);

if (!modified) {
Util.log(` Ignite UI MCP servers already configured in ${VS_CODE_MCP_PATH}`);
return;
if (!modified) {
Util.log(` Ignite UI MCP servers already configured in ${mcpFilePath}`);
} else {
Util.log(Util.greenCheck() + ` MCP servers configured in ${mcpFilePath}`);
}
}
Util.log(Util.greenCheck() + ` MCP servers configured in ${VS_CODE_MCP_PATH}`);
}

export function configureSkills(agents: AIAgentTarget[]): void {
Expand All @@ -26,32 +29,49 @@ export function configureSkills(agents: AIAgentTarget[]): void {
}
}

export async function configure(agents?: AIAgentTarget[], skills = true): Promise<void> {
export async function configure(agents?: AIAgentTarget[], skills = true, assistants?: AiCodingAssistant[]): Promise<void> {
if (!agents?.length) {
agents = await promptForAgents();
}
if (!agents.length) return;
configureMCP();

if (!assistants?.length) {
assistants = await promptForAssistant();
}
configureMCP(assistants);

if (skills) {
configureSkills(agents);
}
copyAgentInstructionFiles(agents);
}

const AI_AGENT_CHECKBOX_DEFAULTS: AIAgentTarget[] = ["generic", "claude"];

const AI_ASSISTANT_CHECKBOX_DEFAULTS: AiCodingAssistant[] = ["vscode", "claude-code"];

const AI_AGENT_CHECKBOX_CHOICES = [
{ value: "none", name: "None (skip AI configuration)" },
{ value: "none", name: "None (skip skills and instructions)" },
...AI_AGENT_CHOICES.map(agent => ({
value: agent,
name: AI_AGENT_LABELS[agent],
checked: AI_AGENT_CHECKBOX_DEFAULTS.includes(agent)
}))
];

const AI_ASSISTANT_CHECKBOX_CHOICES = [
{ value: "none", name: "None (skip MCP configuration)" },
...AI_ASSISTANT_CHOICES.map(a => ({
value: a,
name: AI_ASSISTANT_LABELS[a],
checked: AI_ASSISTANT_CHECKBOX_DEFAULTS.includes(a)
}))
];

export async function promptForAgents(): Promise<AIAgentTarget[]> {
let selected: AIAgentTarget[] = AI_AGENT_CHECKBOX_DEFAULTS;
if (Util.canPrompt()) {
const result = await InquirerWrapper.checkbox({
message: "Which AI tools do you want to generate configuration files for?",
message: "Which AI agents do you want to generate skills and instructions for?",
required: true,
choices: AI_AGENT_CHECKBOX_CHOICES
});
Expand All @@ -60,6 +80,19 @@ export async function promptForAgents(): Promise<AIAgentTarget[]> {
return selected;
}

export async function promptForAssistant(): Promise<AiCodingAssistant[]> {
let selected: AiCodingAssistant[] = AI_ASSISTANT_CHECKBOX_DEFAULTS;
if (Util.canPrompt()) {
const result = await InquirerWrapper.checkbox({
message: "Which coding assistants should MCP servers be configured for?",
required: true,
choices: AI_ASSISTANT_CHECKBOX_CHOICES
});
selected = result.includes("none") ? [] : result as AiCodingAssistant[];
}
return selected;
}

const command: CommandModule = {
command: "ai-config",
describe: "Configures Ignite UI AI tooling (MCP servers, AI coding skills and instructions)",
Expand All @@ -70,9 +103,15 @@ const command: CommandModule = {
describe: "AI agents/tools to generate configuration files for",
choices: AI_AGENT_CHOICES,
type: "array"
})
.option("assistant", {
describe: "Coding assistant(s) to configure MCP servers for",
choices: AI_ASSISTANT_CHOICES,
type: "array"
}),
async handler(argv: ArgumentsCamelCase) {
let agents = argv.agent as AIAgentTarget[] | undefined;
let assistants = argv.assistant as AiCodingAssistant[] | undefined;
GoogleAnalytics.post({
t: "screenview",
cd: "Ai Config"
Expand All @@ -81,17 +120,26 @@ const command: CommandModule = {
if (!agents?.length) {
agents = await promptForAgents();
}
if (!assistants?.length) {
assistants = await promptForAssistant();
}

GoogleAnalytics.post({
t: "event",
ec: "$ig ai-config",
ea: `agent: ${agents.join(", ")}`
ea: `agent: ${agents?.join(", ") || "none"}; assistant: ${assistants?.join(", ") || "none"}`
});

if (!assistants.length) {
Util.log("No MCP configuration selected. Skipping.");
return;
}

if (!agents.length) {
Util.log("No AI configuration selected. Skipping.");
return;
}
await configure(agents);
await configure(agents, true, assistants);
}
};

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/lib/commands/new.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AI_AGENT_CHOICES, AIAgentTarget, GoogleAnalytics, PackageManager, ProjectConfig, ProjectLibrary, Util } from "@igniteui/cli-core";
import { AI_AGENT_CHOICES, AIAgentTarget, AiCodingAssistant, GoogleAnalytics, PackageManager, ProjectConfig, ProjectLibrary, Util } from "@igniteui/cli-core";
import * as path from "path";
import { PromptSession } from "./../PromptSession";
import { NewCommandType, PositionalArgs } from "./types";
Expand Down Expand Up @@ -166,7 +166,7 @@ const command: NewCommandType = {
}

process.chdir(argv.name);
await configure(argv.agents as AIAgentTarget[] | undefined);
await configure(argv.agents as AIAgentTarget[] | undefined, true, argv.assistants as AiCodingAssistant[] | undefined);
process.chdir("..");

if (!argv["skip-git"] && !ProjectConfig.getConfig().skipGit) {
Expand Down
39 changes: 32 additions & 7 deletions packages/core/util/mcp-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ export interface McpServerEntry {
args: string[];
}

export const AI_ASSISTANT_CHOICES = ["vscode", "claude-code", "cursor", "gemini", "junie"] as const;
export type AiCodingAssistant = typeof AI_ASSISTANT_CHOICES[number];

interface AssistantMcpConfig {
mcpFilePath: string;
rootKey: "servers" | "mcpServers";
}

export const AI_ASSISTANT_LABELS: Record<AiCodingAssistant, string> = {
"vscode": "VS Code (GitHub Copilot)",
"claude-code": "Claude Code",
"cursor": "Cursor",
"gemini": "Gemini",
"junie": "JetBrains Junie",
};

export const AI_ASSISTANT_MCP_CONFIGS: Record<AiCodingAssistant, AssistantMcpConfig> = {
"vscode": { mcpFilePath: ".vscode/mcp.json", rootKey: "servers" },
"claude-code": { mcpFilePath: ".mcp.json", rootKey: "mcpServers" },
"cursor": { mcpFilePath: ".cursor/mcp.json", rootKey: "mcpServers" },
"gemini": { mcpFilePath: ".gemini/settings.json", rootKey: "mcpServers" },
"junie": { mcpFilePath: ".junie/mcp/mcp.json", rootKey: "mcpServers" },
};

const IGNITEUI_MCP_SERVERS: Record<string, McpServerEntry> = {
"igniteui-cli": {
command: "npx",
Expand All @@ -18,18 +42,18 @@ const IGNITEUI_MCP_SERVERS: Record<string, McpServerEntry> = {
}
};

export const VS_CODE_MCP_PATH = ".vscode/mcp.json";

/**
* Reads .vscode/mcp.json, ensures all IgniteUI MCP servers are present,
* Reads the assistant-specific MCP config file, ensures all IgniteUI MCP servers are present,
* optionally adds additional servers. Creates the file if it doesn't exist.
* @param assistant target AI coding assistant (defaults to "vscode")
* @param additionalServers optional extra servers to include alongside the built-in ones
* @returns whether the file was modified
*/
export function addMcpServers(
mcpFilePath: string,
assistant: AiCodingAssistant = "vscode",
additionalServers?: Record<string, McpServerEntry>
): boolean {
const { mcpFilePath, rootKey } = AI_ASSISTANT_MCP_CONFIGS[assistant];
const fileSystem = App.container.get<IFileSystem>(FS_TOKEN);
const servers = { ...additionalServers, ...IGNITEUI_MCP_SERVERS };

Expand All @@ -44,20 +68,20 @@ export function addMcpServers(
if (Object.keys(servers).length === 0) {
return false;
}
fileSystem.writeFile(mcpFilePath, JSON.stringify({ servers }, null, 2) + "\n");
fileSystem.writeFile(mcpFilePath, JSON.stringify({ [rootKey]: servers }, null, 2) + "\n");
return true;
}

const parsed = jsonc.parse(existingContent);
const existing = parsed.servers ?? {};
const existing = parsed[rootKey] ?? {};
const formattingOptions: jsonc.FormattingOptions = { tabSize: 2, insertSpaces: true };

let text = existingContent;
let modified = false;

for (const [key, value] of Object.entries(servers)) {
if (!existing[key]) {
const edits = jsonc.modify(text, ["servers", key], value, { formattingOptions });
const edits = jsonc.modify(text, [rootKey, key], value, { formattingOptions });
text = jsonc.applyEdits(text, edits);
modified = true;
}
Expand All @@ -69,3 +93,4 @@ export function addMcpServers(

return modified;
}

26 changes: 24 additions & 2 deletions packages/ng-schematics/src/cli-config/ai-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
"enum": ["none", "claude", "copilot", "cursor", "codex", "windsurf", "gemini", "junie", "generic"]
},
"x-prompt": {
"message": "Which AI tools do you want to generate configuration files for?",
"message": "Which AI agents do you want to generate skills and instructions for?",
"type": "list",
"multiselect": true,
"items": [
{ "value": "none", "label": "None (skip AI configuration)" },
{ "value": "none", "label": "None (skip skills and instructions)" },
{ "value": "generic", "label": "Generic (Adding .agents/skills and AGENTS.md)" },
{ "value": "claude", "label": "Claude (Adding .claude/skills and CLAUDE.md)" },
{ "value": "copilot", "label": "Copilot (Adding .github/skills and copilot-instructions.md)" },
Expand All @@ -30,6 +30,28 @@
{ "value": "junie", "label": "Junie (Adding .junie/skills and .junie/guidelines.md)" }
]
}
},
"assistant": {
"type": "array",
"description": "Coding assistant(s) to configure MCP servers for.",
"default": ["vscode"],
"items": {
"type": "string",
"enum": ["none", "vscode", "cursor", "claude-code", "gemini", "junie"]
},
"x-prompt": {
"message": "Which coding assistants should MCP servers be configured for?",
"type": "list",
"multiselect": true,
"items": [
{ "value": "none", "label": "None (skip MCP configuration)" },
{ "value": "vscode", "label": "VS Code (GitHub Copilot)" },
{ "value": "cursor", "label": "Cursor" },
{ "value": "claude-code", "label": "Claude Code" },
{ "value": "gemini", "label": "Gemini" },
{ "value": "junie", "label": "JetBrains Junie" }
]
}
}
}
}
17 changes: 9 additions & 8 deletions packages/ng-schematics/src/cli-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as ts from "typescript";
import { DependencyNotFoundException } from "@angular-devkit/core";
import { chain, FileDoesNotExistException, Rule, SchematicContext, Tree } from "@angular-devkit/schematics";
import { RunSchematicTask } from "@angular-devkit/schematics/tasks";
import { addClassToBody, addMcpServers, AIAgentTarget, App, copyAgentInstructionFiles, copyAISkillsToProject, FormatSettings, McpServerEntry, NPM_ANGULAR, resolvePackage, TEMPLATE_MANAGER, TypeScriptAstTransformer, TypeScriptUtils, VS_CODE_MCP_PATH } from "@igniteui/cli-core";
import { addClassToBody, addMcpServers, AIAgentTarget, AiCodingAssistant, App, copyAgentInstructionFiles, copyAISkillsToProject, FormatSettings, McpServerEntry, NPM_ANGULAR, resolvePackage, TEMPLATE_MANAGER, TypeScriptAstTransformer, TypeScriptUtils } from "@igniteui/cli-core";
import { AngularTypeScriptFileUpdate } from "@igniteui/angular-templates";
import { createCliConfig } from "../utils/cli-config";
import { setVirtual } from "../utils/NgFileSystem";
Expand Down Expand Up @@ -127,7 +127,7 @@ function appInit(tree: Tree) {
setVirtual(tree);
}

function aiConfig({ init, agents }: { init: boolean; agents: AIAgentTarget[] }): Rule {
function aiConfig({ init, agents, assistants = ["vscode"] }: { init: boolean; agents: AIAgentTarget[]; assistants?: AiCodingAssistant[] }): Rule {
return (tree: Tree) => {
if (init) {
appInit(tree);
Expand All @@ -142,18 +142,19 @@ function aiConfig({ init, agents }: { init: boolean; agents: AIAgentTarget[] }):
}
};

addMcpServers(VS_CODE_MCP_PATH, angularCliServer);
for (const assistant of assistants) {
addMcpServers(assistant, angularCliServer);
}
};
}

/** Standalone `ai-config` schematic entry */
export function addAIConfig(options: { agents?: AIAgentTarget[] } = {}): Rule {
export function addAIConfig(options: { agents?: AIAgentTarget[]; /* TODO: assistants */ assistant?: AiCodingAssistant[] } = {}): Rule {
const selected = options.agents?.length ? options.agents : [] as AIAgentTarget[];
const agents = selected.includes("none" as any) ? [] : selected;
if (!agents.length) {
return (tree: Tree) => tree;
}
return aiConfig({ init: true, agents });
const selectedAssistants = options.assistant?.length ? options.assistant : ["vscode"] as AiCodingAssistant[];
const assistants = selectedAssistants.includes("none" as any) ? [] : selectedAssistants;
return aiConfig({ init: true, agents, assistants });
}

export default function (): Rule {
Expand Down
32 changes: 32 additions & 0 deletions packages/ng-schematics/src/cli-config/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,5 +432,37 @@ export const appConfig: ApplicationConfig = {
expect(aiSkillsModule.copyAISkillsToProject).toHaveBeenCalledWith(["claude", "cursor"]);
expect(aiSkillsModule.copyAgentInstructionFiles).toHaveBeenCalledWith(["claude", "cursor"]);
});

it("should default MCP config to .vscode/mcp.json with servers key", async () => {
await runner.runSchematic("ai-config", {}, tree);

const mcpFilePath = "/.vscode/mcp.json";
expect(tree.exists(mcpFilePath)).toBeTruthy();
const content = JSON.parse(tree.readContent(mcpFilePath));
expect(content.servers).toBeDefined();
expect(content.servers["igniteui-cli"]).toEqual({ command: "npx", args: ["-y", "igniteui-cli", "mcp"] });
});

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

const mcpFilePath = "/.cursor/mcp.json";
expect(tree.exists(mcpFilePath)).toBeTruthy();
const content = JSON.parse(tree.readContent(mcpFilePath));
expect(content.mcpServers).toBeDefined();
expect(content.mcpServers["igniteui-cli"]).toEqual({ command: "npx", args: ["-y", "igniteui-cli", "mcp"] });
expect(content.mcpServers["angular-cli"]).toEqual({ command: "npx", args: ["-y", "@angular/cli", "mcp"] });
expect(content.servers).toBeUndefined();
});

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

const mcpFilePath = "/.mcp.json";
expect(tree.exists(mcpFilePath)).toBeTruthy();
const content = JSON.parse(tree.readContent(mcpFilePath));
expect(content.mcpServers["igniteui-cli"]).toBeDefined();
expect(content.mcpServers["angular-cli"]).toBeDefined();
});
});
});
Loading
Loading