Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Loading