Skip to content

Commit 37cb2af

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 617dd4e commit 37cb2af

File tree

11 files changed

+375
-157
lines changed

11 files changed

+375
-157
lines changed

packages/schematics/angular/BUILD.bazel

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,9 @@ genrule(
5050
srcs = [
5151
"//:node_modules/@angular/core/dir",
5252
],
53-
outs = ["ai-config/files/__rulesName__.template"],
53+
outs = ["ai-config/files/__bestPracticesName__.template"],
5454
cmd = """
55-
echo -e "<% if (frontmatter) { %><%= frontmatter %>\\n<% } %>" > $@
56-
cat "$(location //:node_modules/@angular/core/dir)/resources/best-practices.md" >> $@
55+
cp "$(location //:node_modules/@angular/core/dir)/resources/best-practices.md" $@
5756
""",
5857
)
5958

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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 { FileConfigurationHandlerOptions } from './types';
22+
23+
const TOML_MCP_SERVERS_PROP = '[mcp_servers.angular-cli]';
24+
25+
/**
26+
* Create or update a JSON MCP configuration file to include the Angular MCP server.
27+
*/
28+
export function addJsonMcpConfig(
29+
{ tree, context, fileInfo, tool }: FileConfigurationHandlerOptions,
30+
mcpServersProperty: string,
31+
): Rule {
32+
const { name, directory } = fileInfo;
33+
34+
return mergeWith(
35+
apply(url('./files'), [
36+
filter((path) => path.includes('__jsonConfigName__')),
37+
applyTemplates({
38+
...strings,
39+
jsonConfigName: name,
40+
mcpServersProperty,
41+
}),
42+
move(directory),
43+
forEach((file) => {
44+
if (!tree.exists(file.path)) {
45+
return file;
46+
}
47+
48+
const existingFileBuffer = tree.read(file.path);
49+
50+
// If we have an existing file, update the server property with
51+
// Angular MCP server configuration.
52+
if (existingFileBuffer) {
53+
// The JSON config file should be record-like.
54+
let existing: Record<string, unknown>;
55+
try {
56+
existing = JSON.parse(existingFileBuffer.toString());
57+
} catch {
58+
const path = `${directory}/${name}`;
59+
const toolName = strings.classify(tool);
60+
context.logger.warn(
61+
`Skipping Angular MCP server configuration for '${toolName}'.\n` +
62+
`Unable to modify '${path}'. ` +
63+
'Make sure that the file has a valid JSON syntax.\n',
64+
);
65+
66+
return null;
67+
}
68+
const existingServersProp = existing[mcpServersProperty];
69+
const templateServersProp = JSON.parse(file.content.toString())[mcpServersProperty];
70+
71+
// Note: If the Angular MCP server config already exists, we'll overwrite it.
72+
existing[mcpServersProperty] = existingServersProp
73+
? {
74+
...existingServersProp,
75+
...templateServersProp,
76+
}
77+
: templateServersProp;
78+
79+
tree.overwrite(file.path, JSON.stringify(existing, null, 2));
80+
81+
return null;
82+
}
83+
84+
return file;
85+
}),
86+
]),
87+
);
88+
}
89+
90+
/**
91+
* Create or update a TOML MCP configuration file to include the Angular MCP server.
92+
*/
93+
export function addTomlMcpConfig({
94+
tree,
95+
context,
96+
fileInfo,
97+
tool,
98+
}: FileConfigurationHandlerOptions): Rule {
99+
const { name, directory } = fileInfo;
100+
101+
return mergeWith(
102+
apply(url('./files'), [
103+
filter((path) => path.includes('__tomlConfigName__')),
104+
applyTemplates({
105+
...strings,
106+
tomlConfigName: name,
107+
}),
108+
move(directory),
109+
forEach((file) => {
110+
if (!tree.exists(file.path)) {
111+
return file;
112+
}
113+
114+
const existingFileBuffer = tree.read(file.path);
115+
116+
if (existingFileBuffer) {
117+
let existing = existingFileBuffer.toString();
118+
if (existing.includes(TOML_MCP_SERVERS_PROP)) {
119+
const path = `${directory}/${name}`;
120+
const toolName = strings.classify(tool);
121+
context.logger.warn(
122+
`Skipping Angular MCP server configuration for '${toolName}'.\n` +
123+
`Configuration already exists in '${path}'.\n`,
124+
);
125+
126+
return null;
127+
}
128+
129+
// Add the configuration at the end of the file.
130+
const template = file.content.toString();
131+
existing = existing.length ? existing + '\n' + template : template;
132+
133+
tree.overwrite(file.path, existing);
134+
135+
return null;
136+
}
137+
138+
return file;
139+
}),
140+
]),
141+
);
142+
}
143+
144+
/**
145+
* Create an Angular best practices Markdown.
146+
* If the file exists, the configuration is skipped.
147+
*/
148+
export function addBestPracticesMarkdown({
149+
tree,
150+
context,
151+
fileInfo,
152+
tool,
153+
}: FileConfigurationHandlerOptions): Rule {
154+
const { name, directory } = fileInfo;
155+
const path = `${directory}/${name}`;
156+
157+
if (tree.exists(path)) {
158+
const toolName = strings.classify(tool);
159+
context.logger.warn(
160+
`Skipping configuration file for '${toolName}' at '${path}' because it already exists.\n` +
161+
'This is to prevent overwriting a potentially customized file. ' +
162+
'If you want to regenerate it with Angular recommended defaults, please delete the existing file and re-run the command.\n' +
163+
'You can review the latest recommendations at https://angular.dev/ai/develop-with-ai.\n',
164+
);
165+
166+
return noop();
167+
}
168+
169+
return mergeWith(
170+
apply(url('./files'), [
171+
filter((path) => path.includes('__bestPracticesName__')),
172+
applyTemplates({
173+
...strings,
174+
bestPracticesName: name,
175+
}),
176+
move(directory),
177+
]),
178+
);
179+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
// TBD
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)