reactbits-mcp-server
Version:
MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements
235 lines (234 loc) • 11 kB
JavaScript
/**
* 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 { resourceTemplateHandlers, 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(), category: z.string().optional() };
const searchSchema = { query: z.string(), 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 Bits component',
inputSchema: {
type: 'object',
properties: {
componentName: {
type: 'string',
description: 'Name of the React Bits component (e.g., "AnimatedList", "BlobCursor")',
},
category: {
type: 'string',
description: 'Category of the component (Animations, Backgrounds, Components, TextAnimations)',
enum: ['Animations', 'Backgrounds', 'Components', 'TextAnimations']
}
},
required: ['componentName'],
},
},
{
name: 'get_component_demo',
description: 'Get demo code illustrating how a React Bits component should be used',
inputSchema: {
type: 'object',
properties: {
componentName: {
type: 'string',
description: 'Name of the React Bits component (e.g., "AnimatedList", "BlobCursor")',
},
category: {
type: 'string',
description: 'Category of the component (Animations, Backgrounds, Components, TextAnimations)',
enum: ['Animations', 'Backgrounds', 'Components', 'TextAnimations']
}
},
required: ['componentName'],
},
},
{
name: 'list_components',
description: 'Get all available React Bits components',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Filter by category (Animations, Backgrounds, Components, TextAnimations)',
enum: ['Animations', 'Backgrounds', 'Components', 'TextAnimations']
}
},
},
},
{
name: 'get_component_metadata',
description: 'Get metadata for a specific React Bits component',
inputSchema: {
type: 'object',
properties: {
componentName: {
type: 'string',
description: 'Name of the React Bits component (e.g., "AnimatedList", "BlobCursor")',
},
category: {
type: 'string',
description: 'Category of the component (Animations, Backgrounds, Components, TextAnimations)',
enum: ['Animations', 'Backgrounds', 'Components', 'TextAnimations']
}
},
required: ['componentName'],
},
},
{
name: 'search_components',
description: 'Search for React Bits components by name or functionality',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (e.g., "animation", "cursor", "text")',
},
category: {
type: 'string',
description: 'Filter by category (Animations, Backgrounds, Components, TextAnimations)',
enum: ['Animations', 'Backgrounds', 'Components', 'TextAnimations']
}
},
required: ['query'],
},
},
];
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 result;
}
// Check if this is a generated resource from a template
for (const [pattern, handler] of Object.entries(resourceTemplateHandlers)) {
const regex = new RegExp(pattern.replace(/\{[^}]+\}/g, '(.+)'));
if (regex.test(uri)) {
const result = await Promise.resolve(handler(uri));
return result;
}
}
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_demo':
case 'get_component_metadata':
return z.object(componentSchema);
case 'search_components':
return z.object(searchSchema);
case 'list_components':
return z.object({ category: z.string().optional() });
default:
return undefined;
}
}
catch (error) {
logError('Schema error', error);
return undefined;
}
}