UNPKG

rnr-mcp-server

Version:

A Model Context Protocol (MCP) server for React Native Reusables components, providing AI assistants with access to component source code, demos, and metadata for React Native development.

271 lines (270 loc) 12 kB
/** * Request handler setup for the Model Context Protocol (MCP) server. * * This file configures how the server responds to various MCP requests by setting up * handlers for resources, resource templates, tools, and prompts. * * Updated for MCP SDK 1.16.0 with improved error handling and request processing. */ import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { resourceHandlers, resources } from "./resources.js"; import { promptHandlers, prompts } from "./prompts.js"; import { toolHandlers } from "./tools/index.js"; import { getResourceTemplate, resourceTemplates, } from "./resource-templates.js"; import { z } from "zod"; import { validateAndSanitizeParams } from './utils/validation.js'; import { circuitBreakers } from './utils/circuit-breaker.js'; import { logError, logInfo } from './utils/logger.js'; // Define basic component schemas here for tool validation const componentSchema = { componentName: z.string() }; const searchSchema = { query: z.string() }; const themesSchema = { query: z.string().optional() }; const blocksSchema = { query: z.string().optional(), category: z.string().optional() }; /** * Wrapper function to handle requests with simple error handling */ async function handleRequest(method, params, handler) { try { // Validate and sanitize input parameters const validatedParams = validateAndSanitizeParams(method, params); // Execute the handler with circuit breaker protection for external calls const result = await circuitBreakers.external.execute(() => handler(validatedParams)); return result; } catch (error) { logError(`Error in ${method}`, error); throw error; } } /** * Sets up all request handlers for the MCP server * Following MCP SDK 1.16.0 best practices for handler registration * @param server - The MCP server instance */ export const setupHandlers = (server) => { logInfo('Setting up request handlers...'); // List available resources when clients request them server.setRequestHandler(ListResourcesRequestSchema, async (request) => { return await handleRequest('list_resources', request.params, async () => ({ resources })); }); // Resource Templates server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request) => { return await handleRequest('list_resource_templates', request.params, async () => ({ resourceTemplates })); }); // List available tools server.setRequestHandler(ListToolsRequestSchema, async (request) => { return await handleRequest('list_tools', request.params, async () => { // Return the tools that are registered with the server const registeredTools = [ { name: 'get_component', description: 'Get the source code for a specific React Native Reusables component', inputSchema: { type: 'object', properties: { componentName: { type: 'string', description: 'Name of the React Native Reusables component (e.g., "accordion", "button")', }, }, required: ['componentName'], }, }, { name: 'get_component_demo', description: 'Get demo code illustrating how a React Native Reusables component should be used', inputSchema: { type: 'object', properties: { componentName: { type: 'string', description: 'Name of the React Native Reusables component (e.g., "accordion", "button")', }, }, required: ['componentName'], }, }, { name: 'list_components', description: 'Get all available React Native Reusables components', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_component_metadata', description: 'Get metadata for a specific React Native Reusables component', inputSchema: { type: 'object', properties: { componentName: { type: 'string', description: 'Name of the React Native Reusables component (e.g., "accordion", "button")', }, }, required: ['componentName'], }, }, { name: 'get_directory_structure', description: 'Get the directory structure of the React Native Reusables repository', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Path within the repository (default: packages/ui/src)', }, owner: { type: 'string', description: 'Repository owner (default: "mrzachnugent")', }, repo: { type: 'string', description: 'Repository name (default: "react-native-reusables")', }, branch: { type: 'string', description: 'Branch name (default: "main")', }, }, }, }, { name: 'get_block', description: 'Get source code for a specific React Native Reusables block (e.g., calendar-01, dashboard-01)', inputSchema: { type: 'object', properties: { blockName: { type: 'string', description: 'Name of the block (e.g., "calendar-01", "dashboard-01", "login-02")', }, includeComponents: { type: 'boolean', description: 'Whether to include component files for complex blocks (default: true)', }, }, required: ['blockName'], }, }, { name: 'list_blocks', description: 'Get all available React Native Reusables blocks with categorization', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'Filter by category (calendar, dashboard, login, sidebar, products)', }, }, }, }, ]; return { tools: registeredTools }; }); }); // Return resource content when clients request it server.setRequestHandler(ReadResourceRequestSchema, async (request) => { return await handleRequest('read_resource', request.params, async (validatedParams) => { const { uri } = validatedParams; // Check if this is a static resource const resourceHandler = resourceHandlers[uri]; if (resourceHandler) { const result = await Promise.resolve(resourceHandler()); return { contentType: result.contentType, contents: [{ uri: uri, text: result.content }] }; } // Check if this is a generated resource from a template const resourceTemplateHandler = getResourceTemplate(uri); if (resourceTemplateHandler) { const result = await Promise.resolve(resourceTemplateHandler()); return { contentType: result.contentType, contents: [{ uri: uri, text: result.content }] }; } throw new Error(`Resource not found: ${uri}`); }); }); // List available prompts server.setRequestHandler(ListPromptsRequestSchema, async (request) => { return await handleRequest('list_prompts', request.params, async () => ({ prompts: Object.values(prompts) })); }); // Get specific prompt content with optional arguments server.setRequestHandler(GetPromptRequestSchema, async (request) => { return await handleRequest('get_prompt', request.params, async (validatedParams) => { const { name, arguments: args } = validatedParams; const promptHandler = promptHandlers[name]; if (!promptHandler) { throw new Error(`Prompt not found: ${name}`); } return promptHandler(args); }); }); // Tool request Handler - executes the requested tool with provided parameters server.setRequestHandler(CallToolRequestSchema, async (request) => { return await handleRequest('call_tool', request.params, async (validatedParams) => { const { name, arguments: params } = validatedParams; if (!name || typeof name !== 'string') { throw new Error("Tool name is required"); } const handler = toolHandlers[name]; if (!handler) { throw new Error(`Tool not found: ${name}`); } // Execute handler with circuit breaker protection const result = await circuitBreakers.external.execute(() => Promise.resolve(handler(params || {}))); return result; }); }); // Add global error handler server.onerror = (error) => { logError('MCP server error', error); }; logInfo('Handlers setup complete'); }; /** * Get Zod schema for tool validation if available * Following MCP SDK 1.16.0 best practices for schema validation * @param toolName Name of the tool * @returns Zod schema or undefined */ function getToolSchema(toolName) { try { switch (toolName) { case 'get_component': case 'get_component_details': return z.object(componentSchema); case 'get_examples': return z.object(componentSchema); case 'get_usage': return z.object(componentSchema); case 'search_components': return z.object(searchSchema); case 'get_themes': return z.object(themesSchema); case 'get_blocks': return z.object(blocksSchema); default: return undefined; } } catch (error) { logError('Schema error', error); return undefined; } }