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
7 changes: 5 additions & 2 deletions packages/angular/cli/src/commands/mcp/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
*/

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module';
import type { Argv } from 'yargs';
import {
CommandModule,
type CommandModuleImplementation,
} from '../../command-builder/command-module';
import { isTTY } from '../../utilities/tty';
import { EXPERIMENTAL_TOOLS, createMcpServer } from './mcp-server';

Expand Down
4 changes: 2 additions & 2 deletions packages/angular/cli/src/commands/mcp/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { ChildProcess } from 'child_process';
import { Host } from './host';
import type { ChildProcess } from 'child_process';
import type { Host } from './host';

// Log messages that we want to catch to identify the build status.

Expand Down
8 changes: 4 additions & 4 deletions packages/angular/cli/src/commands/mcp/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
*/

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import path from 'node:path';
import { join } from 'node:path';
import type { AngularWorkspace } from '../../utilities/config';
import { VERSION } from '../../utilities/version';
import { DevServer } from './dev-server';
import type { DevServer } from './dev-server';
import { registerInstructionsResource } from './resources/instructions';
import { AI_TUTOR_TOOL } from './tools/ai-tutor';
import { BEST_PRACTICES_TOOL } from './tools/best-practices';
Expand All @@ -23,7 +23,7 @@ import { FIND_EXAMPLE_TOOL } from './tools/examples';
import { MODERNIZE_TOOL } from './tools/modernize';
import { ZONELESS_MIGRATION_TOOL } from './tools/onpush-zoneless-migration/zoneless-migration';
import { LIST_PROJECTS_TOOL } from './tools/projects';
import { AnyMcpToolDeclaration, registerTools } from './tools/tool-registry';
import { type AnyMcpToolDeclaration, registerTools } from './tools/tool-registry';

/**
* Tools to manage devservers. Should be bundled together, then added to experimental or stable as a group.
Expand Down Expand Up @@ -113,7 +113,7 @@ equivalent actions.
{
workspace: options.workspace,
logger,
exampleDatabasePath: path.join(__dirname, '../../../lib/code-examples.db'),
exampleDatabasePath: join(__dirname, '../../../lib/code-examples.db'),
devServers: new Map<string, DevServer>(),
},
toolDeclarations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { join } from 'node:path';

export function registerInstructionsResource(server: McpServer): void {
server.registerResource(
Expand All @@ -24,7 +24,7 @@ export function registerInstructionsResource(server: McpServer): void {
mimeType: 'text/markdown',
},
async () => {
const text = await readFile(path.join(__dirname, 'best-practices.md'), 'utf-8');
const text = await readFile(join(__dirname, 'best-practices.md'), 'utf-8');

return { contents: [{ uri: 'instructions://best-practices', text }] };
},
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/cli/src/commands/mcp/testing/mock-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { Host } from '../host';
import type { Host } from '../host';

/**
* A mock implementation of the `Host` interface for testing purposes.
Expand Down
7 changes: 2 additions & 5 deletions packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { join } from 'node:path';
import { declareTool } from './tool-registry';

export const AI_TUTOR_TOOL = declareTool({
Expand Down Expand Up @@ -40,10 +40,7 @@ with a new core identity and knowledge base.
let aiTutorText: string;

return async () => {
aiTutorText ??= await readFile(
path.join(__dirname, '..', 'resources', 'ai-tutor.md'),
'utf-8',
);
aiTutorText ??= await readFile(join(__dirname, '../resources/ai-tutor.md'), 'utf-8');

return {
content: [
Expand Down
14 changes: 7 additions & 7 deletions packages/angular/cli/src/commands/mcp/tools/best-practices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@

import { readFile, stat } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
import { z } from 'zod';
import { VERSION } from '../../../utilities/version';
import { McpToolContext, declareTool } from './tool-registry';
import { type McpToolContext, declareTool } from './tool-registry';

const bestPracticesInputSchema = z.object({
workspacePath: z
Expand Down Expand Up @@ -72,7 +72,7 @@ that **MUST** be followed for any task involving the creation, analysis, or modi
* @returns A promise that resolves to the string content of the bundled markdown file.
*/
async function getBundledBestPractices(): Promise<string> {
return readFile(path.join(__dirname, '..', 'resources', 'best-practices.md'), 'utf-8');
return readFile(join(__dirname, '../resources/best-practices.md'), 'utf-8');
}

/**
Expand Down Expand Up @@ -126,14 +126,14 @@ async function getVersionSpecificBestPractices(
bestPracticesInfo.format === 'markdown' &&
typeof bestPracticesInfo.path === 'string'
) {
const packageDirectory = path.dirname(pkgJsonPath);
const guidePath = path.resolve(packageDirectory, bestPracticesInfo.path);
const packageDirectory = dirname(pkgJsonPath);
const guidePath = resolve(packageDirectory, bestPracticesInfo.path);

// Ensure the resolved guide path is within the package boundary.
// Uses path.relative to create a cross-platform, case-insensitive check.
// If the relative path starts with '..' or is absolute, it is a traversal attempt.
const relativePath = path.relative(packageDirectory, guidePath);
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
const relativePath = relative(packageDirectory, guidePath);
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
logger.warn(
`Detected a potential path traversal attempt in '${pkgJsonPath}'. ` +
`The path '${bestPracticesInfo.path}' escapes the package boundary. ` +
Expand Down
4 changes: 2 additions & 2 deletions packages/angular/cli/src/commands/mcp/tools/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
*/

import { z } from 'zod';
import { CommandError, Host, LocalWorkspaceHost } from '../host';
import { CommandError, type Host, LocalWorkspaceHost } from '../host';
import { createStructuredContentOutput } from '../utils';
import { McpToolDeclaration, declareTool } from './tool-registry';
import { type McpToolDeclaration, declareTool } from './tool-registry';

const DEFAULT_CONFIGURATION = 'development';

Expand Down
4 changes: 2 additions & 2 deletions packages/angular/cli/src/commands/mcp/tools/build_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { CommandError, Host } from '../host';
import { MockHost } from '../testing/mock-host';
import type { MockHost } from '../testing/mock-host';
import { runBuild } from './build';

describe('Build Tool', () => {
Expand All @@ -18,7 +18,7 @@ describe('Build Tool', () => {
runCommand: jasmine.createSpy<Host['runCommand']>('runCommand').and.resolveTo({ logs: [] }),
stat: jasmine.createSpy<Host['stat']>('stat'),
existsSync: jasmine.createSpy<Host['existsSync']>('existsSync'),
} as Partial<MockHost> as MockHost;
} as MockHost;
});

it('should construct the command correctly with default configuration', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
*/

import { EventEmitter } from 'events';
import { ChildProcess } from 'node:child_process';
import { MockHost } from '../../testing/mock-host';
import { McpToolContext } from '../tool-registry';
import type { ChildProcess } from 'node:child_process';
import type { MockHost } from '../../testing/mock-host';
import type { McpToolContext } from '../tool-registry';
import { startDevServer } from './start-devserver';
import { stopDevserver } from './stop-devserver';
import { WATCH_DELAY, waitForDevserverBuild } from './wait-for-devserver-build';
Expand All @@ -34,7 +34,7 @@ describe('Serve Tools', () => {
getAvailablePort: jasmine.createSpy('getAvailablePort').and.callFake(() => {
return Promise.resolve(portCounter++);
}),
} as Partial<MockHost> as MockHost;
} as MockHost;

mockContext = {
devServers: new Map(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

import { z } from 'zod';
import { LocalDevServer, devServerKey } from '../../dev-server';
import { Host, LocalWorkspaceHost } from '../../host';
import { type Host, LocalWorkspaceHost } from '../../host';
import { createStructuredContentOutput } from '../../utils';
import { McpToolContext, McpToolDeclaration, declareTool } from '../tool-registry';
import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry';

const startDevServerToolInputSchema = z.object({
project: z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { z } from 'zod';
import { devServerKey } from '../../dev-server';
import { createStructuredContentOutput } from '../../utils';
import { McpToolContext, McpToolDeclaration, declareTool } from '../tool-registry';
import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry';

const stopDevserverToolInputSchema = z.object({
project: z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { z } from 'zod';
import { devServerKey } from '../../dev-server';
import { createStructuredContentOutput } from '../../utils';
import { McpToolContext, McpToolDeclaration, declareTool } from '../tool-registry';
import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry';

/**
* How long to wait to give "ng serve" time to identify whether the watched workspace has changed.
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/cli/src/commands/mcp/tools/doc-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createDecipheriv } from 'node:crypto';
import { Readable } from 'node:stream';
import { z } from 'zod';
import { at, iv, k1 } from '../constants';
import { McpToolContext, declareTool } from './tool-registry';
import { type McpToolContext, declareTool } from './tool-registry';

const ALGOLIA_APP_ID = 'L1XWT2UJ7F';
// https://www.algolia.com/doc/guides/security/api-keys/#search-only-api-key
Expand Down
14 changes: 7 additions & 7 deletions packages/angular/cli/src/commands/mcp/tools/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

import { glob, readFile, stat } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
import type { DatabaseSync, SQLInputValue } from 'node:sqlite';
import { z } from 'zod';
import { McpToolContext, declareTool } from './tool-registry';
import { type McpToolContext, declareTool } from './tool-registry';

const findExampleInputSchema = z.object({
workspacePath: z
Expand Down Expand Up @@ -246,12 +246,12 @@ async function getVersionSpecificExampleDatabase(
const examplesInfo = pkgJson['angular']?.examples;

if (examplesInfo && examplesInfo.format === 'sqlite' && typeof examplesInfo.path === 'string') {
const packageDirectory = path.dirname(pkgJsonPath);
const dbPath = path.resolve(packageDirectory, examplesInfo.path);
const packageDirectory = dirname(pkgJsonPath);
const dbPath = resolve(packageDirectory, examplesInfo.path);

// Ensure the resolved database path is within the package boundary.
const relativePath = path.relative(packageDirectory, dbPath);
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
const relativePath = relative(packageDirectory, dbPath);
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
logger.warn(
`Detected a potential path traversal attempt in '${pkgJsonPath}'. ` +
`The path '${examplesInfo.path}' escapes the package boundary. ` +
Expand Down Expand Up @@ -634,7 +634,7 @@ async function setupRuntimeExamples(examplesPath: string): Promise<DatabaseSync>
continue;
}

const content = await readFile(path.join(entry.parentPath, entry.name), 'utf-8');
const content = await readFile(join(entry.parentPath, entry.name), 'utf-8');
const frontmatter = parseFrontmatter(content);

const validation = frontmatterSchema.safeParse(frontmatter);
Expand Down
20 changes: 3 additions & 17 deletions packages/angular/cli/src/commands/mcp/tools/modernize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

import { dirname, join, relative } from 'path';
import { z } from 'zod';
import { CommandError, Host, LocalWorkspaceHost } from '../host';
import { createStructuredContentOutput } from '../utils';
import { McpToolDeclaration, declareTool } from './tool-registry';
import { CommandError, type Host, LocalWorkspaceHost } from '../host';
import { createStructuredContentOutput, findAngularJsonDir } from '../utils';
import { type McpToolDeclaration, declareTool } from './tool-registry';

interface Transformation {
name: string;
Expand Down Expand Up @@ -93,20 +93,6 @@ const modernizeOutputSchema = z.object({
export type ModernizeInput = z.infer<typeof modernizeInputSchema>;
export type ModernizeOutput = z.infer<typeof modernizeOutputSchema>;

function findAngularJsonDir(startDir: string, host: Host): string | null {
let currentDir = startDir;
while (true) {
if (host.existsSync(join(currentDir, 'angular.json'))) {
return currentDir;
}
const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
return null;
}
currentDir = parentDir;
}
}

export async function runModernization(input: ModernizeInput, host: Host) {
const transformationNames = input.transformations ?? [];
const directories = input.directories ?? [];
Expand Down
6 changes: 3 additions & 3 deletions packages/angular/cli/src/commands/mcp/tools/modernize_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { CommandError } from '../host';
import { MockHost } from '../testing/mock-host';
import { ModernizeOutput, runModernization } from './modernize';
import type { MockHost } from '../testing/mock-host';
import { type ModernizeOutput, runModernization } from './modernize';

describe('Modernize Tool', () => {
let projectDir: string;
Expand All @@ -29,7 +29,7 @@ describe('Modernize Tool', () => {
existsSync: jasmine.createSpy('existsSync').and.callFake((p: string) => {
return p === join(projectDir, 'angular.json');
}),
} as Partial<MockHost> as MockHost;
} as MockHost;
});

afterEach(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

import type { ImportSpecifier, Node, SourceFile } from 'typescript';
import { createUnsupportedZoneUsagesMessage } from './prompts';
import { getImportSpecifier, loadTypescript } from './ts_utils';
import { MigrationResponse } from './types';
import { getImportSpecifier, loadTypescript } from './ts-utils';
import type { MigrationResponse } from './types';

export async function analyzeForUnsupportedZoneUses(
sourceFile: SourceFile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol';
import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types';
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol';
import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types';
import type { SourceFile } from 'typescript';
import { analyzeForUnsupportedZoneUses } from './analyze_for_unsupported_zone_uses';
import { migrateTestFile } from './migrate_test_file';
import { analyzeForUnsupportedZoneUses } from './analyze-for-unsupported-zone-uses';
import { migrateTestFile } from './migrate-test-file';
import { generateZonelessMigrationInstructionsForComponent } from './prompts';
import { sendDebugMessage } from './send_debug_message';
import { getImportSpecifier, loadTypescript } from './ts_utils';
import { MigrationResponse } from './types';
import { sendDebugMessage } from './send-debug-message';
import { getImportSpecifier, loadTypescript } from './ts-utils';
import type { MigrationResponse } from './types';

export async function migrateSingleFile(
sourceFile: SourceFile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol';
import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types';
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol';
import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types';
import ts from 'typescript';
import { migrateSingleFile } from './migrate_single_file';
import { migrateSingleFile } from './migrate-single-file';

const fakeExtras = {
sendDebugMessage: jasmine.createSpy(),
Expand Down
Loading