diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel
index a49e30695138..eed9ad7360f1 100644
--- a/packages/angular/cli/BUILD.bazel
+++ b/packages/angular/cli/BUILD.bazel
@@ -4,7 +4,7 @@
# found in the LICENSE file at https://angular.dev/license
load("@npm//:defs.bzl", "npm_link_all_packages")
-load("//tools:defaults.bzl", "jasmine_test", "ng_examples_db", "npm_package", "ts_project")
+load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project")
load("//tools:ng_cli_schema_generator.bzl", "cli_json_schema")
load("//tools:ts_json_schema.bzl", "ts_json_schema")
@@ -36,7 +36,6 @@ RUNTIME_ASSETS = glob(
],
) + [
"//packages/angular/cli:lib/config/schema.json",
- "//packages/angular/cli:lib/code-examples.db",
":angular_best_practices",
]
@@ -87,17 +86,6 @@ ts_project(
],
)
-ng_examples_db(
- name = "cli_example_database",
- srcs = glob(
- include = [
- "lib/examples/**/*.md",
- ],
- ),
- out = "lib/code-examples.db",
- path = "packages/angular/cli/lib/examples",
-)
-
CLI_SCHEMA_DATA = [
"//packages/angular/build:schemas",
"//packages/angular_devkit/build_angular:schemas",
diff --git a/packages/angular/cli/lib/examples/if-block.md b/packages/angular/cli/lib/examples/if-block.md
deleted file mode 100644
index 806e3d05516c..000000000000
--- a/packages/angular/cli/lib/examples/if-block.md
+++ /dev/null
@@ -1,85 +0,0 @@
----
-title: 'Using the @if Built-in Control Flow Block'
-summary: 'Demonstrates how to use the @if built-in control flow block to conditionally render content in an Angular template based on a boolean expression.'
-keywords:
- - '@if'
- - 'control flow'
- - 'conditional rendering'
- - 'template syntax'
-related_concepts:
- - '@else'
- - '@else if'
- - 'signals'
-related_tools:
- - 'modernize'
----
-
-## Purpose
-
-The purpose of this pattern is to create dynamic user interfaces by controlling which elements are rendered to the DOM based on the application's state. This is a fundamental technique for building responsive and interactive components.
-
-## When to Use
-
-Use the `@if` block as the modern, preferred alternative to the `*ngIf` directive for all conditional rendering. It offers better type-checking and a cleaner, more intuitive syntax within the template.
-
-## Key Concepts
-
-- **`@if` block:** The primary syntax for conditional rendering in modern Angular templates. It evaluates a boolean expression and renders the content within its block if the expression is true.
-
-## Example Files
-
-### `conditional-content.component.ts`
-
-This is a self-contained standalone component that demonstrates the `@if` block with an optional `@else` block.
-
-```typescript
-import { Component, signal } from '@angular/core';
-
-@Component({
- selector: 'app-conditional-content',
- template: `
-
-
- @if (isVisible()) {
-
This content is conditionally displayed.
- } @else {
-
The content is hidden. Click the button to show it.
- }
- `,
-})
-export class ConditionalContentComponent {
- protected readonly isVisible = signal(true);
-
- toggleVisibility(): void {
- this.isVisible.update((v) => !v);
- }
-}
-```
-
-## Usage Notes
-
-- The expression inside the `@if ()` block must evaluate to a boolean.
-- This example uses a signal, which is a common pattern, but any boolean property or method call from the component can be used.
-- The `@else` block is optional and is rendered when the `@if` condition is `false`.
-
-## How to Use This Example
-
-### 1. Import the Component
-
-In a standalone architecture, import the component into the `imports` array of the parent component where you want to use it.
-
-```typescript
-// in app.component.ts
-import { Component } from '@angular/core';
-import { ConditionalContentComponent } from './conditional-content.component';
-
-@Component({
- selector: 'app-root',
- imports: [ConditionalContentComponent],
- template: `
-
My Application
-
- `,
-})
-export class AppComponent {}
-```
diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts
index a2bc1b0f9aeb..c7beb7c83397 100644
--- a/packages/angular/cli/src/commands/mcp/mcp-server.ts
+++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts
@@ -21,7 +21,6 @@ import { DEVSERVER_STOP_TOOL } from './tools/devserver/devserver-stop';
import { DEVSERVER_WAIT_FOR_BUILD_TOOL } from './tools/devserver/devserver-wait-for-build';
import { DOC_SEARCH_TOOL } from './tools/doc-search';
import { E2E_TOOL } from './tools/e2e';
-import { FIND_EXAMPLE_TOOL } from './tools/examples/index';
import { MODERNIZE_TOOL } from './tools/modernize';
import { ZONELESS_MIGRATION_TOOL } from './tools/onpush-zoneless-migration/zoneless-migration';
import { LIST_PROJECTS_TOOL } from './tools/projects';
@@ -41,7 +40,6 @@ const STABLE_TOOLS = [
AI_TUTOR_TOOL,
BEST_PRACTICES_TOOL,
DOC_SEARCH_TOOL,
- FIND_EXAMPLE_TOOL,
LIST_PROJECTS_TOOL,
ZONELESS_MIGRATION_TOOL,
] as const;
@@ -107,7 +105,6 @@ equivalent actions.
* **3. Answer User Questions:**
- For conceptual questions ("what is..."), use \`search_documentation\`.
- - For code examples ("show me how to..."), use \`find_examples\`.
@@ -163,9 +160,6 @@ export function assembleToolDeclarations(
}
const enabledExperimentalTools = new Set(options.experimentalTools);
- if (process.env['NG_MCP_CODE_EXAMPLES'] === '1') {
- enabledExperimentalTools.add('find_examples');
- }
for (const [toolGroupName, toolGroup] of Object.entries(EXPERIMENTAL_TOOL_GROUPS)) {
if (enabledExperimentalTools.delete(toolGroupName)) {
for (const tool of toolGroup) {
diff --git a/packages/angular/cli/src/commands/mcp/tools/examples/database-discovery.ts b/packages/angular/cli/src/commands/mcp/tools/examples/database-discovery.ts
deleted file mode 100644
index aeab96e15871..000000000000
--- a/packages/angular/cli/src/commands/mcp/tools/examples/database-discovery.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * @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 { dirname, isAbsolute, relative, resolve } from 'node:path';
-import type { McpToolContext } from '../tool-registry';
-
-/**
- * A list of known Angular packages that may contain example databases.
- * The tool will attempt to resolve and load example databases from these packages.
- */
-const KNOWN_EXAMPLE_PACKAGES = ['@angular/core', '@angular/aria', '@angular/forms'];
-
-/**
- * Attempts to find version-specific example databases from the user's installed
- * versions of known Angular packages. It looks for a custom `angular` metadata property in each
- * package's `package.json` to locate the database.
- *
- * @example A sample `package.json` `angular` field:
- * ```json
- * {
- * "angular": {
- * "examples": {
- * "format": "sqlite",
- * "path": "./resources/code-examples.db"
- * }
- * }
- * }
- * ```
- *
- * @param workspacePath The absolute path to the user's `angular.json` file.
- * @param logger The MCP tool context logger for reporting warnings.
- * @param host The host interface for file system and module resolution operations.
- * @returns A promise that resolves to an array of objects, each containing a database path and source.
- */
-export async function getVersionSpecificExampleDatabases(
- workspacePath: string,
- logger: McpToolContext['logger'],
- host: McpToolContext['host'],
-): Promise<{ dbPath: string; source: string }[]> {
- const databases: { dbPath: string; source: string }[] = [];
-
- for (const packageName of KNOWN_EXAMPLE_PACKAGES) {
- // 1. Resolve the path to package.json
- let pkgJsonPath: string;
- try {
- pkgJsonPath = host.resolveModule(`${packageName}/package.json`, workspacePath);
- } catch (e) {
- // This is not a warning because the user may not have all known packages installed.
- continue;
- }
-
- // 2. Read and parse package.json, then find the database.
- try {
- const pkgJsonContent = await host.readFile(pkgJsonPath, 'utf-8');
- const pkgJson = JSON.parse(pkgJsonContent);
- const examplesInfo = pkgJson['angular']?.examples;
-
- if (
- examplesInfo &&
- examplesInfo.format === 'sqlite' &&
- typeof examplesInfo.path === 'string'
- ) {
- const packageDirectory = dirname(pkgJsonPath);
- const dbPath = resolve(packageDirectory, examplesInfo.path);
-
- // Ensure the resolved database path is within the package boundary.
- 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. ` +
- 'This database will be skipped.',
- );
- continue;
- }
-
- // Check the file size to prevent reading a very large file.
- const stats = await host.stat(dbPath);
- if (stats.size > 10 * 1024 * 1024) {
- // 10MB
- logger.warn(
- `The example database at '${dbPath}' is larger than 10MB (${stats.size} bytes). ` +
- 'This is unexpected and the file will not be used.',
- );
- continue;
- }
-
- const source = `package ${packageName}@${pkgJson.version}`;
- databases.push({ dbPath, source });
- }
- } catch (e) {
- logger.warn(
- `Failed to read or parse version-specific examples metadata referenced in '${pkgJsonPath}': ${
- e instanceof Error ? e.message : e
- }.`,
- );
- }
- }
-
- return databases;
-}
diff --git a/packages/angular/cli/src/commands/mcp/tools/examples/database-discovery_spec.ts b/packages/angular/cli/src/commands/mcp/tools/examples/database-discovery_spec.ts
deleted file mode 100644
index 0d5680d01c06..000000000000
--- a/packages/angular/cli/src/commands/mcp/tools/examples/database-discovery_spec.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * @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 type { Stats } from 'node:fs';
-import { Host } from '../../host';
-import { getVersionSpecificExampleDatabases } from './database-discovery';
-
-describe('getVersionSpecificExampleDatabases', () => {
- let mockHost: jasmine.SpyObj;
- let mockLogger: { warn: jasmine.Spy };
-
- beforeEach(() => {
- mockHost = jasmine.createSpyObj('Host', ['resolveModule', 'readFile', 'stat']);
- mockLogger = {
- warn: jasmine.createSpy('warn'),
- };
- });
-
- it('should find a valid example database from a package', async () => {
- mockHost.resolveModule.and.callFake((specifier) => {
- if (specifier === '@angular/core/package.json') {
- return '/path/to/node_modules/@angular/core/package.json';
- }
- throw new Error(`Unexpected module specifier: ${specifier}`);
- });
- mockHost.readFile.and.resolveTo(
- JSON.stringify({
- name: '@angular/core',
- version: '18.1.0',
- angular: {
- examples: {
- format: 'sqlite',
- path: './resources/code-examples.db',
- },
- },
- }),
- );
- mockHost.stat.and.resolveTo({ size: 1024 } as Stats);
-
- const databases = await getVersionSpecificExampleDatabases(
- '/path/to/workspace',
- mockLogger,
- mockHost,
- );
-
- expect(databases.length).toBe(1);
- expect(databases[0].dbPath).toBe(
- '/path/to/node_modules/@angular/core/resources/code-examples.db',
- );
- expect(databases[0].source).toBe('package @angular/core@18.1.0');
- expect(mockLogger.warn).not.toHaveBeenCalled();
- });
-
- it('should skip packages without angular.examples metadata', async () => {
- mockHost.resolveModule.and.returnValue('/path/to/node_modules/@angular/core/package.json');
- mockHost.readFile.and.resolveTo(JSON.stringify({ name: '@angular/core', version: '18.1.0' }));
-
- const databases = await getVersionSpecificExampleDatabases(
- '/path/to/workspace',
- mockLogger,
- mockHost,
- );
-
- expect(databases.length).toBe(0);
- });
-
- it('should handle packages that are not found', async () => {
- mockHost.resolveModule.and.throwError(new Error('Cannot find module'));
-
- const databases = await getVersionSpecificExampleDatabases(
- '/path/to/workspace',
- mockLogger,
- mockHost,
- );
-
- expect(databases.length).toBe(0);
- expect(mockLogger.warn).not.toHaveBeenCalled();
- });
-
- it('should reject database paths that attempt path traversal', async () => {
- mockHost.resolveModule.and.returnValue('/path/to/node_modules/@angular/core/package.json');
- mockHost.readFile.and.resolveTo(
- JSON.stringify({
- name: '@angular/core',
- version: '18.1.0',
- angular: {
- examples: {
- format: 'sqlite',
- path: '../outside-package/danger.db',
- },
- },
- }),
- );
-
- const databases = await getVersionSpecificExampleDatabases(
- '/path/to/workspace',
- mockLogger,
- mockHost,
- );
-
- expect(databases.length).toBe(0);
- expect(mockLogger.warn).toHaveBeenCalledWith(
- jasmine.stringMatching(/Detected a potential path traversal attempt/),
- );
- });
-
- it('should skip database files larger than 10MB', async () => {
- mockHost.resolveModule.and.returnValue('/path/to/node_modules/@angular/core/package.json');
- mockHost.readFile.and.resolveTo(
- JSON.stringify({
- name: '@angular/core',
- version: '18.1.0',
- angular: {
- examples: {
- format: 'sqlite',
- path: './resources/code-examples.db',
- },
- },
- }),
- );
- mockHost.stat.and.resolveTo({ size: 11 * 1024 * 1024 } as Stats); // 11MB
-
- const databases = await getVersionSpecificExampleDatabases(
- '/path/to/workspace',
- mockLogger,
- mockHost,
- );
-
- expect(databases.length).toBe(0);
- expect(mockLogger.warn).toHaveBeenCalledWith(jasmine.stringMatching(/is larger than 10MB/));
- });
-});
diff --git a/packages/angular/cli/src/commands/mcp/tools/examples/database.ts b/packages/angular/cli/src/commands/mcp/tools/examples/database.ts
deleted file mode 100644
index cd4f31a655c0..000000000000
--- a/packages/angular/cli/src/commands/mcp/tools/examples/database.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * @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 type { DatabaseSync, SQLInputValue } from 'node:sqlite';
-import { escapeSearchQuery } from './query-escaper';
-import type { FindExampleInput } from './schemas';
-
-const EXPECTED_SCHEMA_VERSION = 1;
-
-/**
- * Validates the schema version of the example database.
- *
- * @param db The database connection to validate.
- * @param dbSource A string identifying the source of the database (e.g., 'bundled' or a version number).
- * @throws An error if the schema version is missing or incompatible.
- */
-export function validateDatabaseSchema(db: DatabaseSync, dbSource: string): void {
- const schemaVersionResult = db
- .prepare('SELECT value FROM metadata WHERE key = ?')
- .get('schema_version') as { value: string } | undefined;
- const actualSchemaVersion = schemaVersionResult ? Number(schemaVersionResult.value) : undefined;
-
- if (actualSchemaVersion !== EXPECTED_SCHEMA_VERSION) {
- db.close();
-
- let errorMessage: string;
- if (actualSchemaVersion === undefined) {
- errorMessage = 'The example database is missing a schema version and cannot be used.';
- } else if (actualSchemaVersion > EXPECTED_SCHEMA_VERSION) {
- errorMessage =
- `This project's example database (version ${actualSchemaVersion})` +
- ` is newer than what this version of the Angular CLI supports (version ${EXPECTED_SCHEMA_VERSION}).` +
- ' Please update your `@angular/cli` package to a newer version.';
- } else {
- errorMessage =
- `This version of the Angular CLI (expects schema version ${EXPECTED_SCHEMA_VERSION})` +
- ` requires a newer example database than the one found in this project (version ${actualSchemaVersion}).`;
- }
-
- throw new Error(
- `Incompatible example database schema from source '${dbSource}':\n${errorMessage}`,
- );
- }
-}
-
-export function queryDatabase(dbs: DatabaseSync[], input: FindExampleInput) {
- const { query, keywords, required_packages, related_concepts, includeExperimental } = input;
-
- // Build the query dynamically
- const params: SQLInputValue[] = [];
- let sql =
- `SELECT e.title, e.summary, e.keywords, e.required_packages, e.related_concepts, e.related_tools, e.content, ` +
- // The `snippet` function generates a contextual snippet of the matched text.
- // Column 6 is the `content` column. We highlight matches with asterisks and limit the snippet size.
- "snippet(examples_fts, 6, '**', '**', '...', 15) AS snippet, " +
- // The `bm25` function returns the relevance score of the match. The weights
- // assigned to each column boost the ranking of documents where the search
- // term appears in a more important field.
- // Column order: title, summary, keywords, required_packages, related_concepts, related_tools, content
- 'bm25(examples_fts, 10.0, 5.0, 5.0, 1.0, 2.0, 1.0, 1.0) AS rank ' +
- 'FROM examples e JOIN examples_fts ON e.id = examples_fts.rowid';
- const whereClauses = [];
-
- // FTS query
- if (query) {
- whereClauses.push('examples_fts MATCH ?');
- params.push(escapeSearchQuery(query));
- }
-
- // JSON array filters
- const addJsonFilter = (column: string, values: string[] | undefined) => {
- if (values?.length) {
- for (const value of values) {
- whereClauses.push(`e.${column} LIKE ?`);
- params.push(`%"${value}"%`);
- }
- }
- };
-
- addJsonFilter('keywords', keywords);
- addJsonFilter('required_packages', required_packages);
- addJsonFilter('related_concepts', related_concepts);
-
- if (!includeExperimental) {
- whereClauses.push('e.experimental = 0');
- }
-
- if (whereClauses.length > 0) {
- sql += ` WHERE ${whereClauses.join(' AND ')}`;
- }
-
- // Query database and return results
- const examples = [];
- const textContent = [];
-
- for (const db of dbs) {
- const queryStatement = db.prepare(sql);
- for (const exampleRecord of queryStatement.all(...params)) {
- const record = exampleRecord as Record;
- const example = {
- title: record['title'] as string,
- summary: record['summary'] as string,
- keywords: JSON.parse((record['keywords'] as string) || '[]') as string[],
- required_packages: JSON.parse((record['required_packages'] as string) || '[]') as string[],
- related_concepts: JSON.parse((record['related_concepts'] as string) || '[]') as string[],
- related_tools: JSON.parse((record['related_tools'] as string) || '[]') as string[],
- content: record['content'] as string,
- snippet: record['snippet'] as string,
- rank: record['rank'] as number,
- };
- examples.push(example);
- }
- }
-
- // Order the combined results by relevance.
- // The `bm25` algorithm returns a smaller number for a more relevant match.
- examples.sort((a, b) => a.rank - b.rank);
-
- // The `rank` field is an internal implementation detail for sorting and should not be
- // returned to the user. We create a new array of examples without the `rank`.
- const finalExamples = examples.map(({ rank, ...rest }) => rest);
-
- for (const example of finalExamples) {
- // Also create a more structured text output
- let text = `## Example: ${example.title}\n**Summary:** ${example.summary}`;
- if (example.snippet) {
- text += `\n**Snippet:** ${example.snippet}`;
- }
- text += `\n\n---\n\n${example.content}`;
- textContent.push({ type: 'text' as const, text });
- }
-
- return {
- content: textContent,
- structuredContent: { examples: finalExamples },
- };
-}
diff --git a/packages/angular/cli/src/commands/mcp/tools/examples/index.ts b/packages/angular/cli/src/commands/mcp/tools/examples/index.ts
deleted file mode 100644
index 8ceecf2d840d..000000000000
--- a/packages/angular/cli/src/commands/mcp/tools/examples/index.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * @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 type { DatabaseSync } from 'node:sqlite';
-import { type McpToolContext, declareTool } from '../tool-registry';
-import { queryDatabase, validateDatabaseSchema } from './database';
-import { getVersionSpecificExampleDatabases } from './database-discovery';
-import { setupRuntimeExamples } from './runtime-database';
-import { type FindExampleInput, findExampleInputSchema, findExampleOutputSchema } from './schemas';
-import { suppressSqliteWarning } from './utils';
-
-export const FIND_EXAMPLE_TOOL = declareTool({
- name: 'find_examples',
- title: 'Find Angular Code Examples',
- description: `
-
-Augments your knowledge base with a curated database of official, best-practice code examples,
-focusing on **modern, new, and recently updated** Angular features. This tool acts as a RAG
-(Retrieval-Augmented Generation) source, providing ground-truth information on the latest Angular
-APIs and patterns. You **MUST** use it to understand and apply current standards when working with
-new or evolving features.
-
-
-
-* **Project-Specific Use (Recommended):** For tasks inside a user's project, you **MUST** provide the
- \`workspacePath\` argument to get examples that match the project's Angular version. Get this
- path from \`list_projects\`.
-* **General Use:** If no project context is available (e.g., for general questions or learning),
- you can call the tool without the \`workspacePath\` argument. It will return the latest
- generic examples.
-* **Tool Selection:** This database primarily contains examples for new and recently updated Angular
- features. For established, core features, the main documentation (via the
- \`search_documentation\` tool) may be a better source of information.
-* The examples in this database are the single source of truth for modern Angular coding patterns.
-* The search query uses a powerful full-text search syntax (FTS5). Refer to the 'query'
- parameter description for detailed syntax rules and examples.
-* You can combine the main 'query' with optional filters like 'keywords', 'required_packages',
- and 'related_concepts' to create highly specific searches.
-`,
- inputSchema: findExampleInputSchema.shape,
- outputSchema: findExampleOutputSchema.shape,
- isReadOnly: true,
- isLocalOnly: true,
- shouldRegister: ({ logger }) => {
- // sqlite database support requires Node.js 22.16+
- const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(Number);
- if (nodeMajor < 22 || (nodeMajor === 22 && nodeMinor < 16)) {
- logger.warn(
- `MCP tool 'find_examples' requires Node.js 22.16 (or higher). ` +
- ' Registration of this tool has been skipped.',
- );
-
- return false;
- }
-
- return true;
- },
- factory: createFindExampleHandler,
-});
-
-async function createFindExampleHandler({ logger, exampleDatabasePath, host }: McpToolContext) {
- const runtimeDb = process.env['NG_MCP_EXAMPLES_DIR']
- ? await setupRuntimeExamples(process.env['NG_MCP_EXAMPLES_DIR'], host)
- : undefined;
-
- suppressSqliteWarning();
-
- return async (input: FindExampleInput) => {
- // If the dev-time override is present, use it and bypass all other logic.
- if (runtimeDb) {
- return queryDatabase([runtimeDb], input);
- }
-
- const resolvedDbs: { path: string; source: string }[] = [];
-
- // First, try to get all available version-specific guides.
- if (input.workspacePath) {
- const versionSpecificDbs = await getVersionSpecificExampleDatabases(
- input.workspacePath,
- logger,
- host,
- );
- for (const db of versionSpecificDbs) {
- resolvedDbs.push({ path: db.dbPath, source: db.source });
- }
- }
-
- // If no version-specific guides were found for any reason, fall back to the bundled version.
- if (resolvedDbs.length === 0 && exampleDatabasePath) {
- resolvedDbs.push({ path: exampleDatabasePath, source: 'bundled' });
- }
-
- if (resolvedDbs.length === 0) {
- // This should be prevented by the registration logic in mcp-server.ts
- throw new Error('No example databases are available.');
- }
-
- const { DatabaseSync } = await import('node:sqlite');
- const dbConnections: DatabaseSync[] = [];
-
- for (const { path, source } of resolvedDbs) {
- const db = new DatabaseSync(path, { readOnly: true });
- try {
- validateDatabaseSchema(db, source);
- dbConnections.push(db);
- } catch (e) {
- logger.warn((e as Error).message);
- // If a database is invalid, we should not query it, but we should not fail the whole tool.
- // We will just skip this database and try to use the others.
- continue;
- }
- }
-
- if (dbConnections.length === 0) {
- throw new Error('All available example databases were invalid. Cannot perform query.');
- }
-
- return queryDatabase(dbConnections, input);
- };
-}
diff --git a/packages/angular/cli/src/commands/mcp/tools/examples/query-escaper.ts b/packages/angular/cli/src/commands/mcp/tools/examples/query-escaper.ts
deleted file mode 100644
index bb6acd375d45..000000000000
--- a/packages/angular/cli/src/commands/mcp/tools/examples/query-escaper.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * @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
- */
-
-/**
- * Escapes a search query for FTS5 by tokenizing and quoting terms.
- *
- * This function processes a raw search string and prepares it for an FTS5 full-text search.
- * It correctly handles quoted phrases, logical operators (AND, OR, NOT), parentheses,
- * and prefix searches (ending with an asterisk), ensuring that individual search
- * terms are properly quoted to be treated as literals by the search engine.
- * This is primarily intended to avoid unintentional usage of FTS5 query syntax by consumers.
- *
- * @param query The raw search query string.
- * @returns A sanitized query string suitable for FTS5.
- */
-export function escapeSearchQuery(query: string): string {
- // This regex tokenizes the query string into parts:
- // 1. Quoted phrases (e.g., "foo bar")
- // 2. Parentheses ( and )
- // 3. FTS5 operators (AND, OR, NOT, NEAR)
- // 4. Words, which can include a trailing asterisk for prefix search (e.g., foo*)
- const tokenizer = /"([^"]*)"|([()])|\b(AND|OR|NOT|NEAR)\b|([^\s()]+)/g;
- let match;
- const result: string[] = [];
- let lastIndex = 0;
-
- while ((match = tokenizer.exec(query)) !== null) {
- // Add any whitespace or other characters between tokens
- if (match.index > lastIndex) {
- result.push(query.substring(lastIndex, match.index));
- }
-
- const [, quoted, parenthesis, operator, term] = match;
-
- if (quoted !== undefined) {
- // It's a quoted phrase, keep it as is.
- result.push(`"${quoted}"`);
- } else if (parenthesis) {
- // It's a parenthesis, keep it as is.
- result.push(parenthesis);
- } else if (operator) {
- // It's an operator, keep it as is.
- result.push(operator);
- } else if (term) {
- // It's a term that needs to be quoted.
- if (term.endsWith('*')) {
- result.push(`"${term.slice(0, -1)}"*`);
- } else {
- result.push(`"${term}"`);
- }
- }
- lastIndex = tokenizer.lastIndex;
- }
-
- // Add any remaining part of the string
- if (lastIndex < query.length) {
- result.push(query.substring(lastIndex));
- }
-
- return result.join('');
-}
diff --git a/packages/angular/cli/src/commands/mcp/tools/examples/query-escaper_spec.ts b/packages/angular/cli/src/commands/mcp/tools/examples/query-escaper_spec.ts
deleted file mode 100644
index 6aa801fe0349..000000000000
--- a/packages/angular/cli/src/commands/mcp/tools/examples/query-escaper_spec.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * @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 { escapeSearchQuery } from './query-escaper';
-
-describe('escapeSearchQuery', () => {
- it('should wrap single terms in double quotes', () => {
- expect(escapeSearchQuery('foo')).toBe('"foo"');
- });
-
- it('should wrap multiple terms in double quotes', () => {
- expect(escapeSearchQuery('foo bar')).toBe('"foo" "bar"');
- });
-
- it('should not wrap FTS5 operators', () => {
- expect(escapeSearchQuery('foo AND bar')).toBe('"foo" AND "bar"');
- expect(escapeSearchQuery('foo OR bar')).toBe('"foo" OR "bar"');
- expect(escapeSearchQuery('foo NOT bar')).toBe('"foo" NOT "bar"');
- expect(escapeSearchQuery('foo NEAR bar')).toBe('"foo" NEAR "bar"');
- });
-
- it('should not wrap terms that are already quoted', () => {
- expect(escapeSearchQuery('"foo" bar')).toBe('"foo" "bar"');
- expect(escapeSearchQuery('"foo bar"')).toBe('"foo bar"');
- });
-
- it('should handle prefix searches', () => {
- expect(escapeSearchQuery('foo*')).toBe('"foo"*');
- expect(escapeSearchQuery('foo* bar')).toBe('"foo"* "bar"');
- });
-
- it('should handle multi-word quoted phrases', () => {
- expect(escapeSearchQuery('"foo bar" baz')).toBe('"foo bar" "baz"');
- expect(escapeSearchQuery('foo "bar baz"')).toBe('"foo" "bar baz"');
- });
-
- it('should handle complex queries', () => {
- expect(escapeSearchQuery('("foo bar" OR baz) AND qux*')).toBe(
- '("foo bar" OR "baz") AND "qux"*',
- );
- });
-
- it('should handle multi-word quoted phrases with three or more words', () => {
- expect(escapeSearchQuery('"foo bar baz" qux')).toBe('"foo bar baz" "qux"');
- expect(escapeSearchQuery('foo "bar baz qux"')).toBe('"foo" "bar baz qux"');
- expect(escapeSearchQuery('foo "bar baz qux" quux')).toBe('"foo" "bar baz qux" "quux"');
- });
-});
diff --git a/packages/angular/cli/src/commands/mcp/tools/examples/runtime-database.ts b/packages/angular/cli/src/commands/mcp/tools/examples/runtime-database.ts
deleted file mode 100644
index 5ca74dc60d63..000000000000
--- a/packages/angular/cli/src/commands/mcp/tools/examples/runtime-database.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-/**
- * @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 { join } from 'node:path';
-import type { DatabaseSync } from 'node:sqlite';
-import { z } from 'zod';
-import type { McpToolContext } from '../tool-registry';
-
-/**
- * A simple YAML front matter parser.
- *
- * This function extracts the YAML block enclosed by `---` at the beginning of a string
- * and parses it into a JavaScript object. It is not a full YAML parser and only
- * supports simple key-value pairs and string arrays.
- *
- * @param content The string content to parse.
- * @returns A record containing the parsed front matter data.
- */
-function parseFrontmatter(content: string): Record {
- const match = content.match(/^---\r?\n(.*?)\r?\n---/s);
- if (!match) {
- return {};
- }
-
- const frontmatter = match[1];
- const data: Record = {};
- const lines = frontmatter.split(/\r?\n/);
-
- let currentKey = '';
- let isArray = false;
- const arrayValues: string[] = [];
-
- for (const line of lines) {
- const keyValueMatch = line.match(/^([^:]+):\s*(.*)/);
- if (keyValueMatch) {
- if (currentKey && isArray) {
- data[currentKey] = arrayValues.slice();
- arrayValues.length = 0;
- }
-
- const [, key, value] = keyValueMatch;
- currentKey = key.trim();
- isArray = value.trim() === '';
-
- if (!isArray) {
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- data[currentKey] = true;
- } else if (trimmedValue === 'false') {
- data[currentKey] = false;
- } else {
- data[currentKey] = trimmedValue;
- }
- }
- } else {
- const arrayItemMatch = line.match(/^\s*-\s*(.*)/);
- if (arrayItemMatch && currentKey && isArray) {
- let value = arrayItemMatch[1].trim();
- // Unquote if the value is quoted.
- if (
- (value.startsWith("'") && value.endsWith("'")) ||
- (value.startsWith('"') && value.endsWith('"'))
- ) {
- value = value.slice(1, -1);
- }
- arrayValues.push(value);
- }
- }
- }
-
- if (currentKey && isArray) {
- data[currentKey] = arrayValues;
- }
-
- return data;
-}
-
-export async function setupRuntimeExamples(
- examplesPath: string,
- host: McpToolContext['host'],
-): Promise {
- const { DatabaseSync } = await import('node:sqlite');
- const db = new DatabaseSync(':memory:');
-
- // Create a relational table to store the structured example data.
- db.exec(`
- CREATE TABLE metadata (
- key TEXT PRIMARY KEY NOT NULL,
- value TEXT NOT NULL
- );
- `);
-
- db.exec(`
- INSERT INTO metadata (key, value) VALUES
- ('schema_version', '1'),
- ('created_at', '${new Date().toISOString()}');
- `);
-
- db.exec(`
- CREATE TABLE examples (
- id INTEGER PRIMARY KEY,
- title TEXT NOT NULL,
- summary TEXT NOT NULL,
- keywords TEXT,
- required_packages TEXT,
- related_concepts TEXT,
- related_tools TEXT,
- experimental INTEGER NOT NULL DEFAULT 0,
- content TEXT NOT NULL
- );
- `);
-
- // Create an FTS5 virtual table to provide full-text search capabilities.
- db.exec(`
- CREATE VIRTUAL TABLE examples_fts USING fts5(
- title,
- summary,
- keywords,
- required_packages,
- related_concepts,
- related_tools,
- content,
- content='examples',
- content_rowid='id',
- tokenize = 'porter ascii'
- );
- `);
-
- // Create triggers to keep the FTS table synchronized with the examples table.
- db.exec(`
- CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
- INSERT INTO examples_fts(rowid, title, summary, keywords, required_packages, related_concepts, related_tools, content)
- VALUES (
- new.id, new.title, new.summary, new.keywords, new.required_packages, new.related_concepts,
- new.related_tools, new.content
- );
- END;
- `);
-
- const insertStatement = db.prepare(
- 'INSERT INTO examples(' +
- 'title, summary, keywords, required_packages, related_concepts, related_tools, experimental, content' +
- ') VALUES(?, ?, ?, ?, ?, ?, ?, ?);',
- );
-
- const frontmatterSchema = z.object({
- title: z.string(),
- summary: z.string(),
- keywords: z.array(z.string()).optional(),
- required_packages: z.array(z.string()).optional(),
- related_concepts: z.array(z.string()).optional(),
- related_tools: z.array(z.string()).optional(),
- experimental: z.boolean().optional(),
- });
-
- db.exec('BEGIN TRANSACTION');
- for await (const entry of host.glob('**/*.md', { cwd: examplesPath })) {
- if (!entry.isFile()) {
- continue;
- }
-
- const content = await host.readFile(join(entry.parentPath, entry.name), 'utf-8');
- const frontmatter = parseFrontmatter(content);
-
- const validation = frontmatterSchema.safeParse(frontmatter);
- if (!validation.success) {
- // eslint-disable-next-line no-console
- console.warn(`Skipping invalid example file ${entry.name}:`, validation.error.issues);
- continue;
- }
-
- const {
- title,
- summary,
- keywords,
- required_packages,
- related_concepts,
- related_tools,
- experimental,
- } = validation.data;
-
- insertStatement.run(
- title,
- summary,
- JSON.stringify(keywords ?? []),
- JSON.stringify(required_packages ?? []),
- JSON.stringify(related_concepts ?? []),
- JSON.stringify(related_tools ?? []),
- experimental ? 1 : 0,
- content,
- );
- }
- db.exec('END TRANSACTION');
-
- return db;
-}
diff --git a/packages/angular/cli/src/commands/mcp/tools/examples/schemas.ts b/packages/angular/cli/src/commands/mcp/tools/examples/schemas.ts
deleted file mode 100644
index 2122f6775bc8..000000000000
--- a/packages/angular/cli/src/commands/mcp/tools/examples/schemas.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * @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 { z } from 'zod';
-
-export const findExampleInputSchema = z.object({
- workspacePath: z
- .string()
- .optional()
- .describe(
- 'The absolute path to the `angular.json` file for the workspace. This is used to find the ' +
- 'version-specific code examples that correspond to the installed version of the ' +
- 'Angular framework. You **MUST** get this path from the `list_projects` tool. ' +
- 'If omitted, the tool will search the generic code examples bundled with the CLI.',
- ),
- query: z
- .string()
- .describe(
- "The primary, conceptual search query. This should capture the user's main goal or question " +
- "(e.g., 'lazy loading a route' or 'how to use signal inputs'). The query will be processed " +
- 'by a powerful full-text search engine.\n\n' +
- 'Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):\n' +
- ' - AND (default): Space-separated terms are combined with AND.\n' +
- ' - Example: \'standalone component\' (finds results with both "standalone" and "component")\n' +
- ' - OR: Use the OR operator to find results with either term.\n' +
- " - Example: 'validation OR validator'\n" +
- ' - NOT: Use the NOT operator to exclude terms.\n' +
- " - Example: 'forms NOT reactive'\n" +
- ' - Grouping: Use parentheses () to group expressions.\n' +
- " - Example: '(validation OR validator) AND forms'\n" +
- ' - Phrase Search: Use double quotes "" for exact phrases.\n' +
- ' - Example: \'"template-driven forms"\'\n' +
- ' - Prefix Search: Use an asterisk * for prefix matching.\n' +
- ' - Example: \'rout*\' (matches "route", "router", "routing")',
- ),
- keywords: z
- .array(z.string())
- .optional()
- .describe(
- 'A list of specific, exact keywords to narrow the search. Use this for precise terms like ',
- ),
- required_packages: z
- .array(z.string())
- .optional()
- .describe(
- "A list of NPM packages that an example must use. Use this when the user's request is " +
- 'specific to a feature within a certain package (e.g., if the user asks about `ngModel`, ' +
- 'you should filter by `@angular/forms`).',
- ),
- related_concepts: z
- .array(z.string())
- .optional()
- .describe(
- 'A list of high-level concepts to filter by. Use this to find examples related to broader ' +
- 'architectural ideas or patterns (e.g., `signals`, `dependency injection`, `routing`).',
- ),
- includeExperimental: z
- .boolean()
- .optional()
- .default(false)
- .describe(
- 'By default, this tool returns only production-safe examples. Set this to `true` **only if** ' +
- 'the user explicitly asks for a bleeding-edge feature or if a stable solution to their ' +
- 'problem cannot be found. If you set this to `true`, you **MUST** preface your answer by ' +
- 'warning the user that the example uses experimental APIs that are not suitable for production.',
- ),
-});
-
-export type FindExampleInput = z.infer;
-
-export const findExampleOutputSchema = z.object({
- examples: z.array(
- z.object({
- title: z
- .string()
- .describe(
- 'The title of the example. Use this as a heading when presenting the example to the user.',
- ),
- summary: z
- .string()
- .describe(
- "A one-sentence summary of the example's purpose. Use this to help the user decide " +
- 'if the example is relevant to them.',
- ),
- keywords: z
- .array(z.string())
- .optional()
- .describe(
- 'A list of keywords for the example. You can use these to explain why this example ' +
- "was a good match for the user's query.",
- ),
- required_packages: z
- .array(z.string())
- .optional()
- .describe(
- 'A list of NPM packages required for the example to work. Before presenting the code, ' +
- 'you should inform the user if any of these packages need to be installed.',
- ),
- related_concepts: z
- .array(z.string())
- .optional()
- .describe(
- 'A list of related concepts. You can suggest these to the user as topics for ' +
- 'follow-up questions.',
- ),
- related_tools: z
- .array(z.string())
- .optional()
- .describe(
- 'A list of related MCP tools. You can suggest these as potential next steps for the user.',
- ),
- content: z
- .string()
- .describe(
- 'A complete, self-contained Angular code example in Markdown format. This should be ' +
- 'presented to the user inside a markdown code block.',
- ),
- snippet: z
- .string()
- .optional()
- .describe(
- 'A contextual snippet from the content showing the matched search term. This field is ' +
- 'critical for efficiently evaluating a result`s relevance. It enables two primary ',
- ),
- }),
- ),
-});
diff --git a/packages/angular/cli/src/commands/mcp/tools/examples/utils.ts b/packages/angular/cli/src/commands/mcp/tools/examples/utils.ts
deleted file mode 100644
index 312994d7b3fd..000000000000
--- a/packages/angular/cli/src/commands/mcp/tools/examples/utils.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * @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
- */
-
-/**
- * Suppresses the experimental warning emitted by Node.js for the `node:sqlite` module.
- *
- * This is a workaround to prevent the console from being cluttered with warnings
- * about the experimental status of the SQLite module, which is used by this tool.
- */
-export function suppressSqliteWarning(): void {
- const originalProcessEmit = process.emit;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- process.emit = function (event: string, error?: unknown): any {
- if (
- event === 'warning' &&
- error instanceof Error &&
- error.name === 'ExperimentalWarning' &&
- error.message.includes('SQLite')
- ) {
- return false;
- }
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-rest-params
- return originalProcessEmit.apply(process, arguments as any);
- };
-}
diff --git a/tests/e2e/tests/mcp/find-examples-basic.ts b/tests/e2e/tests/mcp/find-examples-basic.ts
deleted file mode 100644
index b7f42045076c..000000000000
--- a/tests/e2e/tests/mcp/find-examples-basic.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { exec, ProcessOutput, silentNpm } from '../../utils/process';
-import assert from 'node:assert/strict';
-
-const MCP_INSPECTOR_PACKAGE_NAME = '@modelcontextprotocol/inspector-cli';
-const MCP_INSPECTOR_PACKAGE_VERSION = '0.16.2';
-const MCP_INSPECTOR_COMMAND_NAME = 'mcp-inspector-cli';
-
-async function runInspector(...args: string[]): Promise {
- const result = await exec(
- MCP_INSPECTOR_COMMAND_NAME,
- '--cli',
- 'npx',
- '--no',
- '@angular/cli',
- 'mcp',
- ...args,
- );
-
- return result;
-}
-
-export default async function () {
- const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(Number);
- if (nodeMajor < 22 || (nodeMajor === 22 && nodeMinor < 16)) {
- console.log('Test bypassed: find_examples tool requires Node.js 22.16 or higher.');
-
- return;
- }
-
- await silentNpm(
- 'install',
- '--ignore-scripts',
- '-g',
- `${MCP_INSPECTOR_PACKAGE_NAME}@${MCP_INSPECTOR_PACKAGE_VERSION}`,
- );
-
- // Ensure `get_best_practices` returns the markdown content
- const { stdout: stdoutInsideWorkspace } = await runInspector(
- '--method',
- 'tools/call',
- '--tool-name',
- 'find_examples',
- '--tool-arg',
- 'query=if',
- );
-
- assert.match(stdoutInsideWorkspace, /Using the @if Built-in Control Flow Block/);
-}
diff --git a/tools/defaults.bzl b/tools/defaults.bzl
index dd706151d169..dd054c9c1462 100644
--- a/tools/defaults.bzl
+++ b/tools/defaults.bzl
@@ -2,7 +2,6 @@ load("@aspect_rules_jasmine//jasmine:defs.bzl", _jasmine_test = "jasmine_test")
load("@aspect_rules_js//js:defs.bzl", _js_binary = "js_binary")
load("@bazel_lib//lib:copy_to_bin.bzl", _copy_to_bin = "copy_to_bin")
load("@devinfra//bazel/ts_project:index.bzl", "strict_deps_test")
-load("@rules_angular//src/ng_examples_db:index.bzl", _ng_examples_db = "ng_examples_db")
load("@rules_angular//src/ng_package:index.bzl", _ng_package = "ng_package")
load("@rules_angular//src/ts_project:index.bzl", _ts_project = "ts_project")
load("//tools:substitutions.bzl", "substitutions")
@@ -91,6 +90,3 @@ def jasmine_test(data = [], args = [], **kwargs):
data = data + ["//:node_modules/source-map-support"],
**kwargs
)
-
-def ng_examples_db(**kwargs):
- _ng_examples_db(**kwargs)