Skip to main content
Glama
push-based

Angular Toolkit MCP

by push-based
angular-mcp-server.ts8.88 kB
import { PROMPTS, PROMPTS_IMPL } from './prompts/prompt-registry.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { CallToolRequest, CallToolRequestSchema, GetPromptRequestSchema, GetPromptResult, ListPromptsRequestSchema, ListPromptsResult, ListResourcesRequestSchema, ListResourcesResult, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { TOOLS } from './tools/tools.js'; import { toolNotFound } from './tools/utils.js'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { AngularMcpServerOptionsSchema, AngularMcpServerOptions, } from './validation/angular-mcp-server-options.schema.js'; import { validateAngularMcpServerFilesExist } from './validation/file-existence.js'; import { validateDeprecatedCssClassesFile } from './validation/ds-components-file.validation.js'; export class AngularMcpServerWrapper { private readonly mcpServer: McpServer; private readonly workspaceRoot: string; private readonly storybookDocsRoot?: string; private readonly deprecatedCssClassesPath?: string; private readonly uiRoot: string; /** * Private constructor - use AngularMcpServerWrapper.create() instead. * Config is already validated when this constructor is called. */ private constructor(config: AngularMcpServerOptions) { // Config is already validated, no need to validate again const { workspaceRoot, ds } = config; this.workspaceRoot = workspaceRoot; this.storybookDocsRoot = ds.storybookDocsRoot; this.deprecatedCssClassesPath = ds.deprecatedCssClassesPath; this.uiRoot = ds.uiRoot; this.mcpServer = new McpServer({ name: 'Angular MCP', version: '0.0.0', }); this.mcpServer.server.registerCapabilities({ prompts: {}, tools: {}, resources: {}, }); this.registerPrompts(); this.registerTools(); this.registerResources(); } /** * Creates and validates an AngularMcpServerWrapper instance. * This is the recommended way to create an instance as it performs all necessary validations. * * @param config - The Angular MCP server configuration options * @returns A Promise that resolves to a fully configured AngularMcpServerWrapper instance * @throws {Error} If configuration validation fails or required files don't exist */ static async create( config: AngularMcpServerOptions, ): Promise<AngularMcpServerWrapper> { // Validate config using the Zod schema - only once here const validatedConfig = AngularMcpServerOptionsSchema.parse(config); // Validate file existence (optional keys are checked only when provided) validateAngularMcpServerFilesExist(validatedConfig); // Load and validate deprecatedCssClassesPath content only if provided if (validatedConfig.ds.deprecatedCssClassesPath) { await validateDeprecatedCssClassesFile(validatedConfig); } return new AngularMcpServerWrapper(validatedConfig); } getMcpServer(): McpServer { return this.mcpServer; } private registerResources() { this.mcpServer.server.setRequestHandler( ListResourcesRequestSchema, async (): Promise<ListResourcesResult> => { const resources = []; // Try to read the llms.txt file from the package root (optional) try { const currentDir = path.dirname(fileURLToPath(import.meta.url)); const filePath = path.resolve(currentDir, '../../llms.txt'); // Only attempt to read if file exists if (fs.existsSync(filePath)) { const content = fs.readFileSync(filePath, 'utf-8'); const lines = content.split('\n'); let currentSection = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Skip empty lines and comments that don't start with # if (!line || (line.startsWith('#') && !line.includes(':'))) { continue; } // Update section if line starts with # if (line.startsWith('# ')) { currentSection = line.substring(2).replace(':', '').trim(); continue; } // Parse markdown links: [name](url) const linkMatch = line.match(/- \[(.*?)\]\((.*?)\):(.*)/); if (linkMatch) { const [, name, uri, description = ''] = linkMatch; resources.push({ uri, name: name.trim(), type: currentSection.toLowerCase(), content: description.trim() || name.trim(), }); continue; } // Parse simple links: - [name](url) const simpleLinkMatch = line.match(/- \[(.*?)\]\((.*?)\)/); if (simpleLinkMatch) { const [, name, uri] = simpleLinkMatch; resources.push({ uri, name: name.trim(), type: currentSection.toLowerCase(), content: name.trim(), }); } } } } catch { // Silently ignore errors reading llms.txt (non-fatal) } // Scan available design system components to add them as discoverable resources try { if (this.storybookDocsRoot) { const dsUiPath = path.resolve( process.cwd(), this.storybookDocsRoot, ); if (fs.existsSync(dsUiPath)) { const componentFolders = fs .readdirSync(dsUiPath, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); for (const folder of componentFolders) { // Convert kebab-case to PascalCase with 'Ds' prefix const componentName = 'Ds' + folder .split('-') .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); resources.push({ uri: `ds-component://${folder}`, name: componentName, type: 'design-system-component', content: `Design System component: ${componentName}`, }); } } } } catch { // Silently ignore errors scanning DS components (non-fatal) } return { resources, }; }, ); } private registerPrompts() { this.mcpServer.server.setRequestHandler( ListPromptsRequestSchema, async (): Promise<ListPromptsResult> => { return { prompts: Object.values(PROMPTS), }; }, ); this.mcpServer.server.setRequestHandler( GetPromptRequestSchema, async (request): Promise<GetPromptResult> => { const prompt = PROMPTS[request.params.name]; if (!prompt) { throw new Error(`Prompt not found: ${request.params.name}`); } const promptResult = PROMPTS_IMPL[request.params.name]; // Register all prompts if (promptResult && promptResult.text) { return { messages: [ { role: 'user', content: { type: 'text', text: promptResult.text(request.params.arguments ?? {}), }, }, ], }; } throw new Error('Prompt implementation not found'); }, ); } private registerTools() { this.mcpServer.server.setRequestHandler( ListToolsRequestSchema, async () => { return { tools: TOOLS.map(({ schema }) => schema), }; }, ); this.mcpServer.server.setRequestHandler( CallToolRequestSchema, async (request: CallToolRequest) => { const tool = TOOLS.find( ({ schema }) => request.params.name === schema.name, ); if (tool?.schema && tool.schema.name === request.params.name) { return await tool.handler({ ...request, params: { ...request.params, arguments: { ...request.params.arguments, storybookDocsRoot: this.storybookDocsRoot, deprecatedCssClassesPath: this.deprecatedCssClassesPath, uiRoot: this.uiRoot, cwd: this.workspaceRoot, workspaceRoot: this.workspaceRoot, }, }, }); } return { content: [toolNotFound(request)], isError: false, }; }, ); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/push-based/angular-toolkit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server