Skip to main content
Glama
ToolRegistry.ts10.8 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WordPressClient } from "@/client/api.js"; import { getErrorMessage } from "@/utils/error.js"; import { EnhancedError, ErrorHandlers } from "@/utils/enhancedError.js"; import { config } from "@/config/Config.js"; import * as Tools from "@/tools/index.js"; import { z } from "zod"; import type { MCPToolSchema, JSONSchemaProperty } from "@/types/mcp.js"; /** * Interface for tool definition */ export interface ToolDefinition { name: string; description?: string; parameters?: Array<{ name: string; type?: string; description?: string; required?: boolean; enum?: string[]; items?: { type?: string }; }>; inputSchema?: MCPToolSchema; handler: (client: WordPressClient, args: Record<string, unknown>) => Promise<unknown>; } /** * Registry for managing MCP tools * Handles tool registration, parameter validation, and execution */ export class ToolRegistry { // Exposed for tests that assert presence of these fields public server: McpServer; public wordpressClients: Map<string, WordPressClient>; constructor(server: McpServer, wordpressClients: Map<string, WordPressClient>) { this.server = server; this.wordpressClients = wordpressClients; } /** * Register all available tools with the MCP server */ public registerAllTools(): void { // Register all tools from the tools directory Object.values(Tools).forEach((ToolClass) => { let toolInstance: { getTools(): unknown[] }; // Cache and Performance tools need the clients map if (ToolClass.name === "CacheTools" || ToolClass.name === "PerformanceTools") { toolInstance = new ToolClass(this.wordpressClients); } else { toolInstance = new (ToolClass as new () => { getTools(): unknown[] })(); } const tools = toolInstance.getTools(); tools.forEach((tool: unknown) => { this.registerTool(tool as ToolDefinition); }); }); } /** * Register a single tool with parameter validation and execution handling */ private registerTool(tool: ToolDefinition): void { // Create base parameter schema with site parameter const baseSchema = { site: z .string() .optional() .describe( "The ID of the WordPress site to target (from mcp-wordpress.config.json). Required if multiple sites are configured.", ), }; // Merge with tool-specific parameters const parameterSchema = this.buildParameterSchema(tool, baseSchema); // Make site parameter required if multiple sites are configured if ( this.wordpressClients.size > 1 && parameterSchema.site && typeof parameterSchema.site === "object" && "describe" in parameterSchema.site ) { parameterSchema.site = (parameterSchema.site as z.ZodString).describe( "The ID of the WordPress site to target (from mcp-wordpress.config.json). Required when multiple sites are configured.", ); } this.server.tool( tool.name, tool.description || `WordPress tool: ${tool.name}`, parameterSchema, async (args: Record<string, unknown>) => { try { let siteId = args.site; // If no site specified and multiple sites configured, require site parameter if (!siteId && this.wordpressClients.size > 1) { const availableSites = Array.from(this.wordpressClients.keys()); const error = ErrorHandlers.siteParameterMissing(availableSites); return { content: [ { type: "text" as const, text: error.toString(), }, ], isError: true, }; } // Intelligent site selection for single-site configurations if (!siteId) { siteId = this.selectBestSite(tool.name, args); } const client = this.wordpressClients.get(siteId as string); if (!client) { const availableSites = Array.from(this.wordpressClients.keys()); const error = ErrorHandlers.siteNotFound(siteId as string, availableSites); return { content: [ { type: "text" as const, text: error.toString(), }, ], isError: true, }; } // Call the tool handler with the client and parameters const result = await tool.handler(client, args); return { content: [ { type: "text" as const, text: typeof result === "string" ? result : JSON.stringify(result, null, 2), }, ], }; } catch (_error) { if (this.isAuthenticationError(_error)) { return { content: [ { type: "text" as const, text: `Authentication failed for site '${args.site || "default"}'. Please check your credentials.`, }, ], isError: true, }; } // Handle enhanced errors with suggestions if (_error instanceof EnhancedError) { return { content: [ { type: "text" as const, text: _error.toString(), }, ], isError: true, }; } return { content: [ { type: "text" as const, text: `Error: ${getErrorMessage(_error)}`, }, ], isError: true, }; } }, ); } /** * Build Zod parameter schema from tool definition */ private buildParameterSchema(tool: ToolDefinition, baseSchema: Record<string, unknown>): Record<string, unknown> { // If tool has inputSchema (new format), convert it to Zod schema if (tool.inputSchema) { const schema = { ...baseSchema }; const properties = tool.inputSchema.properties || {}; const required = tool.inputSchema.required || []; for (const [propName, propDef] of Object.entries(properties)) { let zodType = this.getZodTypeForProperty(propDef); if (propDef.description) { zodType = zodType.describe(propDef.description); } if (!required.includes(propName)) { zodType = zodType.optional(); } schema[propName] = zodType; } return schema; } // Fall back to old parameters format return ( tool.parameters?.reduce( ( schema: Record<string, unknown>, param: { name: string; type?: string; required?: boolean; [key: string]: unknown }, ) => { let zodType = this.getZodTypeForParameter(param); if (param.description) { zodType = zodType.describe(param.description as string); } if (!param.required) { zodType = zodType.optional(); } schema[param.name] = zodType; return schema; }, { ...baseSchema }, ) || baseSchema ); } /** * Get appropriate Zod type for inputSchema property definition */ private getZodTypeForProperty(propDef: JSONSchemaProperty): z.ZodType { // Handle enum types if (propDef.enum && propDef.enum.length > 0) { const enumValues = propDef.enum as [string | number, ...(string | number)[]]; return z.enum(enumValues as [string, ...string[]]); } // Handle array types if (propDef.type === "array") { const itemType = propDef.items?.type || "string"; switch (itemType) { case "number": return z.array(z.number()); case "boolean": return z.array(z.boolean()); case "object": return z.array(z.record(z.string(), z.unknown())); default: return z.array(z.string()); } } // Handle primitive types switch (propDef.type) { case "string": return z.string(); case "number": return z.number(); case "boolean": return z.boolean(); case "object": return z.record(z.string(), z.unknown()); default: return z.string(); } } /** * Get appropriate Zod type for parameter definition (old format) */ private getZodTypeForParameter(param: { type?: string; required?: boolean; [key: string]: unknown }): z.ZodType { switch (param.type) { case "string": return z.string(); case "number": return z.number(); case "boolean": return z.boolean(); case "array": return z.array(z.string()); case "object": return z.record(z.string(), z.unknown()); default: return z.string(); } } /** * Intelligent site selection based on context */ private selectBestSite(toolName: string, args: Record<string, unknown>): string { const availableSites = Array.from(this.wordpressClients.keys()); // Single site scenario - use it directly if (availableSites.length === 1) { return availableSites[0]; } // Multiple sites scenario - intelligent selection if (availableSites.length > 1) { // Try to find a site based on context clues // 1. Check if there's a 'default' site if (availableSites.includes("default")) { return "default"; } // 2. Check if there's a 'main' or 'primary' site const primarySites = availableSites.filter((site) => ["main", "primary", "prod", "production"].includes(site.toLowerCase()), ); if (primarySites.length > 0) { return primarySites[0]; } // 3. For development/test operations, prefer dev sites if (toolName.includes("test") || config().app.isDevelopment) { const devSites = availableSites.filter((site) => ["dev", "test", "staging", "local"].includes(site.toLowerCase()), ); if (devSites.length > 0) { return devSites[0]; } } // 4. Default to first available site return availableSites[0]; } // Fallback to 'default' if no sites available return "default"; } /** * Check if error is authentication-related */ private isAuthenticationError(error: unknown): boolean { const errorObj = error as { response?: { status?: number }; code?: string }; if (errorObj?.response?.status && [401, 403].includes(errorObj.response.status)) { return true; } return errorObj?.code === "WORDPRESS_AUTH_ERROR"; } }

Implementation Reference

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/docdyhr/mcp-wordpress'

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