Skip to content
Merged
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
43 changes: 42 additions & 1 deletion packages/ng-schematics/src/cli-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,46 @@ function importStyles(): Rule {
};
}

export function addAIConfig(): Rule {
return (tree: Tree) => {
const mcpFilePath = "/.vscode/mcp.json";
const igniteuiServer = {
command: "npx",
args: ["-y", "igniteui-cli@next", "mcp"]
};
const igniteuiThemingServer = {
command: "npx",
args: ["-y", "igniteui-theming", "igniteui-theming-mcp"]
};

if (tree.exists(mcpFilePath)) {
const content = JSON.parse(tree.read(mcpFilePath)!.toString());
const servers = content.servers ?? {};
let modified = false;
if (!servers["igniteui"]) {
Comment on lines +132 to +136
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addAIConfig assumes /.vscode/mcp.json is valid JSON and that content.servers is a mutable object. If the file is empty/invalid JSON (or servers is a string/array), JSON.parse/property assignment will throw and abort the schematic run. Consider wrapping the read/parse in try/catch (or using a tolerant parser if mcp.json can contain comments) and validating servers is a plain object before mutating; if parsing/validation fails, log a warning and skip updating rather than crashing.

Copilot uses AI. Check for mistakes.
servers["igniteui"] = igniteuiServer;
modified = true;
}
if (!servers["igniteui-theming"]) {
servers["igniteui-theming"] = igniteuiThemingServer;
modified = true;
}
if (modified) {
content.servers = servers;
tree.overwrite(mcpFilePath, JSON.stringify(content, null, 2));
}
} else {
const mcpConfig = {
servers: {
"igniteui": igniteuiServer,
"igniteui-theming": igniteuiThemingServer
}
};
tree.create(mcpFilePath, JSON.stringify(mcpConfig, null, 2));
}
};
}

export default function (): Rule {
return (tree: Tree) => {
setVirtual(tree);
Expand All @@ -125,7 +165,8 @@ export default function (): Rule {
addTypographyToProj(),
importBrowserAnimations(),
createCliConfig(),
displayVersionMismatch()
displayVersionMismatch(),
addAIConfig()
]);
};
}
81 changes: 81 additions & 0 deletions packages/ng-schematics/src/cli-config/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,85 @@ export const appConfig: ApplicationConfig = {
await runner.runSchematic("cli-config", {}, tree);
expect(warns).toContain(jasmine.stringMatching(pattern));
});

describe("addAIConfig", () => {
const mcpFilePath = "/.vscode/mcp.json";

it("should create .vscode/mcp.json with both servers when file does not exist", async () => {
await runner.runSchematic("cli-config", {}, tree);

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

it("should add both servers to existing .vscode/mcp.json that has no servers", async () => {
tree.create(mcpFilePath, JSON.stringify({ servers: {} }));

await runner.runSchematic("cli-config", {}, tree);

const content = JSON.parse(tree.readContent(mcpFilePath));
expect(content.servers["igniteui"]).toEqual({ command: "npx", args: ["-y", "igniteui-cli@next", "mcp"] });
expect(content.servers["igniteui-theming"]).toEqual({ command: "npx", args: ["-y", "igniteui-theming", "igniteui-theming-mcp"] });
});

it("should add missing igniteui-theming server if only igniteui is already present", async () => {
tree.create(mcpFilePath, JSON.stringify({
servers: {
"igniteui": { command: "npx", args: ["-y", "igniteui-cli@next", "mcp"] }
}
}));

await runner.runSchematic("cli-config", {}, tree);

const content = JSON.parse(tree.readContent(mcpFilePath));
expect(content.servers["igniteui"]).toEqual({ command: "npx", args: ["-y", "igniteui-cli@next", "mcp"] });
expect(content.servers["igniteui-theming"]).toEqual({ command: "npx", args: ["-y", "igniteui-theming", "igniteui-theming-mcp"] });
});

it("should add missing igniteui server if only igniteui-theming is already present", async () => {
tree.create(mcpFilePath, JSON.stringify({
servers: {
"igniteui-theming": { command: "npx", args: ["-y", "igniteui-theming", "igniteui-theming-mcp"] }
}
}));

await runner.runSchematic("cli-config", {}, tree);

const content = JSON.parse(tree.readContent(mcpFilePath));
expect(content.servers["igniteui"]).toEqual({ command: "npx", args: ["-y", "igniteui-cli@next", "mcp"] });
expect(content.servers["igniteui-theming"]).toEqual({ command: "npx", args: ["-y", "igniteui-theming", "igniteui-theming-mcp"] });
});

it("should not modify .vscode/mcp.json if both servers are already present", async () => {
const existing = {
servers: {
"igniteui": { command: "npx", args: ["-y", "igniteui-cli@next", "mcp"] },
"igniteui-theming": { command: "npx", args: ["-y", "igniteui-theming", "igniteui-theming-mcp"] }
}
};
tree.create(mcpFilePath, JSON.stringify(existing));

await runner.runSchematic("cli-config", {}, tree);

const content = JSON.parse(tree.readContent(mcpFilePath));
expect(content).toEqual(existing);
});

it("should preserve existing servers when adding igniteui servers", async () => {
tree.create(mcpFilePath, JSON.stringify({
servers: {
"other-server": { command: "node", args: ["server.js"] }
}
}));

await runner.runSchematic("cli-config", {}, tree);

const content = JSON.parse(tree.readContent(mcpFilePath));
expect(content.servers["other-server"]).toEqual({ command: "node", args: ["server.js"] });
expect(content.servers["igniteui"]).toBeDefined();
expect(content.servers["igniteui-theming"]).toBeDefined();
});
});
});
2 changes: 2 additions & 0 deletions packages/ng-schematics/src/ng-new/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { defer, Observable } from "rxjs";
import { NewProjectOptions } from "../app-projects/schema";
import { SchematicsPromptSession } from "../prompt/SchematicsPromptSession";
import { SchematicsTemplateManager } from "../SchematicsTemplateManager";
import { addAIConfig } from "../cli-config/index";
import { setVirtual } from "../utils/NgFileSystem";
import { OptionsSchema } from "./schema";

Expand Down Expand Up @@ -150,6 +151,7 @@ export function newProject(options: OptionsSchema): Rule {
});
}
},
addAIConfig(),
(_tree: Tree, _context: IgxSchematicContext) => {
return move(options.name!);
}
Expand Down
34 changes: 32 additions & 2 deletions packages/ng-schematics/src/ng-new/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe("Schematics ng-new", () => {
expect(mockFunc[1]).toHaveBeenCalled();
}
expect(AppProjectSchematic.default).toHaveBeenCalled();
expect(e.files.length).toEqual(1);
expect(e.files.length).toEqual(2);
expect(e.exists(`${workingDirectory}/.gitignore`)).toBeTruthy();
const taskOptions = runner.tasks.map(task => task.options);
const expectedInstall: NodePackageTaskOptions = {
Expand Down Expand Up @@ -189,7 +189,7 @@ describe("Schematics ng-new", () => {
runner.runSchematic("ng-new", { version: "8.0.3", name: workingDirectory }, myTree)
.then((e: UnitTestTree) => {
expect(AppProjectSchematic.default).toHaveBeenCalled();
expect(e.files.length).toEqual(1);
expect(e.files.length).toEqual(2);
expect(e.exists(`${workingDirectory}/.gitignore`)).toBeTruthy();
const taskOptions = runner.tasks.map(task => task.options);
const expectedInstall: NodePackageTaskOptions = {
Expand All @@ -212,4 +212,34 @@ describe("Schematics ng-new", () => {
expect(taskOptions).toContain(expectedInit);
});
});

describe("addAIConfig via ng-new", () => {
const workingDirectory = "my-test-project";
const mcpFilePath = `${workingDirectory}/.vscode/mcp.json`;

function setupAndRun(runner: SchematicTestRunner, myTree: Tree): Promise<UnitTestTree> {
spyOn(AppProjectSchematic, "default").and.returnValue((currentTree: Tree, _context: SchematicContext) => {
currentTree.create("gitignore", "");
return currentTree;
});

const userAnswers = new Map<string, any>();
userAnswers.set("upgradePackages", false);
spyOnProperty(SchematicsPromptSession.prototype, "userAnswers", "get").and.returnValue(userAnswers);

return runner.runSchematic("ng-new", { version: "8.0.3", name: workingDirectory, skipInstall: true, skipGit: true }, myTree);
}

it("should create .vscode/mcp.json with both servers during ng-new", async () => {
const runner = new SchematicTestRunner("schematics", collectionPath);
const myTree = Tree.empty();

const e = await setupAndRun(runner, myTree);

expect(e.exists(mcpFilePath)).toBeTruthy();
const content = JSON.parse(e.readContent(mcpFilePath));
expect(content.servers["igniteui"]).toEqual({ command: "npx", args: ["-y", "igniteui-cli@next", "mcp"] });
expect(content.servers["igniteui-theming"]).toEqual({ command: "npx", args: ["-y", "igniteui-theming", "igniteui-theming-mcp"] });
});
});
});
Loading