Skip to main content
Glama
index.ts29.8 kB
/** * @module tools * * MCP Tool Registry and Request Handlers * * This module registers and implements all MCP tools exposed by the Macroforge server. * It serves as the main business logic layer, handling tool requests from MCP clients. * * ## Registered Tools * * | Tool | Purpose | * |------|---------| * | `list-sections` | List all documentation sections with metadata | * | `get-documentation` | Retrieve full content for specific sections | * | `macroforge-autofixer` | Validate TypeScript code with @derive decorators | * | `expand-code` | Expand macros and show generated code | * | `get-macro-info` | Get documentation for macros and decorators | * * ## Architecture * * The module uses: * - `docs-loader.js` for documentation loading and search * - `@macroforge/core` (optional) for native code validation and expansion * * Native bindings are loaded dynamically and gracefully degrade if unavailable. * * @see {@link registerTools} for the main entry point */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { loadSections, getSection, searchSections, type Section } from './docs-loader.js'; /** Cached documentation sections loaded at server startup */ let sections: Section[] = []; /** * Registers all Macroforge MCP tools with the server instance. * * This function: * 1. Loads documentation sections from disk * 2. Registers a `tools/list` handler returning all available tools * 3. Registers a `tools/call` handler routing requests to appropriate handlers * * The tools registered provide documentation access and code analysis capabilities * for AI assistants working with Macroforge. * * @param server - The MCP Server instance to register tools with * * @example * ```typescript * import { Server } from '@modelcontextprotocol/sdk/server/index.js'; * import { registerTools } from './tools/index.js'; * * const server = new Server({ name: 'macroforge', version: '1.0.0' }, { capabilities: { tools: {} } }); * registerTools(server); * ``` */ export function registerTools(server: Server): void { // Load documentation sections sections = loadSections(); // Register tool listing server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'list-sections', description: `Lists all Macroforge documentation sections. Returns sections with: - title: Section name - use_cases: When this doc is useful (comma-separated keywords) - path: File path - category: Category name WORKFLOW: 1. Call list-sections FIRST for any Macroforge-related task 2. Analyze use_cases to find relevant sections 3. Call get-documentation with ALL relevant section names Example use_cases: "setup, install", "serialization, json", "validation, email"`, inputSchema: { type: 'object', properties: {}, }, }, { name: 'get-documentation', description: `Retrieves full documentation content for Macroforge sections. Supports flexible search by: - Title (e.g., "Debug", "Vite Plugin") - ID (e.g., "debug", "vite-plugin") - Partial matches Can accept a single section name or an array of sections. After calling list-sections, analyze the use_cases and fetch ALL relevant sections at once.`, inputSchema: { type: 'object', properties: { section: { anyOf: [ { type: 'string' }, { type: 'array', items: { type: 'string' } }, ], description: 'Section name(s) to retrieve. Supports single string or array of strings.', }, }, required: ['section'], }, }, { name: 'macroforge-autofixer', description: `Validates TypeScript code with @derive decorators using Macroforge's native validation. Returns structured JSON diagnostics with: - level: error | warning | info - message: What's wrong - location: Line and column number (when available) - help: Suggested fix (when available) - notes: Additional context (when available) - summary: Count of errors, warnings, and info messages This tool MUST be used before sending Macroforge code to the user. If require_another_tool_call_after_fixing is true, fix the issues and validate again. Detects: - Invalid/unknown macro names - Malformed @derive decorators - @serde validator issues (email, url, length, etc.) - Macro expansion failures - Syntax errors in generated code`, inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'TypeScript code with @derive decorators to validate', }, filename: { type: 'string', description: 'Filename for the code (default: input.ts)', }, }, required: ['code'], }, }, { name: 'expand-code', description: `Expands Macroforge macros in TypeScript code and returns the transformed result. Shows: - The fully expanded TypeScript code with all generated methods - Any diagnostics (errors, warnings, info) with line/column locations - Help text for fixing issues (when available) Useful for: - Seeing what code the macros generate - Understanding how @derive decorators transform your classes - Debugging macro expansion issues`, inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'TypeScript code with @derive decorators to expand', }, filename: { type: 'string', description: 'Filename for the code (default: input.ts)', }, }, required: ['code'], }, }, { name: 'get-macro-info', description: `Get documentation for Macroforge macros and decorators. Returns information about: - Macro descriptions (e.g., Debug, Serialize, Clone) - Decorator documentation (e.g., @serde, @debug field decorators) - Available macro options and configuration Use without parameters to get the full manifest of all available macros and decorators. Use with a name parameter to get info for a specific macro or decorator.`, inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Optional: specific macro or decorator name to look up', }, }, }, }, ], }; }); // Register tool call handler server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'list-sections': return handleListSections(); case 'get-documentation': return handleGetDocumentation(args as { section: string | string[] }); case 'macroforge-autofixer': return handleAutofixer(args as { code: string; filename?: string }); case 'expand-code': return handleExpandCode(args as { code: string; filename?: string }); case 'get-macro-info': return handleGetMacroInfo(args as { name?: string }); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } }); } /** * Handles the `list-sections` tool call. * * Returns a formatted list of all available documentation sections, filtering out * sub-chunks to show only top-level sections. Each section displays its title, * use cases, file path, and category. * * Sub-chunks (sections with a `parent_id`) are excluded from this list as they * are accessed through their parent section via `get-documentation`. * * @returns MCP response with formatted text listing all sections */ function handleListSections() { // Filter out sub-chunks (sections with parent_id) - only show top-level sections const topLevelSections = sections.filter((s) => !s.parent_id); const formatted = topLevelSections .map( (s) => `* title: [${s.title}], use_cases: [${s.use_cases}], path: [${s.path}], category: [${s.category_title}]` ) .join('\n'); return { content: [ { type: 'text' as const, text: `Available Macroforge documentation sections:\n\n${formatted}`, }, ], }; } /** * Handles the `get-documentation` tool call. * * Retrieves full documentation content for one or more sections. Supports flexible * lookups by ID, title, or partial match. * * ## Chunked Section Handling * * For large documentation sections that are split into chunks: * 1. Returns the first chunk's content as the main response * 2. Appends a list of additional chunk IDs that can be requested separately * * This allows clients to progressively load large documentation without * overwhelming context windows. * * @param args - Tool arguments * @param args.section - Section name(s) to retrieve (string or array of strings) * @returns MCP response with formatted markdown documentation content */ function handleGetDocumentation(args: { section: string | string[] }) { const sectionNames = Array.isArray(args.section) ? args.section : [args.section]; const results: string[] = []; for (const name of sectionNames) { const section = getSection(sections, name); if (section) { // Check if this is a chunked section if (section.is_chunked && section.chunk_ids && section.chunk_ids.length > 0) { // Get the first chunk const firstChunkId = section.chunk_ids[0]; const firstChunk = sections.find((s) => s.id === firstChunkId); if (firstChunk) { let result = `# ${section.title}\n\n${firstChunk.content}`; // Add list of other available chunks if (section.chunk_ids.length > 1) { const otherChunks = section.chunk_ids.slice(1); const chunkList = otherChunks .map((id) => { const chunk = sections.find((s) => s.id === id); return chunk ? `- \`${id}\`: ${chunk.title.replace(`${section.title}: `, '')}` : null; }) .filter(Boolean) .join('\n'); result += `\n\n---\n\n**This section has additional chunks available:**\n${chunkList}\n\nRequest specific chunks with \`get-documentation\` for more details.`; } results.push(result); } else { results.push(`# ${section.title}\n\nChunked content not found.`); } } else { // Regular section - return content directly results.push(`# ${section.title}\n\n${section.content}`); } } else { // Try fuzzy search const matches = searchSections(sections, name); if (matches.length > 0) { const match = matches[0]; // Handle chunked sections in fuzzy match too if (match.is_chunked && match.chunk_ids && match.chunk_ids.length > 0) { const firstChunk = sections.find((s) => s.id === match.chunk_ids![0]); if (firstChunk) { let result = `# ${match.title}\n\n${firstChunk.content}`; if (match.chunk_ids.length > 1) { const otherChunks = match.chunk_ids.slice(1); const chunkList = otherChunks .map((id) => { const chunk = sections.find((s) => s.id === id); return chunk ? `- \`${id}\`: ${chunk.title.replace(`${match.title}: `, '')}` : null; }) .filter(Boolean) .join('\n'); result += `\n\n---\n\n**This section has additional chunks available:**\n${chunkList}\n\nRequest specific chunks with \`get-documentation\` for more details.`; } results.push(result); } else { results.push(`# ${match.title}\n\n${match.content}`); } } else { results.push(`# ${match.title}\n\n${match.content}`); } } else { results.push(`Documentation for "${name}" not found.`); } } } return { content: [ { type: 'text' as const, text: results.join('\n\n---\n\n'), }, ], }; } /** * Handles the `macroforge-autofixer` tool call. * * Validates TypeScript code containing @derive decorators using Macroforge's * native Rust-based analyzer. Returns structured JSON diagnostics that clients * can use to provide error messages and fix suggestions. * * ## Diagnostic Levels * * - `error` - Invalid code that will fail to compile * - `warning` - Code that works but may have issues * - `info` - Informational messages about the code * * ## Response Format * * Returns JSON with: * - `diagnostics` - Array of issues with location, message, and help text * - `summary` - Counts of errors, warnings, and info messages * - `require_another_tool_call_after_fixing` - True if errors exist (should revalidate) * * @param args - Tool arguments * @param args.code - TypeScript source code to validate * @param args.filename - Optional filename for error messages (default: "input.ts") * @returns MCP response with JSON-formatted diagnostics */ async function handleAutofixer(args: { code: string; filename?: string }) { const filename = args.filename || 'input.ts'; try { const macroforge = await importMacroforge(); if (!macroforge) { return { content: [ { type: 'text' as const, text: JSON.stringify({ diagnostics: [{ level: 'error', message: 'Native Macroforge bindings not available. Install @macroforge/core.', }], summary: { errors: 1, warnings: 0, info: 0 }, require_another_tool_call_after_fixing: false, }, null, 2), }, ], }; } const result = macroforge.expandSync(args.code, filename, {}); const diagnostics = result.diagnostics || []; const output: AutofixerResult = { diagnostics: diagnostics.map((d) => ({ level: normalizeLevel(d.level), message: d.message, location: d.span ? { line: d.span.start.line, column: d.span.start.column } : undefined, help: d.help || undefined, notes: d.notes && d.notes.length > 0 ? d.notes : undefined, })), summary: { errors: diagnostics.filter((d) => d.level === 'Error').length, warnings: diagnostics.filter((d) => d.level === 'Warning').length, info: diagnostics.filter((d) => d.level === 'Info').length, }, require_another_tool_call_after_fixing: diagnostics.some((d) => d.level === 'Error'), }; return { content: [ { type: 'text' as const, text: JSON.stringify(output, null, 2), }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text' as const, text: JSON.stringify({ diagnostics: [{ level: 'error', message: `Error during analysis: ${message}`, }], summary: { errors: 1, warnings: 0, info: 0 }, require_another_tool_call_after_fixing: false, }, null, 2), }, ], }; } } /** * Handles the `expand-code` tool call. * * Expands Macroforge macros in TypeScript code and returns the transformed result. * This is useful for understanding what code the macros generate and debugging * macro expansion issues. * * ## Output Format * * Returns human-readable markdown with: * - The fully expanded TypeScript code in a code block * - Any diagnostics (errors, warnings, info) with line/column locations * - Help text for fixing issues when available * * @param args - Tool arguments * @param args.code - TypeScript source code with @derive decorators to expand * @param args.filename - Optional filename for error messages (default: "input.ts") * @returns MCP response with formatted expanded code and diagnostics */ async function handleExpandCode(args: { code: string; filename?: string }) { const filename = args.filename || 'input.ts'; try { const macroforge = await importMacroforge(); if (!macroforge) { return { content: [ { type: 'text' as const, text: 'Native Macroforge bindings not available. Install @macroforge/core to enable code expansion.', }, ], }; } const result = macroforge.expandSync(args.code, filename, {}); const diagnostics = result.diagnostics || []; // Build structured output const output: ExpandResult = { expandedCode: result.code, diagnostics: diagnostics.map((d) => ({ level: normalizeLevel(d.level), message: d.message, location: d.span?.start, help: d.help || undefined, })), hasErrors: diagnostics.some((d) => d.level === 'Error'), }; // Format human-readable text let text = `## Expanded Code\n\n\`\`\`typescript\n${result.code}\n\`\`\``; if (diagnostics.length > 0) { text += '\n\n## Diagnostics\n\n'; for (const d of diagnostics) { const loc = d.span ? ` (line ${d.span.start.line}, col ${d.span.start.column})` : ''; text += `- **[${normalizeLevel(d.level)}]**${loc} ${d.message}\n`; if (d.help) { text += ` - Help: ${d.help}\n`; } if (d.notes && d.notes.length > 0) { for (const note of d.notes) { text += ` - Note: ${note}\n`; } } } } return { content: [{ type: 'text' as const, text }], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text' as const, text: `Error expanding code: ${message}`, }, ], }; } } /** * Handles the `get-macro-info` tool call. * * Retrieves documentation for Macroforge macros and field decorators from the * native manifest. Can return info for a specific macro/decorator or the full * manifest of all available macros and decorators. * * ## Usage Modes * * - **Without name**: Returns full manifest with all macros and decorators * - **With name**: Returns detailed info for the specific macro or decorator * * ## Manifest Contents * * - **Macros**: @derive decorators like Debug, Serialize, Clone * - **Decorators**: Field decorators like @serde.skip, @serde.rename * * @param args - Tool arguments * @param args.name - Optional macro or decorator name to look up * @returns MCP response with formatted macro/decorator documentation */ async function handleGetMacroInfo(args: { name?: string }) { try { const macroforge = await importMacroforge(); if (!macroforge || !macroforge.__macroforgeGetManifest) { return { content: [ { type: 'text' as const, text: 'Native Macroforge bindings not available. Install @macroforge/core to access macro documentation.', }, ], }; } const manifest = macroforge.__macroforgeGetManifest(); if (args.name) { // Look up specific macro or decorator const nameLower = args.name.toLowerCase(); const macro = manifest.macros.find(m => m.name.toLowerCase() === nameLower); const decorator = manifest.decorators.find(d => d.export.toLowerCase() === nameLower); if (!macro && !decorator) { return { content: [ { type: 'text' as const, text: `No macro or decorator found with name "${args.name}". Available macros: ${manifest.macros.map(m => m.name).join(', ')} Available decorators: ${manifest.decorators.map(d => d.export).join(', ')}`, }, ], }; } let result = ''; if (macro) { result += `## Macro: @derive(${macro.name})\n\n`; result += `**Description:** ${macro.description || 'No description available'}\n`; result += `**Kind:** ${macro.kind}\n`; result += `**Package:** ${macro.package}\n`; } if (decorator) { if (result) result += '\n---\n\n'; result += `## Decorator: @${decorator.export}\n\n`; result += `**Documentation:** ${decorator.docs || 'No documentation available'}\n`; result += `**Kind:** ${decorator.kind}\n`; result += `**Module:** ${decorator.module}\n`; } return { content: [{ type: 'text' as const, text: result }], }; } // Return full manifest let result = '# Macroforge Macro Manifest\n\n'; result += '## Available Macros\n\n'; for (const macro of manifest.macros) { result += `### @derive(${macro.name})\n`; result += `${macro.description || 'No description'}\n\n`; } if (manifest.decorators.length > 0) { result += '## Available Field Decorators\n\n'; for (const decorator of manifest.decorators) { result += `### @${decorator.export}\n`; result += `${decorator.docs || 'No documentation'}\n\n`; } } return { content: [{ type: 'text' as const, text: result }], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text' as const, text: `Error getting macro info: ${message}`, }, ], }; } } // ============================================================================ // Types - Match Rust's Diagnostic structure from macroforge_ts_syn/src/abi/patch.rs // ============================================================================ /** * Represents a location span in source code. * * Used by diagnostics to indicate where an error or warning occurred. * Matches the Rust `Span` type from the macroforge_ts_syn crate. * * @property start - Starting position (line and column, 1-indexed) * @property end - Ending position (line and column, 1-indexed) */ interface DiagnosticSpan { start: { line: number; column: number }; end: { line: number; column: number }; } /** * Represents a diagnostic message from the Macroforge analyzer. * * Diagnostics are produced during code validation and expansion to report * errors, warnings, and informational messages. Matches the Rust `Diagnostic` * type from macroforge_ts_syn/src/abi/patch.rs. * * @property level - Severity level: 'Error', 'Warning', or 'Info' * @property message - Human-readable description of the issue * @property span - Optional source location where the issue occurred * @property notes - Additional context or explanatory notes * @property help - Optional suggestion for how to fix the issue */ interface Diagnostic { level: 'Error' | 'Warning' | 'Info'; message: string; span?: DiagnosticSpan; notes: string[]; help?: string; } /** * Metadata for a Macroforge macro in the manifest. * * Describes a @derive macro that can be applied to classes. * * @property name - Macro name as used in @derive (e.g., "Debug", "Serialize") * @property kind - Type of macro (e.g., "derive") * @property description - Human-readable description of what the macro does * @property package - Package that provides this macro */ interface MacroManifestEntry { name: string; kind: string; description: string; package: string; } /** * Metadata for a field decorator in the manifest. * * Describes a decorator that can be applied to class fields to customize * macro behavior (e.g., @serde.skip, @debug.format). * * @property module - Module path where the decorator is defined * @property export - Export name of the decorator * @property kind - Type of decorator (e.g., "field") * @property docs - Documentation string for the decorator */ interface DecoratorManifestEntry { module: string; export: string; kind: string; docs: string; } /** * Complete manifest of available Macroforge macros and decorators. * * Returned by the native bindings to provide documentation and metadata * for all available macros and field decorators. * * @property version - Manifest format version * @property macros - Array of available @derive macros * @property decorators - Array of available field decorators */ interface MacroManifest { version: number; macros: MacroManifestEntry[]; decorators: DecoratorManifestEntry[]; } /** * Interface for the native Macroforge module (@macroforge/core). * * Defines the expected API surface of the optional native bindings that * provide code validation, expansion, and manifest access. * * @property expandSync - Synchronously expands macros in TypeScript code * @property __macroforgeGetManifest - Optional function to retrieve the macro manifest */ interface MacroforgeModule { /** * Synchronously expands Macroforge macros in TypeScript code. * * @param code - TypeScript source code with @derive decorators * @param filename - Filename for error reporting * @param options - Expansion options (currently unused) * @returns Object with expanded code and any diagnostics */ expandSync: (code: string, filename: string, options: object) => { code: string; diagnostics?: Diagnostic[]; }; /** * Retrieves the macro manifest with all available macros and decorators. * Optional - may not be available in all versions. */ __macroforgeGetManifest?: () => MacroManifest; } // ============================================================================ // Output types for structured responses // ============================================================================ /** * Structured output format for the `macroforge-autofixer` tool. * * Provides a JSON response that clients can parse to display errors, * navigate to problem locations, and determine if re-validation is needed. * * @property diagnostics - Array of diagnostic messages with locations * @property summary - Counts of errors, warnings, and info messages * @property require_another_tool_call_after_fixing - True if errors exist and client should revalidate after fixing */ interface AutofixerResult { diagnostics: Array<{ /** Severity level: "error", "warning", or "info" */ level: string; /** Human-readable description of the issue */ message: string; /** Source location (line and column) if available */ location?: { line: number; column: number }; /** Suggested fix for the issue */ help?: string; /** Additional context or explanatory notes */ notes?: string[]; }>; /** Summary counts for quick overview */ summary: { errors: number; warnings: number; info: number; }; /** If true, client should fix issues and call autofixer again */ require_another_tool_call_after_fixing: boolean; } /** * Structured output format for the `expand-code` tool. * * Contains the fully expanded code along with any diagnostics produced * during expansion. * * @property expandedCode - The TypeScript code after macro expansion * @property diagnostics - Array of diagnostic messages from expansion * @property hasErrors - True if any error-level diagnostics were produced */ interface ExpandResult { expandedCode: string; diagnostics: Array<{ /** Severity level: "error", "warning", or "info" */ level: string; /** Human-readable description of the issue */ message: string; /** Source location (line and column) if available */ location?: { line: number; column: number }; /** Suggested fix for the issue */ help?: string; }>; hasErrors: boolean; } // ============================================================================ // Helper functions // ============================================================================ /** * Dynamically imports the native Macroforge bindings. * * The @macroforge/core package is an optional peer dependency that provides * native Rust-based code analysis and expansion. This function attempts to * load it at runtime and gracefully returns null if unavailable. * * Using dynamic import allows the MCP server to run and serve documentation * even when the native bindings are not installed. * * @returns The Macroforge module if available, or null if not installed * * @example * ```typescript * const macroforge = await importMacroforge(); * if (macroforge) { * const result = macroforge.expandSync(code, 'input.ts', {}); * } * ``` */ async function importMacroforge(): Promise<MacroforgeModule | null> { try { // Dynamic import to avoid build-time errors when @macroforge/core is not installed // @ts-expect-error - dynamic import of optional dependency const mod = await import('@macroforge/core'); return mod as MacroforgeModule; } catch { // Package not installed - return null to indicate unavailability return null; } } /** * Normalizes a diagnostic level string to lowercase. * * The Rust analyzer returns levels as PascalCase ('Error', 'Warning', 'Info'), * but JSON output should use lowercase ('error', 'warning', 'info') for * consistency with common diagnostic formats. * * @param level - Diagnostic level from Rust (e.g., 'Error') * @returns Lowercase level string (e.g., 'error') */ function normalizeLevel(level: string): string { return level.toLowerCase(); }

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/macroforge-ts/mcp-server'

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