Skip to main content
Glama
handlers.tsβ€’16.8 kB
/** * Request handlers for prompt endpoints */ import { Request, Response } from 'express'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { ServerContext } from '@/server/createServer.js'; import { setGlobalContext } from '@/api/lazy-client.js'; import { getAllPrompts, getPromptById, getPromptsByCategory, getAllCategories, } from '@/prompts/templates/index.js'; import { PromptTemplate, PromptExecutionRequest } from '@/prompts/types.js'; import { createErrorResult } from '@/prompts/error-handler.js'; import { getPromptsListPayload } from '@/utils/mcp-discovery.js'; import { getAllPromptsV1, getPromptV1ByName, isV1Prompt } from './v1/index.js'; import { validateArguments, checkTokenBudget, createValidationError, createBudgetExceededError, } from './v1/utils/validation.js'; import { calculatePromptTokens } from './v1/utils/token-metadata.js'; import { logPromptTelemetry, createTelemetryEvent, } from './v1/utils/telemetry.js'; import { isDevMetaEnabled } from './v1/constants.js'; // Import Handlebars using ES module import // This avoids the "require is not defined in ES module scope" error import Handlebars from 'handlebars'; // Define template delegate type since it's not exported by the Handlebars module type HandlebarsTemplateDelegate = ( context: Record<string, unknown>, options?: Record<string, unknown> ) => string; /** * Interface for template cache options */ interface TemplateCacheOptions { maxSize: number; } /** * Template cache implementation for storing compiled Handlebars templates * Provides caching with size limits to prevent memory leaks */ class TemplateCache { private cache = new Map<string, HandlebarsTemplateDelegate>(); private options: TemplateCacheOptions; /** * Create a new template cache * * @param options - Cache configuration options */ constructor(options: Partial<TemplateCacheOptions> = {}) { this.options = { maxSize: 100, // Sized for typical production load (50-100 unique prompts with parameter variations) ...options, }; } /** * Get a compiled template from the cache * * @param key - Template key * @returns The compiled template or undefined if not found */ get(key: string): HandlebarsTemplateDelegate | undefined { return this.cache.get(key); } /** * Store a compiled template in the cache * * @param key - Template key * @param template - Compiled template to store */ set(key: string, template: HandlebarsTemplateDelegate): void { // Check if we need to evict entries (simple LRU implementation) if (this.cache.size >= this.options.maxSize && !this.cache.has(key)) { // Delete the first entry (oldest) const firstKeyValue = this.cache.keys().next(); if (!firstKeyValue.done && firstKeyValue.value) { this.cache.delete(firstKeyValue.value); } } this.cache.set(key, template); } /** * Check if a template exists in the cache * * @param key - Template key * @returns True if the template exists in the cache */ has(key: string): boolean { return this.cache.has(key); } /** * Clear all templates from the cache */ clear(): void { this.cache.clear(); } /** * Get the current size of the cache * * @returns Number of templates in the cache */ size(): number { return this.cache.size; } } // Create the template cache instance const templateCache = new TemplateCache({ maxSize: 100 }); // Register Handlebars helpers Handlebars.registerHelper( 'if', function ( this: Record<string, unknown>, conditional: unknown, options: { fn: (context: unknown) => string; inverse: (context: unknown) => string; } ): string { if (conditional) { return options.fn(this); } else { return options.inverse(this); } } ); /** * List all available prompts * * @param req - Express request object * @param res - Express response object */ export async function listPrompts(req: Request, res: Response): Promise<void> { try { const category = req.query.category as string | undefined; const prompts = category ? getPromptsByCategory(category) : getAllPrompts(); res.json({ success: true, data: prompts, }); } catch (error: unknown) { const errorObj = new Error('Failed to list prompts'); const errorResult = createErrorResult( errorObj, error instanceof Error ? error.message : 'Unknown error', 500, { ...getRequestMetadata(req, 'prompts.list'), context: { category: req.query.category, }, } ); res.status(errorResult.error.code).json(errorResult); } } /** * List all available prompt categories * * @param req - Express request object * @param res - Express response object */ export async function listPromptCategories( req: Request, res: Response ): Promise<void> { try { const categories = getAllCategories(); res.json({ success: true, data: categories, }); } catch (error: unknown) { const errorObj = new Error('Failed to list prompt categories'); const errorResult = createErrorResult( errorObj, error instanceof Error ? error.message : 'Unknown error', 500, { ...getRequestMetadata(req, 'prompts.categories'), context: {}, } ); res.status(errorResult.error.code).json(errorResult); } } /** * Get details for a specific prompt * * @param req - Express request object * @param res - Express response object */ export async function getPromptDetails( req: Request, res: Response ): Promise<void> { const promptId = req.params.id; try { const prompt = getPromptById(promptId); if (!prompt) { const errorObj = new Error('Prompt not found'); const errorResult = createErrorResult( errorObj, `No prompt found with ID: ${promptId}`, 404, { ...getRequestMetadata(req, 'prompts.get'), context: { promptId }, } ); res.status(errorResult.error.code).json(errorResult); return; } res.json({ success: true, data: prompt, }); } catch (error: unknown) { const errorObj = new Error('Failed to get prompt details'); const errorResult = createErrorResult( errorObj, error instanceof Error ? error.message : 'Unknown error', 500, { ...getRequestMetadata(req, 'prompts.get'), context: { promptId }, } ); res.status(errorResult.error.code).json(errorResult); } } /** * Validate parameters against prompt definition * * @param prompt - Prompt template to validate against * @param parameters - Parameters to validate * @returns Object with validation result and any error messages */ function validateParameters( prompt: PromptTemplate, parameters: Record<string, unknown> ): { valid: boolean; errors: string[] } { const errors: string[] = []; // Check for required parameters prompt.parameters.forEach((param) => { if ( param.required && (parameters[param.name] === undefined || parameters[param.name] === null) ) { errors.push(`Missing required parameter: ${param.name}`); } }); // Check enum values prompt.parameters.forEach((param) => { if ( param.enum && parameters[param.name] !== undefined && typeof parameters[param.name] === 'string' && !param.enum.includes(parameters[param.name] as string) ) { errors.push( `Invalid value for parameter ${param.name}. ` + `Expected one of: ${param.enum.join(', ')}` ); } }); // Check parameter types prompt.parameters.forEach((param) => { if (parameters[param.name] !== undefined) { const paramValue = parameters[param.name]; let typeError = false; switch (param.type as string) { case 'string': typeError = typeof paramValue !== 'string'; break; case 'number': typeError = typeof paramValue !== 'number'; break; case 'boolean': typeError = typeof paramValue !== 'boolean'; break; case 'array': typeError = !Array.isArray(paramValue); break; case 'object': typeError = typeof paramValue !== 'object' || Array.isArray(paramValue) || paramValue === null; break; } if (typeError) { errors.push( `Invalid type for parameter ${param.name}. Expected ${param.type}.` ); } } }); return { valid: errors.length === 0, errors, }; } /** * Apply default values to missing parameters * * @param prompt - Prompt template with parameter definitions * @param parameters - User-provided parameters * @returns Parameters with defaults applied */ function applyDefaultValues( prompt: PromptTemplate, parameters: Record<string, unknown> ): Record<string, unknown> { const result = { ...parameters }; prompt.parameters.forEach((param) => { if ( (result[param.name] === undefined || result[param.name] === null) && param.default !== undefined ) { result[param.name] = param.default; } }); return result; } /** * Execute a prompt with provided parameters * * @param req - Express request object * @param res - Express response object */ export async function executePrompt( req: Request, res: Response ): Promise<void> { const promptId = req.params.id; try { const prompt = getPromptById(promptId); if (!prompt) { const errorObj = new Error('Prompt not found'); const errorResult = createErrorResult( errorObj, `No prompt found with ID: ${promptId}`, 404 ); res.status(errorResult.error.code).json(errorResult); return; } const executionRequest = req.body as PromptExecutionRequest; // Validate parameters const validation = validateParameters(prompt, executionRequest.parameters); if (!validation.valid) { const errorObj = new Error('Invalid parameters'); const errorResult = createErrorResult( errorObj, validation.errors.join(', '), 400, { ...getRequestMetadata(req, 'prompts.execute'), context: { promptId, validationErrors: validation.errors, }, } ); res.status(errorResult.error.code).json(errorResult); return; } // Apply default values const parameters = applyDefaultValues(prompt, executionRequest.parameters); // Get or compile template with caching let template = templateCache.get(promptId); if (!template) { try { template = Handlebars.compile(prompt.template); templateCache.set(promptId, template); } catch (compileError: unknown) { const errorObj = new Error('Failed to compile template'); const errorResult = createErrorResult( errorObj, `Template compilation error for prompt ${promptId}: ${ compileError instanceof Error ? compileError.message : 'Unknown error' }`, 500, { ...getRequestMetadata(req, 'prompts.execute'), context: { promptId, stage: 'compile', }, } ); res.status(errorResult.error.code).json(errorResult); return; } } // Execute the template with error handling let result: string; try { result = template(parameters); } catch (renderError: unknown) { const errorObj = new Error('Failed to render template'); const errorResult = createErrorResult( errorObj, `Template rendering error for prompt ${promptId}: ${ renderError instanceof Error ? renderError.message : 'Unknown error' }`, 500, { ...getRequestMetadata(req, 'prompts.execute'), context: { promptId, stage: 'render', }, } ); res.status(errorResult.error.code).json(errorResult); return; } res.json({ success: true, data: { prompt: prompt.id, result, }, }); } catch (error: unknown) { const errorObj = new Error('Failed to execute prompt'); const errorResult = createErrorResult( errorObj, error instanceof Error ? error.message : 'Unknown error', 500, { ...getRequestMetadata(req, 'prompts.execute'), context: { promptId, stage: 'unhandled', }, } ); res.status(errorResult.error.code).json(errorResult); } } /** * Register MCP prompt handlers with the server * * This function registers handlers for the MCP prompts/list and prompts/get endpoints * required by the Model Context Protocol specification. These endpoints enable * Claude Desktop to discover and retrieve prompt templates from the server. * * Supports both legacy Handlebars prompts and new v1 MCP-compliant prompts. * * @param server - MCP server instance * @param context - Optional server context * @example * ```typescript * const server = new Server(); * registerPromptHandlers(server); * ``` */ export function registerPromptHandlers( server: Server, context?: ServerContext ): void { // Set the global context for lazy initialization if provided if (context) { setGlobalContext(context); } // Register handler for prompts/list endpoint server.setRequestHandler(ListPromptsRequestSchema, async () => { const legacyPrompts = getPromptsListPayload().prompts; const v1Prompts = getAllPromptsV1().map((p) => ({ name: p.metadata.name, description: p.metadata.description, arguments: p.arguments.map((arg) => ({ name: arg.name, description: arg.description, required: arg.required, })), })); return { prompts: [...legacyPrompts, ...v1Prompts], }; }); // Register handler for prompts/get endpoint server.setRequestHandler(GetPromptRequestSchema, async (request) => { const promptName = request.params.name as string; const args = (request.params.arguments || {}) as Record<string, unknown>; const startTime = Date.now(); // Check if this is a v1 prompt if (isV1Prompt(promptName)) { const promptDef = getPromptV1ByName(promptName); if (!promptDef) { throw new Error(`Prompt not found: ${promptName}`); } // Validate arguments const validation = validateArguments(args, promptDef.arguments); if (!validation.success) { const error = createValidationError(validation.errors); throw new Error(error.message); } // Build messages const messages = promptDef.buildMessages(validation.data); // Check token budget const budgetCheck = await checkTokenBudget(promptName, messages); if (!budgetCheck.withinBudget) { const error = createBudgetExceededError(budgetCheck); throw new Error(error.message); } // Calculate token metadata const tokenMetadata = await calculatePromptTokens(messages); // Log telemetry const telemetryEvent = createTelemetryEvent( promptName, tokenMetadata, messages.length, startTime, false // budget not exceeded (already checked above) ); logPromptTelemetry(telemetryEvent); // Build response const response: Record<string, unknown> = { description: promptDef.metadata.description, messages: messages, }; // Add dev metadata if enabled if (isDevMetaEnabled()) { response._meta = tokenMetadata; } return response; } // Handle legacy Handlebars prompts const prompt = getPromptById(promptName); if (!prompt) { throw new Error(`Prompt not found: ${promptName}`); } return { description: prompt.description, messages: [ { role: 'user', content: { type: 'text', text: prompt.template, }, }, ], }; }); } function getRequestMetadata(req: Request, toolName: string) { const requestId = req.header('x-request-id') || req.header('x-correlation-id') || req.header('x-amzn-trace-id'); const userId = req.header('x-attio-user-id') || req.header('x-user-id'); return { toolName, requestId: requestId ?? 'unknown', userId: userId ?? 'unknown', }; }

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/kesslerio/attio-mcp-server'

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