-
Notifications
You must be signed in to change notification settings - Fork 12k
feat(@angular/cli): add modernize tool to the MCP server #30845
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
32 changes: 32 additions & 0 deletions
32
packages/angular/cli/src/commands/mcp/resources/instructions.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.dev/license | ||
*/ | ||
|
||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
import { readFile } from 'node:fs/promises'; | ||
import path from 'node:path'; | ||
|
||
export function registerInstructionsResource(server: McpServer): void { | ||
server.registerResource( | ||
'instructions', | ||
'instructions://best-practices', | ||
{ | ||
title: 'Angular Best Practices and Code Generation Guide', | ||
description: | ||
"A comprehensive guide detailing Angular's best practices for code generation and development." + | ||
' This guide should be used as a reference by an LLM to ensure any generated code' + | ||
' adheres to modern Angular standards, including the use of standalone components,' + | ||
' typed forms, modern control flow syntax, and other current conventions.', | ||
mimeType: 'text/markdown', | ||
}, | ||
async () => { | ||
const text = await readFile(path.join(__dirname, 'best-practices.md'), 'utf-8'); | ||
|
||
return { contents: [{ uri: 'instructions://best-practices', text }] }; | ||
}, | ||
); | ||
} |
162 changes: 162 additions & 0 deletions
162
packages/angular/cli/src/commands/mcp/tools/modernize.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.dev/license | ||
*/ | ||
|
||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
import { z } from 'zod'; | ||
|
||
interface Transformation { | ||
name: string; | ||
description: string; | ||
documentationUrl: string; | ||
instructions?: string; | ||
} | ||
|
||
const TRANSFORMATIONS: Array<Transformation> = [ | ||
{ | ||
name: 'control-flow-migration', | ||
description: | ||
'Migrates from `*ngIf`, `*ngFor`, and `*ngSwitch` to the new `@if`, `@for`, and `@switch` block syntax in templates.', | ||
documentationUrl: 'https://angular.dev/reference/migrations/control-flow', | ||
}, | ||
{ | ||
name: 'self-closing-tags-migration', | ||
description: | ||
'Converts tags for elements with no content to be self-closing (e.g., `<app-foo></app-foo>` becomes `<app-foo />`).', | ||
documentationUrl: 'https://angular.dev/reference/migrations/self-closing-tags', | ||
}, | ||
{ | ||
name: 'test-bed-get', | ||
description: | ||
'Updates `TestBed.get` to the preferred and type-safe `TestBed.inject` in TypeScript test files.', | ||
documentationUrl: 'https://angular.dev/guide/testing/dependency-injection', | ||
}, | ||
{ | ||
name: 'inject-flags', | ||
description: | ||
'Updates `inject` calls from using the InjectFlags enum to a more modern and readable options object.', | ||
documentationUrl: 'https://angular.dev/reference/migrations/inject-function', | ||
}, | ||
{ | ||
name: 'output-migration', | ||
description: 'Converts `@Output` declarations to the new functional `output()` syntax.', | ||
documentationUrl: 'https://angular.dev/reference/migrations/outputs', | ||
}, | ||
{ | ||
name: 'signal-input-migration', | ||
description: 'Migrates `@Input` declarations to the new signal-based `input()` syntax.', | ||
documentationUrl: 'https://angular.dev/reference/migrations/signal-inputs', | ||
}, | ||
{ | ||
name: 'signal-queries-migration', | ||
description: | ||
'Migrates `@ViewChild` and `@ContentChild` queries to their signal-based `viewChild` and `contentChild` versions.', | ||
documentationUrl: 'https://angular.dev/reference/migrations/signal-queries', | ||
}, | ||
{ | ||
name: 'standalone', | ||
description: | ||
'Converts the application to use standalone components, directives, and pipes. This is a ' + | ||
'three-step process. After each step, you should verify that your application builds and ' + | ||
'runs correctly.', | ||
instructions: | ||
'This migration requires running a cli schematic multiple times. Run the commands in the ' + | ||
'order listed below, verifying that your code builds and runs between each step:\n\n' + | ||
'1. Run `ng g @angular/core:standalone` and select "Convert all components, directives and pipes to standalone"\n' + | ||
'2. Run `ng g @angular/core:standalone` and select "Remove unnecessary NgModule classes"\n' + | ||
'3. Run `ng g @angular/core:standalone` and select "Bootstrap the project using standalone APIs"', | ||
documentationUrl: 'https://angular.dev/reference/migrations/standalone', | ||
}, | ||
{ | ||
name: 'zoneless', | ||
description: 'Migrates the application to be zoneless.', | ||
documentationUrl: 'https://angular.dev/guide/zoneless', | ||
}, | ||
]; | ||
|
||
const modernizeInputSchema = z.object({ | ||
// Casting to [string, ...string[]] since the enum definition requires a nonempty array. | ||
transformations: z | ||
.array(z.enum(TRANSFORMATIONS.map((t) => t.name) as [string, ...string[]])) | ||
.optional(), | ||
}); | ||
|
||
export type ModernizeInput = z.infer<typeof modernizeInputSchema>; | ||
|
||
function generateInstructions(transformationNames: string[]): string[] { | ||
if (transformationNames.length === 0) { | ||
return [ | ||
'See https://angular.dev/best-practices for Angular best practices. ' + | ||
'You can call this tool if you have specific transformation you want to run.', | ||
]; | ||
} | ||
|
||
const instructions: string[] = []; | ||
const transformationsToRun = TRANSFORMATIONS.filter((t) => transformationNames?.includes(t.name)); | ||
|
||
for (const transformation of transformationsToRun) { | ||
let transformationInstructions = ''; | ||
if (transformation.instructions) { | ||
transformationInstructions = transformation.instructions; | ||
} else { | ||
// If no instructions are included, default to running a cli schematic with the transformation name. | ||
const command = `ng generate @angular/core:${transformation.name}`; | ||
transformationInstructions = `To run the ${transformation.name} migration, execute the following command: \`${command}\`.`; | ||
} | ||
if (transformation.documentationUrl) { | ||
transformationInstructions += `\nFor more information, see ${transformation.documentationUrl}.`; | ||
} | ||
instructions.push(transformationInstructions); | ||
} | ||
|
||
return instructions; | ||
} | ||
|
||
export async function runModernization(input: ModernizeInput) { | ||
const structuredContent = { instructions: generateInstructions(input.transformations ?? []) }; | ||
|
||
return { | ||
content: [{ type: 'text' as const, text: JSON.stringify(structuredContent) }], | ||
structuredContent, | ||
}; | ||
} | ||
|
||
export function registerModernizeTool(server: McpServer): void { | ||
server.registerTool( | ||
'modernize', | ||
{ | ||
title: 'Modernize Angular Code', | ||
description: | ||
'<Purpose>\n' + | ||
'This tool modernizes Angular code by applying the latest best practices and syntax improvements, ' + | ||
'ensuring it is idiomatic, readable, and maintainable.\n\n' + | ||
'</Purpose>\n' + | ||
'<Use Cases>\n' + | ||
'* After generating new code: Run this tool immediately after creating new Angular components, directives, ' + | ||
'or services to ensure they adhere to modern standards.\n' + | ||
'* On existing code: Apply to existing TypeScript files (.ts) and Angular templates (.ng.html) to update ' + | ||
'them with the latest features, such as the new built-in control flow syntax.\n\n' + | ||
'* When the user asks for a specific transformation: When the transformation list is populated, ' + | ||
'these specific ones will be ran on the inputs.\n' + | ||
'</Use Cases>\n' + | ||
'<Transformations>\n' + | ||
TRANSFORMATIONS.map((t) => `* ${t.name}: ${t.description}`).join('\n') + | ||
'\n</Transformations>\n', | ||
annotations: { | ||
readOnlyHint: true, | ||
}, | ||
inputSchema: modernizeInputSchema.shape, | ||
outputSchema: { | ||
instructions: z | ||
.array(z.string()) | ||
.optional() | ||
.describe('A list of instructions on how to run the migrations.'), | ||
}, | ||
}, | ||
(input) => runModernization(input), | ||
); | ||
} |
73 changes: 73 additions & 0 deletions
73
packages/angular/cli/src/commands/mcp/tools/modernize_spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.dev/license | ||
*/ | ||
|
||
import { ModernizeInput, runModernization } from './modernize'; | ||
|
||
describe('Modernize Tool', () => { | ||
async function getInstructions(input: ModernizeInput): Promise<string[] | undefined> { | ||
const { structuredContent } = await runModernization(input); | ||
|
||
if (!structuredContent || !('instructions' in structuredContent)) { | ||
fail('Expected instructions to be present in the result'); | ||
|
||
return; | ||
} | ||
|
||
return structuredContent.instructions; | ||
} | ||
|
||
it('should return an instruction for a single transformation', async () => { | ||
const instructions = await getInstructions({ | ||
transformations: ['self-closing-tags-migration'], | ||
}); | ||
|
||
expect(instructions).toEqual([ | ||
'To run the self-closing-tags-migration migration, execute the following command: ' + | ||
'`ng generate @angular/core:self-closing-tags-migration`.\nFor more information, ' + | ||
'see https://angular.dev/reference/migrations/self-closing-tags.', | ||
]); | ||
}); | ||
|
||
it('should return instructions for multiple transformations', async () => { | ||
const instructions = await getInstructions({ | ||
transformations: ['self-closing-tags-migration', 'test-bed-get'], | ||
}); | ||
|
||
const expectedInstructions = [ | ||
'To run the self-closing-tags-migration migration, execute the following command: ' + | ||
'`ng generate @angular/core:self-closing-tags-migration`.\nFor more information, ' + | ||
'see https://angular.dev/reference/migrations/self-closing-tags.', | ||
'To run the test-bed-get migration, execute the following command: ' + | ||
'`ng generate @angular/core:test-bed-get`.\nFor more information, ' + | ||
'see https://angular.dev/guide/testing/dependency-injection.', | ||
]; | ||
|
||
expect(instructions?.sort()).toEqual(expectedInstructions.sort()); | ||
}); | ||
|
||
it('should return a link to the best practices page when no transformations are requested', async () => { | ||
const instructions = await getInstructions({ | ||
transformations: [], | ||
}); | ||
|
||
expect(instructions).toEqual([ | ||
'See https://angular.dev/best-practices for Angular best practices. You can call this ' + | ||
'tool if you have specific transformation you want to run.', | ||
]); | ||
}); | ||
|
||
it('should return special instructions for standalone migration', async () => { | ||
const instructions = await getInstructions({ | ||
transformations: ['standalone'], | ||
}); | ||
|
||
expect(instructions?.[0]).toContain( | ||
'Run the commands in the order listed below, verifying that your code builds and runs between each step:', | ||
); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.