Skip to content

Commit 0677e32

Browse files
committed
feat(@schematics/angular): update ai-config to include Angular MCP server config
Update the `ai-config` schematic, which is activated during workspace creation, to enable Angular MCP server by default.
1 parent 60481e9 commit 0677e32

11 files changed

Lines changed: 415 additions & 148 deletions

File tree

BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ exports_files([
1515
"tsconfig-build-ng.json",
1616
"tsconfig-build.json",
1717
"package.json",
18+
"node_modules/@angular/core/resources/best-practices.md",
1819
])
1920

2021
npm_link_all_packages()

packages/schematics/angular/BUILD.bazel

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Use of this source code is governed by an MIT-style license that can be
44
# found in the LICENSE file at https://angular.dev/license
55

6+
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
67
load("@npm//:defs.bzl", "npm_link_all_packages")
78
load("//tools:defaults.bzl", "copy_to_bin", "jasmine_test", "npm_package", "ts_project")
89
load("//tools:ts_json_schema.bzl", "ts_json_schema")
@@ -45,16 +46,10 @@ copy_to_bin(
4546
srcs = glob(["**/schema.json"]),
4647
)
4748

48-
genrule(
49+
copy_file(
4950
name = "angular_best_practices",
50-
srcs = [
51-
"//:node_modules/@angular/core/dir",
52-
],
53-
outs = ["ai-config/files/__rulesName__.template"],
54-
cmd = """
55-
echo -e "<% if (frontmatter) { %><%= frontmatter %>\\n<% } %>" > $@
56-
cat "$(location //:node_modules/@angular/core/dir)/resources/best-practices.md" >> $@
57-
""",
51+
src = "//:node_modules/@angular/core/resources/best-practices.md",
52+
out = "ai-config/files/__bestPracticesName__.template",
5853
)
5954

6055
RUNTIME_ASSETS = [
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
Rule,
11+
apply,
12+
applyTemplates,
13+
filter,
14+
forEach,
15+
mergeWith,
16+
move,
17+
noop,
18+
strings,
19+
url,
20+
} from '@angular-devkit/schematics';
21+
import { parse } from 'jsonc-parser';
22+
import { JSONFile } from '../utility/json-file';
23+
import { FileConfigurationHandlerOptions } from './types';
24+
25+
const TOML_MCP_SERVERS_PROP = '[mcp_servers.angular-cli]';
26+
27+
/**
28+
* Create or update a JSON MCP configuration file to include the Angular MCP server.
29+
*/
30+
export function addJsonMcpConfig(
31+
{ tree, fileInfo }: FileConfigurationHandlerOptions,
32+
mcpServersProperty: string,
33+
): Rule {
34+
const { name, directory } = fileInfo;
35+
36+
return mergeWith(
37+
apply(url('./files'), [
38+
filter((path) => path.includes('__jsonConfigName__')),
39+
applyTemplates({
40+
...strings,
41+
jsonConfigName: name,
42+
mcpServersProperty,
43+
}),
44+
move(directory),
45+
forEach((file) => {
46+
if (!tree.exists(file.path)) {
47+
return file;
48+
}
49+
50+
// If we have an existing file, update the server property with
51+
// Angular MCP server configuration.
52+
const existingConfig = new JSONFile(tree, file.path);
53+
const existingMcpServers = existingConfig.get([mcpServersProperty]) ?? {};
54+
const templateServersProp = parse(file.content.toString())[mcpServersProperty];
55+
56+
existingConfig.modify([mcpServersProperty], {
57+
...existingMcpServers,
58+
...templateServersProp,
59+
});
60+
61+
return null;
62+
}),
63+
]),
64+
);
65+
}
66+
67+
/**
68+
* Create or update a TOML MCP configuration file to include the Angular MCP server.
69+
*/
70+
export function addTomlMcpConfig({
71+
tree,
72+
context,
73+
fileInfo,
74+
tool,
75+
}: FileConfigurationHandlerOptions): Rule {
76+
const { name, directory } = fileInfo;
77+
78+
return mergeWith(
79+
apply(url('./files'), [
80+
filter((path) => path.includes('__tomlConfigName__')),
81+
applyTemplates({
82+
...strings,
83+
tomlConfigName: name,
84+
}),
85+
move(directory),
86+
forEach((file) => {
87+
if (!tree.exists(file.path)) {
88+
return file;
89+
}
90+
91+
const existingFileBuffer = tree.read(file.path);
92+
93+
if (existingFileBuffer) {
94+
let existing = existingFileBuffer.toString();
95+
if (existing.includes(TOML_MCP_SERVERS_PROP)) {
96+
const path = `${directory}/${name}`;
97+
const toolName = strings.classify(tool);
98+
context.logger.warn(
99+
`Skipping Angular MCP server configuration for '${toolName}'.\n` +
100+
`Configuration already exists in '${path}'.\n`,
101+
);
102+
103+
return null;
104+
}
105+
106+
// Add the configuration at the end of the file.
107+
const template = file.content.toString();
108+
existing = existing.length ? existing + '\n\n' + template : template;
109+
110+
tree.overwrite(file.path, existing);
111+
112+
return null;
113+
}
114+
115+
return file;
116+
}),
117+
]),
118+
);
119+
}
120+
121+
/**
122+
* Create an Angular best practices Markdown.
123+
* If the file exists, the configuration is skipped.
124+
*/
125+
export function addBestPracticesMarkdown({
126+
tree,
127+
context,
128+
fileInfo,
129+
tool,
130+
}: FileConfigurationHandlerOptions): Rule {
131+
const { name, directory } = fileInfo;
132+
const path = `${directory}/${name}`;
133+
134+
if (tree.exists(path)) {
135+
const toolName = strings.classify(tool);
136+
context.logger.warn(
137+
`Skipping configuration file for '${toolName}' at '${path}' because it already exists.\n` +
138+
'This is to prevent overwriting a potentially customized file. ' +
139+
'If you want to regenerate it with Angular recommended defaults, please delete the existing file and re-run the command.\n' +
140+
'You can review the latest recommendations at https://angular.dev/ai/develop-with-ai.\n',
141+
);
142+
143+
return noop();
144+
}
145+
146+
return mergeWith(
147+
apply(url('./files'), [
148+
filter((path) => path.includes('__bestPracticesName__')),
149+
applyTemplates({
150+
...strings,
151+
bestPracticesName: name,
152+
}),
153+
move(directory),
154+
]),
155+
);
156+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"<%= mcpServersProperty %>": {
3+
"angular-cli": {
4+
"command": "npx",
5+
"args": ["-y", "@angular/cli", "mcp"]
6+
}
7+
}
8+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[mcp_servers.angular-cli]
2+
command = "npx"
3+
args = ["-y", "@angular/cli", "mcp"]

packages/schematics/angular/ai-config/index.ts

Lines changed: 82 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -6,57 +6,63 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {
10-
Rule,
11-
apply,
12-
applyTemplates,
13-
chain,
14-
mergeWith,
15-
move,
16-
noop,
17-
strings,
18-
url,
19-
} from '@angular-devkit/schematics';
9+
import { Rule, chain, noop, strings } from '@angular-devkit/schematics';
10+
import { addBestPracticesMarkdown, addJsonMcpConfig, addTomlMcpConfig } from './file_utils';
2011
import { Schema as ConfigOptions, Tool } from './schema';
12+
import { ContextFileInfo, ContextFileType, FileConfigurationHandlerOptions } from './types';
2113

22-
const AI_TOOLS: { [key in Exclude<Tool, Tool.None>]: ContextFileInfo } = {
23-
agents: {
24-
rulesName: 'AGENTS.md',
25-
directory: '.',
26-
},
27-
gemini: {
28-
rulesName: 'GEMINI.md',
29-
directory: '.gemini',
30-
},
31-
claude: {
32-
rulesName: 'CLAUDE.md',
33-
directory: '.claude',
34-
},
35-
copilot: {
36-
rulesName: 'copilot-instructions.md',
37-
directory: '.github',
38-
},
39-
windsurf: {
40-
rulesName: 'guidelines.md',
41-
directory: '.windsurf/rules',
42-
},
43-
jetbrains: {
44-
rulesName: 'guidelines.md',
45-
directory: '.junie',
46-
},
47-
// Cursor file has a front matter section.
48-
cursor: {
49-
rulesName: 'cursor.mdc',
50-
directory: '.cursor/rules',
51-
frontmatter: `---\ncontext: true\npriority: high\nscope: project\n---`,
52-
},
14+
const AGENTS_MD_CFG: ContextFileInfo = {
15+
type: ContextFileType.BestPracticesMd,
16+
name: 'AGENTS.md',
17+
directory: '.',
5318
};
5419

55-
interface ContextFileInfo {
56-
rulesName: string;
57-
directory: string;
58-
frontmatter?: string;
59-
}
20+
const AI_TOOLS: { [key in Exclude<Tool, Tool.None>]: ContextFileInfo[] } = {
21+
['claude-code']: [
22+
AGENTS_MD_CFG,
23+
{
24+
type: ContextFileType.McpConfig,
25+
name: '.mcp.json',
26+
directory: '.',
27+
},
28+
],
29+
cursor: [
30+
AGENTS_MD_CFG,
31+
{
32+
type: ContextFileType.McpConfig,
33+
name: 'mcp.json',
34+
directory: '.cursor',
35+
},
36+
],
37+
['gemini-cli']: [
38+
{
39+
type: ContextFileType.BestPracticesMd,
40+
name: 'GEMINI.md',
41+
directory: '.gemini',
42+
},
43+
{
44+
type: ContextFileType.McpConfig,
45+
name: 'settings.json',
46+
directory: '.gemini',
47+
},
48+
],
49+
['open-ai-codex']: [
50+
AGENTS_MD_CFG,
51+
{
52+
type: ContextFileType.McpConfig,
53+
name: 'config.toml',
54+
directory: '.codex',
55+
},
56+
],
57+
vscode: [
58+
AGENTS_MD_CFG,
59+
{
60+
type: ContextFileType.McpConfig,
61+
name: 'mcp.json',
62+
directory: '.vscode',
63+
},
64+
],
65+
};
6066

6167
export default function ({ tool }: ConfigOptions): Rule {
6268
return (tree, context) => {
@@ -66,33 +72,36 @@ export default function ({ tool }: ConfigOptions): Rule {
6672

6773
const rules = tool
6874
.filter((tool) => tool !== Tool.None)
69-
.map((selectedTool) => {
70-
const { rulesName, directory, frontmatter } = AI_TOOLS[selectedTool];
71-
const path = `${directory}/${rulesName}`;
72-
73-
if (tree.exists(path)) {
74-
const toolName = strings.classify(selectedTool);
75-
context.logger.warn(
76-
`Skipping configuration file for '${toolName}' at '${path}' because it already exists.\n` +
77-
'This is to prevent overwriting a potentially customized file. ' +
78-
'If you want to regenerate it with Angular recommended defaults, please delete the existing file and re-run the command.\n' +
79-
'You can review the latest recommendations at https://angular.dev/ai/develop-with-ai.',
80-
);
81-
82-
return noop();
83-
}
75+
.flatMap((selectedTool) =>
76+
AI_TOOLS[selectedTool].map((fileInfo) => {
77+
const fileCfgOpts: FileConfigurationHandlerOptions = {
78+
tree,
79+
context,
80+
fileInfo,
81+
tool: selectedTool,
82+
};
8483

85-
return mergeWith(
86-
apply(url('./files'), [
87-
applyTemplates({
88-
...strings,
89-
rulesName,
90-
frontmatter,
91-
}),
92-
move(directory),
93-
]),
94-
);
95-
});
84+
switch (fileInfo.type) {
85+
case ContextFileType.BestPracticesMd:
86+
return addBestPracticesMarkdown(fileCfgOpts);
87+
case ContextFileType.McpConfig:
88+
switch (selectedTool) {
89+
case Tool.ClaudeCode:
90+
case Tool.Cursor:
91+
case Tool.GeminiCli:
92+
return addJsonMcpConfig(fileCfgOpts, 'mcpServers');
93+
case Tool.OpenAiCodex:
94+
return addTomlMcpConfig(fileCfgOpts);
95+
case Tool.Vscode:
96+
return addJsonMcpConfig(fileCfgOpts, 'servers');
97+
default:
98+
throw new Error(
99+
`Unsupported '${strings.classify(selectedTool)}' MCP server configuraiton.`,
100+
);
101+
}
102+
}
103+
}),
104+
);
96105

97106
return chain(rules);
98107
};

0 commit comments

Comments
 (0)