Skip to content
Draft
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
6 changes: 3 additions & 3 deletions packages/cli/lib/PromptSession.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
AI_AGENT_LABELS, AI_AGENT_SKILLS_DIRS, AIAgentTarget,
BasePromptSession, GoogleAnalytics, InquirerWrapper, PackageManager, ProjectConfig,
BasePromptSession, configureMcpForAgents, GoogleAnalytics, InquirerWrapper, PackageManager, ProjectConfig,
ProjectLibrary, PromptTaskContext, Task, Util
} from "@igniteui/cli-core";
import * as path from "path";
import { default as add } from "./commands/add";
import { configure, configureMCP } from "./commands/ai-config";
import { configure } from "./commands/ai-config";
import { default as start } from "./commands/start";
import { default as upgrade } from "./commands/upgrade";
import { TemplateManager } from "./TemplateManager";
Expand Down Expand Up @@ -125,7 +125,7 @@ export class PromptSession extends BasePromptSession {

protected async configureAI(): Promise<void> {
// skip adding skills since those are baked into the project template atm:
configureMCP();
configureMcpForAgents([]);
}

/**
Expand Down
14 changes: 2 additions & 12 deletions packages/cli/lib/commands/ai-config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import { addMcpServers, AI_AGENT_LABELS, AI_AGENT_SKILLS_DIRS, AIAgentTarget, copyAgentInstructionFiles, copyAISkillsToProject, getSkillsDir, GoogleAnalytics, InquirerWrapper, Util, VS_CODE_MCP_PATH } from "@igniteui/cli-core";
import { AI_AGENT_LABELS, AI_AGENT_SKILLS_DIRS, AIAgentTarget, configureMcpForAgents, copyAgentInstructionFiles, copyAISkillsToProject, getSkillsDir, GoogleAnalytics, InquirerWrapper, Util } from "@igniteui/cli-core";
import { ArgumentsCamelCase, CommandModule } from "yargs";

export function configureMCP(): void {
const modified = addMcpServers(VS_CODE_MCP_PATH);

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

export function configureSkills(skillsDir: string): void {
const result = copyAISkillsToProject(skillsDir);
if (result.found === 0) {
Expand All @@ -27,7 +17,7 @@ export function configureSkills(skillsDir: string): void {
}

export function configure(agents: AIAgentTarget[]): void {
configureMCP();
configureMcpForAgents(agents);
for (const agent of agents) {
configureSkills(getSkillsDir(agent));
}
Expand Down

This file was deleted.

This file was deleted.

47 changes: 42 additions & 5 deletions packages/core/util/mcp-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FS_TOKEN, IFileSystem } from "../types/FileSystem";
import * as jsonc from "jsonc-parser";
import { App } from "./App";
import { AIAgentTarget } from "./ai-skills";

export interface McpServerEntry {
command: string;
Expand All @@ -19,16 +20,28 @@ const IGNITEUI_MCP_SERVERS: Record<string, McpServerEntry> = {
};

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

/** Project-level MCP config file paths and their servers root key, keyed by AI agent target.
* Agents not listed here do not have a project-level MCP config and will only receive the
* baseline VS Code configuration when `configureMcpForAgents` is called.
*/
export const AI_AGENT_MCP_CONFIGS: Partial<Record<AIAgentTarget, { path: string; serversKey: string }>> = {
cursor: { path: CURSOR_MCP_PATH, serversKey: "mcpServers" }
};

/**
* Reads .vscode/mcp.json, ensures all IgniteUI MCP servers are present,
* Reads a MCP config JSON file, ensures all IgniteUI MCP servers are present,
* optionally adds additional servers. Creates the file if it doesn't exist.
* @param mcpFilePath path to the MCP config file
* @param additionalServers optional extra servers to include alongside the built-in ones
* @param serversKey root key used for the servers object in the JSON (default: "servers")
* @returns whether the file was modified
*/
export function addMcpServers(
mcpFilePath: string,
additionalServers?: Record<string, McpServerEntry>
additionalServers?: Record<string, McpServerEntry>,
serversKey = "servers"
): boolean {
const fileSystem = App.container.get<IFileSystem>(FS_TOKEN);
const servers = { ...additionalServers, ...IGNITEUI_MCP_SERVERS };
Expand All @@ -44,20 +57,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({ [serversKey]: servers }, null, 2) + "\n");
return true;
}

const parsed = jsonc.parse(existingContent);
const existing = parsed.servers ?? {};
const existing = parsed[serversKey] ?? {};
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, [serversKey, key], value, { formattingOptions });
text = jsonc.applyEdits(text, edits);
modified = true;
}
Expand All @@ -69,3 +82,27 @@ export function addMcpServers(

return modified;
}

/**
* Writes MCP server configuration for the selected AI agents.
* Always writes `.vscode/mcp.json` (VS Code baseline) — even when `agents` is empty.
* Also writes `.cursor/mcp.json` (with `mcpServers` key) when `cursor` is in the agents list.
* Agents without an entry in `AI_AGENT_MCP_CONFIGS` only receive the VS Code baseline file.
* @param agents list of AI agent targets (pass an empty array to configure VS Code only)
* @param additionalServers optional extra servers to include alongside the built-in ones
*/
export function configureMcpForAgents(
agents: AIAgentTarget[],
additionalServers?: Record<string, McpServerEntry>
): void {
// VS Code is always written as the baseline
addMcpServers(VS_CODE_MCP_PATH, additionalServers, "servers");

// Write agent-specific config files for agents that have a project-level MCP config
for (const agent of agents) {
const agentConfig = AI_AGENT_MCP_CONFIGS[agent];
if (agentConfig) {
addMcpServers(agentConfig.path, additionalServers, agentConfig.serversKey);
}
}
}

This file was deleted.

4 changes: 2 additions & 2 deletions packages/ng-schematics/src/cli-config/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as ts from "typescript";
import { DependencyNotFoundException } from "@angular-devkit/core";
import { chain, FileDoesNotExistException, Rule, SchematicContext, Tree } from "@angular-devkit/schematics";
import { addClassToBody, addMcpServers, AIAgentTarget, App, copyAgentInstructionFiles, copyAISkillsToProject, FormatSettings, getSkillsDir, McpServerEntry, NPM_ANGULAR, resolvePackage, TEMPLATE_MANAGER, TypeScriptAstTransformer, TypeScriptUtils, VS_CODE_MCP_PATH } from "@igniteui/cli-core";
import { addClassToBody, AIAgentTarget, App, configureMcpForAgents, copyAgentInstructionFiles, copyAISkillsToProject, FormatSettings, getSkillsDir, 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 @@ -143,7 +143,7 @@ function aiConfig({ init, agents }: { init: boolean; agents: AIAgentTarget[] }):
}
};

addMcpServers(VS_CODE_MCP_PATH, angularCliServer);
configureMcpForAgents(agents, angularCliServer);
};
}

Expand Down
10 changes: 10 additions & 0 deletions packages/ng-schematics/src/cli-config/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,16 @@ export const appConfig: ApplicationConfig = {
expect(aiSkillsModule.copyAgentInstructionFiles).toHaveBeenCalledWith(["generic"]);
});

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

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

it("should configure multiple agents", async () => {
await runner.runSchematic("ai-config", { agent: ["claude", "cursor"] }, tree);

Expand Down
11 changes: 7 additions & 4 deletions spec/unit/PromptSession-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { PromptSession } from "../../packages/cli/lib/PromptSession";
import { TemplateManager } from "../../packages/cli/lib/TemplateManager";
import { Separator } from "@inquirer/prompts";

// Require the leaf module directly so spyOn works (same pattern as index_spec.ts)
const mcpConfigModule = require("@igniteui/cli-core/util/mcp-config");

function createMockBaseTemplate(): BaseTemplate {
return {
id: "mock-template-id",
Expand Down Expand Up @@ -102,7 +105,7 @@ describe("Unit - PromptSession", () => {
});

beforeEach(() => {
spyOn(aiConfig, "configureMCP");
spyOn(mcpConfigModule, "configureMcpForAgents");
});

// TODO: most of the tests use same setup - move the setup to beforeAll call
Expand Down Expand Up @@ -504,7 +507,7 @@ describe("Unit - PromptSession", () => {
expect(Util.log).toHaveBeenCalledTimes(3);
expect(PackageManager.flushQueue).toHaveBeenCalledWith(true);
expect(start.start).toHaveBeenCalledTimes(1);
expect(aiConfig.configureMCP).toHaveBeenCalledTimes(1);
expect(mcpConfigModule.configureMcpForAgents).toHaveBeenCalledTimes(1);

expect(add.addTemplate).toHaveBeenCalledTimes(1);
expect(InquirerWrapper.input).toHaveBeenCalledWith({
Expand Down Expand Up @@ -583,7 +586,7 @@ describe("Unit - PromptSession", () => {
expect(Util.log).toHaveBeenCalledTimes(3);
expect(PackageManager.flushQueue).toHaveBeenCalledWith(true);
expect(start.start).toHaveBeenCalledTimes(1);
expect(aiConfig.configureMCP).toHaveBeenCalledTimes(1);
expect(mcpConfigModule.configureMcpForAgents).toHaveBeenCalledTimes(1);

expect(Util.getAvailableName).toHaveBeenCalledTimes(1);
expect(add.addTemplate).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -709,7 +712,7 @@ describe("Unit - PromptSession", () => {
expect(Util.log).toHaveBeenCalledTimes(3);
expect(PackageManager.flushQueue).toHaveBeenCalledWith(true);
expect(start.start).toHaveBeenCalledTimes(1);
expect(aiConfig.configureMCP).toHaveBeenCalledTimes(1);
expect(mcpConfigModule.configureMcpForAgents).toHaveBeenCalledTimes(1);

expect(add.addTemplate).toHaveBeenCalledTimes(1);
expect(InquirerWrapper.checkbox).toHaveBeenCalledWith({
Expand Down
Loading