Skip to main content
Glama

example-mcp-server-sse

by yigitkonur
server.ts50.7 kB
#!/usr/bin/env node /** * @file server.ts * @summary Educational Reference Implementation for a Stateful, Singleton MCP Server. * @description This server demonstrates the "Singleton Server Pattern" for MCP. A single, * shared `McpServer` instance holds all business logic and state, making it highly * memory-efficient for single-node deployments. Each connecting client receives a * lightweight, session-specific `StreamableHTTPServerTransport`. * * @architecture * 1. **Singleton `McpServer`**: One instance created at startup holds all capabilities. * 2. **Per-Session Transports**: A new `StreamableHTTPServerTransport` is created for each client session. * 3. **In-Memory State**: Active transports are stored in a simple `{[sessionId]: transport}` map. * 4. **Decoupled Logic**: Business logic (`createCalculatorServer`) is separate from the web server transport layer. * * @error_handling * This server prioritizes protocol compliance and clear error communication. * - **Invalid user input** within a tool (e.g., division by zero) throws a specific * `McpError` with `ErrorCode.InvalidParams`. This informs the client that the * user's request was flawed, not the server. * - **Unexpected internal failures** are caught and wrapped in a generic * `McpError` with `ErrorCode.InternalError` to prevent leaking implementation details. * - **Invalid session requests** at the transport layer return HTTP 400/404 with a * JSON-RPC error object, clearly distinguishing session issues from tool errors. * * @philosophy * This code prioritizes clarity, correctness, and adherence to the MCP specification. * It serves as a robust foundation for building real-world, production-ready services. * We trust the SDK to handle the low-level wire protocol and focus on clean application logic. */ // --- Core Node.js and Express Dependencies --- import express, { type Request, type Response } from 'express'; import cors from 'cors'; import { randomUUID } from 'node:crypto'; import type http from 'http'; // Explicitly import for type safety on httpServer // --- Model Context Protocol (MCP) SDK Dependencies --- // The core server class that holds all capabilities. import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; // The deprecated SSE transport for backward compatibility with MCP Inspector. import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; // Error types for proper error handling. import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; // --- Application-Specific Dependencies --- // Zod is used for robust, type-safe schema validation. import { z } from 'zod'; // Local type definitions and constants. import { type CalculationHistoryEntry, MATH_CONSTANTS } from './types.js'; // --- Global Server Configuration --- // These settings are configured via environment variables or command-line arguments. const args = process.argv.slice(2); const portIndex = args.indexOf('--port'); const portArg = portIndex !== -1 ? args[portIndex + 1] : undefined; const PORT = (portArg ? parseInt(portArg, 10) : undefined) || Number(process.env['PORT']) || 1923; const HOST = process.env['HOST'] || 'localhost'; // WARNING: In a real production environment, CORS_ORIGIN should be a specific domain. const CORS_ORIGIN = process.env['CORS_ORIGIN'] || '*'; // --- Global State Management --- // A simple in-memory map to store active client transports, keyed by session ID. // This is the core of our session management for this singleton architecture. // Key: sessionId from SSE transport (string) // Value: The transport instance for that session. const transports: { [sessionId: string]: SSEServerTransport } = {}; // =================================================================================== // === BUSINESS LOGIC CORE (The Calculator Factory) // =================================================================================== /** * Creates and configures a new `McpServer` instance with all calculator-related * capabilities. This function is the single source of truth for what our server can do. * It is completely decoupled from the transport layer (Express, HTTP). * * @returns A fully configured McpServer instance. */ export function createCalculatorServer(): McpServer { // Tool and resource name constants to prevent typos and enable refactoring const TOOL_NAMES = { CALCULATE: 'calculate', DEMO_PROGRESS: 'demo_progress', SOLVE_MATH_PROBLEM: 'solve_math_problem', EXPLAIN_FORMULA: 'explain_formula', CALCULATOR_ASSISTANT: 'calculator_assistant', BATCH_CALCULATE: 'batch_calculate', ADVANCED_CALCULATE: 'advanced_calculate', } as const; const RESOURCE_NAMES = { CONSTANTS: 'calculator://constants', STATS: 'calculator://stats', HISTORY: 'calculator://history/{id}', } as const; const PROMPT_NAMES = { EXPLAIN_CALCULATION: 'explain-calculation', GENERATE_PROBLEMS: 'generate-problems', CALCULATOR_TUTORIAL: 'calculator-tutorial', } as const; const server = new McpServer( { name: 'calculator-sse-server', version: '1.0.0', }, { capabilities: { tools: {}, resources: {}, prompts: {}, logging: {}, }, }, ); // In-memory state for this server instance. In a singleton pattern, this // array is shared across ALL connected clients. const calculationHistory: CalculationHistoryEntry[] = []; // TOOL: Educational Echo (Only if SAMPLE_TOOL_NAME env var is set) if (process.env['SAMPLE_TOOL_NAME']) { const sampleToolName = process.env['SAMPLE_TOOL_NAME']; server.tool( sampleToolName, `Educational echo tool for learning MCP concepts`, { value: z.string().describe('String to echo back'), }, async ({ value }) => ({ content: [ { type: 'text', text: `test string print: ${value}`, }, ], }), ); } // --- TOOL: calculate --- // The primary tool for performing arithmetic. Demonstrates: // 1. Direct Zod schema usage for input validation. // 2. Simple state mutation (pushing to shared history). // 3. Protocol-compliant error handling (e.g., for division by zero). server.tool( TOOL_NAMES.CALCULATE, 'Performs arithmetic operations with calculation history tracking', { op: z .enum(['add', 'subtract', 'multiply', 'divide', 'power', 'sqrt']) .describe('Arithmetic operation to perform'), a: z.number().describe('First operand for the calculation'), b: z.number().optional().describe('Second operand (not required for sqrt operation)'), precision: z .number() .optional() .default(2) .describe('Number of decimal places for result (default: 2)'), }, async ({ op, a, b, precision = 2 }) => { const operation = op; const input_1 = a; const input_2 = b; let result: number; let expression: string; // Perform calculation based on operation switch (operation) { case 'add': result = input_1 + (input_2 ?? 0); break; case 'subtract': result = input_1 - (input_2 ?? 0); break; case 'multiply': result = input_1 * (input_2 ?? 1); break; case 'divide': if (input_2 === 0) { // CAVEAT: Throwing a specific McpError is critical. A generic `Error` // would result in a vague "Internal Server Error" for the client. // By using `ErrorCode.InvalidParams`, we clearly signal that the // user's input was the source of the failure, not a server bug. throw new McpError(ErrorCode.InvalidParams, 'Division by zero is not allowed'); } result = input_1 / (input_2 ?? 1); break; case 'power': result = Math.pow(input_1, input_2 ?? 2); break; case 'sqrt': if (input_1 < 0) { // ERROR HANDLING PATTERN: InvalidParams for user input validation // This error tells the client "your input is mathematically invalid" // rather than "the server failed". This distinction is crucial for // proper client-side error handling and user experience. throw new McpError( ErrorCode.InvalidParams, 'Cannot calculate square root of negative number', ); } result = Math.sqrt(input_1); break; default: // DEFENSIVE PROGRAMMING: This should never happen due to Zod validation, // but we handle it gracefully anyway. InvalidParams is appropriate here // because an unknown operation is fundamentally a client input problem. throw new McpError(ErrorCode.InvalidParams, `Unknown operation: ${operation}`); } // Check for invalid results if (!isFinite(result)) { // INTERNAL ERROR PATTERN: This represents a computational failure // that shouldn't happen with valid inputs. We use InternalError // because this indicates a problem with our calculation logic, // not the user's input (which was already validated above). throw new McpError(ErrorCode.InternalError, 'Result is not a finite number'); } // Format result to specified precision const formattedResult = Number(result.toFixed(precision)); // Build expression with formatted result switch (operation) { case 'add': expression = `${input_1} + ${input_2} = ${formattedResult}`; break; case 'subtract': expression = `${input_1} - ${input_2} = ${formattedResult}`; break; case 'multiply': expression = `${input_1} × ${input_2} = ${formattedResult}`; break; case 'divide': expression = `${input_1} ÷ ${input_2} = ${formattedResult}`; break; case 'power': expression = `${input_1}^${input_2 ?? 2} = ${formattedResult}`; break; case 'sqrt': expression = `√${input_1} = ${formattedResult}`; break; } // Store in history with formatted result const inputs = input_2 !== undefined ? [input_1, input_2] : [input_1]; const historyEntry: CalculationHistoryEntry = { id: randomUUID(), timestamp: new Date().toISOString(), operation, inputs, result: formattedResult, expression, }; calculationHistory.push(historyEntry); return { content: [ { type: 'text', text: expression, }, ], }; }, ); // RESOURCE: Mathematical Constants (Static) server.registerResource( 'calculator-constants', RESOURCE_NAMES.CONSTANTS, { title: 'Mathematical Constants', description: 'Common mathematical constants for calculations', mimeType: 'application/json', }, async () => ({ contents: [ { uri: RESOURCE_NAMES.CONSTANTS, mimeType: 'application/json', text: JSON.stringify(MATH_CONSTANTS, null, 2), }, ], }), ); // --- RESOURCE: calculation-history --- // Demonstrates a dynamic resource using a ResourceTemplate. It can serve: // 1. A list of available sub-resources (e.g., 'last 10'). // 2. Content for a specific URI based on a parameter (ID or limit queries). server.registerResource( 'calculation-history', new ResourceTemplate(RESOURCE_NAMES.HISTORY, { list: async () => ({ resources: [ // Predefined limit-based resources { uri: 'calculator://history/10', name: 'Recent Calculations (Last 10)', description: 'View the last 10 calculations', mimeType: 'application/json', }, { uri: 'calculator://history/50', name: 'Extended History (Last 50)', description: 'View the last 50 calculations', mimeType: 'application/json', }, { uri: 'calculator://history/all', name: 'Complete History', description: 'View all calculations from this session', mimeType: 'application/json', }, // Dynamic ID-based resources for recent calculations ...calculationHistory.slice(-5).map((entry) => ({ uri: `calculator://history/${entry.id}`, name: `Calculation: ${entry.expression}`, description: `Performed at ${entry.timestamp}`, mimeType: 'application/json', })), ], }), }), { title: 'Calculation History', description: 'Access calculation history by limit or specific ID', mimeType: 'application/json', }, async (uri, params) => { const param = params?.['param'] as string; // Check if param is a UUID (calculation ID) const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(param); if (isUuid) { // Handle ID-based query const calculation = calculationHistory.find((entry) => entry.id === param); if (!calculation) { throw new McpError(ErrorCode.InvalidParams, `Calculation not found: ${param}`); } return { contents: [ { uri: uri.href, mimeType: 'application/json', text: JSON.stringify(calculation, null, 2), }, ], }; } else { // Handle limit-based query let limit: number | undefined; if (param === 'all') { limit = undefined; } else { limit = parseInt(param) || 10; } const recent = limit ? calculationHistory.slice(-limit) : calculationHistory; return { contents: [ { uri: uri.href, mimeType: 'application/json', text: JSON.stringify( { count: recent.length, totalCount: calculationHistory.length, calculations: recent, }, null, 2, ), }, ], }; } }, ); // RESOURCE: Calculator Statistics server.registerResource( 'calculator-stats', RESOURCE_NAMES.STATS, { title: 'Calculator Statistics', description: 'Usage statistics for the current session', mimeType: 'application/json', }, async () => { const operationCounts = calculationHistory.reduce( (acc, entry) => { acc[entry.operation] = (acc[entry.operation] || 0) + 1; return acc; }, {} as Record<string, number>, ); // Calculate average calculations per minute let averageCalculationsPerMinute = 0; if (calculationHistory.length > 0) { const firstEntry = calculationHistory[0]; const lastEntry = calculationHistory[calculationHistory.length - 1]; if (firstEntry && lastEntry) { const firstTimestamp = new Date(firstEntry.timestamp); const lastTimestamp = new Date(lastEntry.timestamp); const durationMinutes = (lastTimestamp.getTime() - firstTimestamp.getTime()) / (1000 * 60); averageCalculationsPerMinute = durationMinutes > 0 ? calculationHistory.length / durationMinutes : calculationHistory.length; } } const stats = { totalCalculations: calculationHistory.length, operationCounts, sessionStarted: calculationHistory[0]?.timestamp || new Date().toISOString(), lastCalculation: calculationHistory[calculationHistory.length - 1]?.timestamp || null, averageCalculationsPerMinute, }; return { contents: [ { uri: RESOURCE_NAMES.STATS, mimeType: 'application/json', text: JSON.stringify(stats, null, 2), }, ], }; }, ); // PROMPT: Explain Calculation server.prompt( PROMPT_NAMES.EXPLAIN_CALCULATION, 'Get a detailed explanation of a mathematical operation', { expression: z.string().describe('Mathematical expression to explain'), level: z.string().optional().describe('Level: elementary, intermediate, or advanced'), includeSteps: z.string().optional().describe('Include steps: true or false'), }, ({ expression, level = 'intermediate', includeSteps = 'true' }) => { const includeStepsBool = includeSteps === 'true'; return { messages: [ { role: 'user', content: { type: 'text', text: `Please explain this mathematical expression: "${expression}" Target audience: ${level} level ${includeStepsBool ? 'Include step-by-step solution.' : 'Provide a concise explanation.'} Focus on: - What the expression means - How to solve it${includeStepsBool ? ' step by step' : ''} - Common mistakes to avoid - Real-world applications where this type of calculation is used`, }, }, ], }; }, ); // PROMPT: Generate Practice Problems server.prompt( PROMPT_NAMES.GENERATE_PROBLEMS, 'Create practice problems based on recent calculations', { topic: z.string().describe('Type of problems: arithmetic, algebra, geometry, or mixed'), difficulty: z.string().optional().describe('Difficulty: easy, medium, or hard'), count: z.string().optional().describe('Number of problems (1-10)'), }, ({ topic, difficulty = 'medium', count = '5' }) => { const countNum = parseInt(count) || 5; // Analyze recent calculations to inform problem generation const recentOperations = calculationHistory .slice(-10) .map((h) => h.operation) .filter((op, index, self) => self.indexOf(op) === index); return { messages: [ { role: 'user', content: { type: 'text', text: `Generate ${countNum} ${difficulty} ${topic} practice problems. ${recentOperations.length > 0 ? `Recent operations used: ${recentOperations.join(', ')}` : ''} Requirements: - Include clear problem statements - Provide answer keys at the end - Make problems progressively challenging - Include word problems where appropriate - Format for easy reading - If possible, relate to the types of calculations recently performed`, }, }, ], }; }, ); // PROMPT: Calculator Tutorial server.prompt( PROMPT_NAMES.CALCULATOR_TUTORIAL, 'Generate a tutorial for using this calculator effectively', { focusArea: z .enum(['basic', 'advanced', 'tips']) .optional() .describe('Area to focus the tutorial on'), }, ({ focusArea = 'basic' }) => ({ messages: [ { role: 'user', content: { type: 'text', text: `Create a tutorial for using this calculator MCP server. Focus area: ${focusArea} Available operations: add, subtract, multiply, divide, power, sqrt Please cover: ${ focusArea === 'basic' ? '- How to perform basic calculations\n- Understanding the operation types\n- Reading calculation results' : focusArea === 'advanced' ? '- Using power and square root operations\n- Precision control\n- Accessing calculation history and statistics' : '- Best practices for accuracy\n- Common calculation patterns\n- How to use the explain-calculation feature' } Make it practical with examples.`, }, }, ], }), ); // --- TOOL: demo_progress --- // Demonstrates the correct way to handle progress notifications. // KEY INSIGHT: The `progressToken` MUST come from the client via `_meta.progressToken`. // The server should not generate its own token. server.tool( TOOL_NAMES.DEMO_PROGRESS, 'Demonstrates progress notifications via SSE stream', { task: z.string().optional().describe('Task name for the progress demo'), }, async ({ task = 'Processing' }, { sendNotification, _meta }) => { const progressToken = _meta?.progressToken; if (!progressToken) { // Client didn't request progress updates return { content: [ { type: 'text', text: 'Progress notifications not requested by client. Use _meta.progressToken to enable progress updates.', }, ], }; } // Send progress notifications asynchronously void (async () => { try { await sendNotification({ method: 'notifications/message', params: { level: 'info', data: `Starting progress demo: ${task} (progressToken: ${progressToken})`, }, }); // Send progress updates for (let i = 20; i <= 100; i += 20) { await new Promise((resolve) => setTimeout(resolve, 500)); await sendNotification({ method: 'notifications/progress', params: { progressToken, progress: i, total: 100, message: `${task} - ${i}% complete`, }, }); } // Send completion notification await sendNotification({ method: 'notifications/progress', params: { progressToken, progress: 100, total: 100, message: `${task} - Completed`, }, }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error('Failed to send progress notifications:', errorMessage, { originalError: error, }); // Send error notification try { await sendNotification({ method: 'notifications/message', params: { level: 'error', data: `Progress demo failed: ${errorMessage}`, }, }); } catch (notifyError: unknown) { const notifyErrorMessage = notifyError instanceof Error ? notifyError.message : String(notifyError); console.error('Failed to send error notification:', notifyErrorMessage); } } })(); return { content: [ { type: 'text', text: `Task started. Progress updates will be sent to token: ${progressToken}`, }, ], }; }, ); // TOOL: Solve Math Problem (Extended) server.tool( TOOL_NAMES.SOLVE_MATH_PROBLEM, 'Solve complex math problems step by step', { problem: z.string().describe('Mathematical problem to solve'), showSteps: z.boolean().optional().default(true).describe('Show step-by-step solution'), }, async ({ problem, showSteps = true }) => { // Simple problem solver for demonstration // In production, integrate with a proper math solving engine const steps = []; let solution = ''; try { // Parse common word problems if (problem.toLowerCase().includes('solve for')) { // Simple linear equation solver const match = problem.match(/solve for (\w+).*?([\w\s+\-*/=0-9]+)/i); if (match && match[1] && match[2]) { const variable = match[1]; const equation = match[2]; steps.push(`Given equation: ${equation}`); steps.push(`Solving for variable: ${variable}`); // Very basic solver for ax + b = c format const simpleMatch = equation.match(/(\d*)\s*\*?\s*(\w+)\s*([+-])\s*(\d+)\s*=\s*(\d+)/); if (simpleMatch && simpleMatch[3] && simpleMatch[4] && simpleMatch[5]) { const coeff = simpleMatch[1] || '1'; const op = simpleMatch[3]; const constant = parseFloat(simpleMatch[4]); const result = parseFloat(simpleMatch[5]); if (op === '+') { const value = (result - constant) / parseFloat(coeff); solution = `${variable} = ${value}`; steps.push(`Subtract ${constant} from both sides`); steps.push(`${result} - ${constant} = ${result - constant}`); steps.push(`Divide by ${coeff}`); steps.push(solution); } else { const value = (result + constant) / parseFloat(coeff); solution = `${variable} = ${value}`; steps.push(`Add ${constant} to both sides`); steps.push(`${result} + ${constant} = ${result + constant}`); steps.push(`Divide by ${coeff}`); steps.push(solution); } } } } else if (problem.toLowerCase().includes('what is')) { // Simple arithmetic evaluation const match = problem.match(/what is ([\d\s+\-*/().]+)/i); if (match) { const expression = match[1]; steps.push(`Evaluating: ${expression}`); const result = Function('"use strict"; return (' + expression + ')')(); solution = `${expression} = ${result}`; steps.push(solution); } } if (!solution) { // Fallback for unrecognized problems steps.push('This problem requires advanced mathematical analysis.'); steps.push('Breaking down the problem...'); solution = 'Please provide a more specific mathematical expression or equation.'; } return { content: [ { type: 'text', text: JSON.stringify( { problem, solution, steps: showSteps ? steps : undefined, timestamp: new Date().toISOString(), }, null, 2, ), }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: JSON.stringify( { error: `Failed to solve problem: ${errorMessage}`, problem, }, null, 2, ), }, ], }; } }, ); // TOOL: Explain Formula (Extended) server.tool( TOOL_NAMES.EXPLAIN_FORMULA, 'Explain mathematical formulas and their applications', { formula: z.string().describe('Mathematical formula to explain'), context: z.string().optional().describe('Context or field of application'), }, async ({ formula, context }) => { // Formula explanation database interface FormulaExplanation { name: string; description: string; variables?: Record<string, string>; applications?: string[]; example?: string; analysis?: string; elements?: string[]; operators?: string[]; suggestion?: string; } const formulaExplanations: Record<string, FormulaExplanation> = { 'a^2 + b^2 = c^2': { name: 'Pythagorean Theorem', description: 'Relates the lengths of sides in a right triangle', variables: { a: 'Length of one leg', b: 'Length of other leg', c: 'Length of hypotenuse', }, applications: ['Construction', 'Navigation', 'Computer graphics'], example: 'If a=3 and b=4, then c=5 (since 9+16=25)', }, 'E = mc^2': { name: 'Mass-Energy Equivalence', description: "Einstein's equation showing the relationship between mass and energy", variables: { E: 'Energy (joules)', m: 'Mass (kg)', c: 'Speed of light (m/s)', }, applications: ['Nuclear physics', 'Particle physics', 'Cosmology'], example: '1 kg of matter contains 9×10^16 joules of energy', }, 'F = ma': { name: "Newton's Second Law", description: 'The fundamental equation of classical mechanics', variables: { F: 'Force (Newtons)', m: 'Mass (kg)', a: 'Acceleration (m/s²)', }, applications: ['Engineering', 'Physics', 'Rocket science'], example: 'A 10kg object accelerating at 2m/s² experiences 20N of force', }, 'A = πr²': { name: 'Area of a Circle', description: 'Calculates the area enclosed by a circle', variables: { A: 'Area', π: 'Pi (approximately 3.14159)', r: 'Radius of the circle', }, applications: ['Geometry', 'Engineering', 'Architecture'], example: 'A circle with radius 5 has area 25π ≈ 78.54 square units', }, }; // Try to match the formula const normalizedFormula = formula.replace(/\s+/g, '').toLowerCase(); let explanation = null; for (const [key, value] of Object.entries(formulaExplanations)) { if (key.replace(/\s+/g, '').toLowerCase() === normalizedFormula) { explanation = value; break; } } if (!explanation) { // Provide generic explanation for unrecognized formulas explanation = { name: 'Custom Formula', description: 'This appears to be a mathematical relationship between variables', analysis: `The formula "${formula}" contains the following elements:`, elements: formula.match(/[a-zA-Z]+/g) || [], operators: formula.match(/[+\-*/^=]/g) || [], suggestion: 'Consider breaking down each variable and operation to understand the relationship', }; } return { content: [ { type: 'text', text: JSON.stringify( { formula, context, explanation, timestamp: new Date().toISOString(), }, null, 2, ), }, ], }; }, ); // TOOL: Calculator Assistant (Extended) server.tool( TOOL_NAMES.CALCULATOR_ASSISTANT, 'Interactive calculator assistant for mathematical queries', { query: z.string().describe('Natural language query about calculations or math'), includeHistory: z .boolean() .optional() .default(false) .describe('Include calculation history in context'), }, async ({ query, includeHistory = false }) => { interface AssistantResponse { query: string; timestamp: string; type?: string; message?: string; capabilities?: string[]; examples?: string[]; history?: CalculationHistoryEntry[]; totalCalculations?: number; operation?: { a: number; b: number; op: string }; result?: number; expression?: string; recentHistory?: CalculationHistoryEntry[]; } const response: AssistantResponse = { query, timestamp: new Date().toISOString(), }; // Parse the query for intent const lowerQuery = query.toLowerCase(); if (lowerQuery.includes('help') || lowerQuery.includes('what can')) { response.type = 'help'; response.message = 'I can help you with:'; response.capabilities = [ 'Basic arithmetic (add, subtract, multiply, divide)', 'Advanced operations (power, square root)', 'Solving simple equations', 'Explaining mathematical formulas', 'Batch calculations', 'Calculation history and statistics', ]; response.examples = [ 'Calculate 25 * 4', 'What is the square root of 144?', 'Solve for x: 2x + 5 = 15', 'Explain the Pythagorean theorem', 'Show my calculation history', ]; } else if (lowerQuery.includes('history')) { response.type = 'history'; response.history = calculationHistory.slice(-5); response.totalCalculations = calculationHistory.length; response.message = `You have performed ${calculationHistory.length} calculations in this session.`; } else if (lowerQuery.includes('calculate') || lowerQuery.includes('what is')) { // Extract numbers and operations from the query const numbers = query.match(/-?\d+(\.\d+)?/g); const operations = query.match( /\b(add|subtract|multiply|divide|plus|minus|times|divided by)\b/gi, ); if (numbers && numbers.length >= 2 && operations && operations.length > 0) { const a = parseFloat(numbers[0]!); const b = parseFloat(numbers[1]!); let op = operations[0].toLowerCase(); // Map word operations to symbols const opMap: Record<string, string> = { add: 'add', plus: 'add', subtract: 'subtract', minus: 'subtract', multiply: 'multiply', times: 'multiply', divide: 'divide', 'divided by': 'divide', }; op = opMap[op] || op; // Perform calculation let result: number; switch (op) { case 'add': result = a + b; break; case 'subtract': result = a - b; break; case 'multiply': result = a * b; break; case 'divide': result = a / b; break; default: result = 0; } response.type = 'calculation'; response.operation = { a, b, op }; response.result = Math.round(result * 10000) / 10000; response.expression = `${a} ${op} ${b} = ${response.result}`; response.message = `The result is ${response.result}`; } else { response.type = 'clarification'; response.message = 'I need more information to perform this calculation. Please provide two numbers and an operation.'; } } else { response.type = 'general'; response.message = 'I\'m ready to help with mathematical calculations. Try asking me to calculate something or type "help" for more options.'; } if (includeHistory && calculationHistory.length > 0) { response.recentHistory = calculationHistory.slice(-3); } return { content: [ { type: 'text', text: JSON.stringify(response, null, 2), }, ], }; }, ); // TOOL: Batch Calculate server.tool( TOOL_NAMES.BATCH_CALCULATE, 'Perform multiple calculations in a single request', { operations: z .array( z.object({ op: z.enum(['add', 'subtract', 'multiply', 'divide', 'power', 'sqrt']), a: z.number(), b: z.number().optional(), precision: z.number().optional().default(4), }), ) .describe('Array of calculation operations'), }, async ({ operations }) => { const results = []; for (const operation of operations) { try { let result: number; const { op, a, b = 0, precision = 4 } = operation; switch (op) { case 'add': result = a + b; break; case 'subtract': result = a - b; break; case 'multiply': result = a * b; break; case 'divide': if (b === 0) { results.push({ error: 'Division by zero', operation, }); continue; } result = a / b; break; case 'power': result = Math.pow(a, b); break; case 'sqrt': if (a < 0) { results.push({ error: 'Cannot calculate square root of negative number', operation, }); continue; } result = Math.sqrt(a); break; } // Round to specified precision result = Math.round(result * Math.pow(10, precision)) / Math.pow(10, precision); const expression = op === 'sqrt' ? `sqrt(${a})` : `${a} ${op} ${b}`; results.push({ expression, result, operation, }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); results.push({ error: `Failed to calculate: ${errorMessage}`, operation, }); } } return { content: [ { type: 'text', text: JSON.stringify( { totalOperations: operations.length, successful: results.filter((r) => !r.error).length, failed: results.filter((r) => r.error).length, results, }, null, 2, ), }, ], }; }, ); // TOOL: Advanced Calculate (SECURITY WARNING - DEMONSTRATION ONLY) server.tool( TOOL_NAMES.ADVANCED_CALCULATE, 'SECURITY WARNING: This tool demonstrates unsafe expression evaluation and should NOT be used in production', { expression: z.string().describe('Mathematical expression to evaluate'), variables: z .record(z.number()) .optional() .describe('Variables to substitute in the expression'), }, async ({ expression, variables = {} }) => { // !!! CRITICAL SECURITY WARNING !!! // The use of `new Function()` is functionally equivalent to `eval()` and is // EXTREMELY DANGEROUS when used with untrusted user input, as it allows for // Remote Code Execution (RCE) on the server. // // This tool is included for DEMONSTRATION PURPOSES ONLY to show complex // input parsing. // // In a REAL PRODUCTION ENVIRONMENT, you MUST replace this with a safe, // sandboxed math expression parser library like `mathjs` or a similar // vetted tool. NEVER use `eval` or `new Function` on user input. // !!! END CRITICAL SECURITY WARNING !!! console.warn( '⚠️ SECURITY WARNING: advanced_calculate tool is executing potentially unsafe code evaluation', ); try { // Simple expression evaluator (UNSAFE - DO NOT USE IN PRODUCTION) let evalExpression = expression; // Replace variables for (const [varName, value] of Object.entries(variables)) { evalExpression = evalExpression.replace( new RegExp(`\\b${varName}\\b`, 'g'), value.toString(), ); } // Basic validation - only allow numbers, operators, and math functions if (!/^[0-9+\-*/().\s\w]+$/.test(evalExpression)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid expression: contains unsupported characters', ); } // Replace math functions with Math.* equivalents evalExpression = evalExpression .replace(/\bsqrt\(/g, 'Math.sqrt(') .replace(/\bpow\(/g, 'Math.pow(') .replace(/\bsin\(/g, 'Math.sin(') .replace(/\bcos\(/g, 'Math.cos(') .replace(/\btan\(/g, 'Math.tan(') .replace(/\babs\(/g, 'Math.abs(') .replace(/\bfloor\(/g, 'Math.floor(') .replace(/\bceil\(/g, 'Math.ceil(') .replace(/\bround\(/g, 'Math.round(') .replace(/\bPI\b/g, 'Math.PI') .replace(/\bE\b/g, 'Math.E'); // UNSAFE EVALUATION - DO NOT USE IN PRODUCTION // This is a demonstration of what NOT to do for security reasons const result = Function('"use strict"; return (' + evalExpression + ')')(); if (typeof result !== 'number' || !isFinite(result)) { throw new McpError( ErrorCode.InvalidParams, 'Expression did not evaluate to a finite number', ); } return { content: [ { type: 'text', text: JSON.stringify( { securityWarning: 'This tool uses unsafe code evaluation and should not be used in production', expression, variables, evaluatedExpression: evalExpression, result: Math.round(result * 10000) / 10000, // Round to 4 decimal places recommendation: 'Use a safe math expression parser like mathjs in production environments', }, null, 2, ), }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: JSON.stringify( { error: `Failed to evaluate expression: ${errorMessage}`, expression, variables, securityNote: 'This failure demonstrates why safe expression parsers should be used', }, null, 2, ), }, ], }; } }, ); return server; } // =================================================================================== // === SINGLETON PATTERN INSTANTIATION // =================================================================================== // We create ONE single instance of our McpServer. This instance is shared across // all incoming connections, making it highly memory efficient. All shared state // (like `calculationHistory`) lives inside this single object. const sharedMcpServer: McpServer = createCalculatorServer(); console.warn('[Server] Shared Calculator MCP Server instance created.'); // =================================================================================== // === WEB SERVER SETUP (Express.js) // =================================================================================== const app = express(); // --- Middleware Configuration --- // 1. CORS (Cross-Origin Resource Sharing) // This is critical for browser-based clients to be able to connect to the server. app.use( cors({ origin: CORS_ORIGIN, credentials: true, // IMPORTANT: This header MUST be exposed. It allows the browser client's // JavaScript code to read the session ID from the response headers. exposedHeaders: ['Mcp-Session-Id'], }), ); // 2. JSON Body Parser // This middleware automatically parses incoming JSON request bodies. app.use(express.json()); // 3. Request Logger // A simple middleware to log every incoming request for debugging purposes. app.use((_req: Request, _res: Response, next: express.NextFunction) => { // Simple request logging middleware next(); }); // =================================================================================== // === DEPRECATED SSE ENDPOINTS (FOR MCP INSPECTOR COMPATIBILITY) // =================================================================================== // The SSE transport requires two separate endpoints: // 1. GET /sse - Establishes the SSE stream // 2. POST /messages - Receives client messages with sessionId in query param // SSE endpoint for establishing the stream app.get('/sse', (_req: Request, res: Response) => { void (async () => { console.warn('[MCP] GET /sse - Establishing SSE stream...'); try { // Create a new SSE transport for this client // The '/messages' endpoint will handle incoming POST requests const transport = new SSEServerTransport('/messages', res); // Store the transport by its auto-generated session ID const sessionId = transport.sessionId; transports[sessionId] = transport; console.warn(`[MCP] SSE stream established with session ID: ${sessionId}`); // Set up cleanup when the connection closes transport.onclose = () => { console.warn(`[MCP] SSE transport closed for session ${sessionId}`); delete transports[sessionId]; }; // Connect the transport to our shared MCP server await sharedMcpServer.connect(transport); } catch (error) { console.error('[MCP] Error establishing SSE stream:', error); if (!res.headersSent) { res.status(500).send('Error establishing SSE stream'); } } })(); }); // Messages endpoint for receiving client JSON-RPC requests app.post('/messages', (req: Request, res: Response) => { void (async () => { console.warn('[MCP] POST /messages - Handling client message...'); // Extract session ID from URL query parameter (not header!) const sessionId = req.query.sessionId as string | undefined; if (!sessionId) { console.error('[MCP] No session ID provided in query parameter'); res.status(400).send('Missing sessionId query parameter'); return; } const transport = transports[sessionId]; if (!transport) { console.error(`[MCP] No active transport found for session ID: ${sessionId}`); res.status(404).send('Session not found'); return; } try { // Delegate to the SSE transport to handle the message await transport.handlePostMessage(req, res, req.body); } catch (error) { console.error('[MCP] Error handling POST message:', error); if (!res.headersSent) { res.status(500).send('Error handling request'); } } })(); }); // Health check endpoint (remains the same) app.get('/health', (_req: Request, res: Response) => { res.json({ status: 'healthy', activeSessions: Object.keys(transports).length, transport: 'streamableHttp', // Updated transport name uptime: process.uptime(), memory: process.memoryUsage(), }); }); // Root endpoint app.get('/', (_req: Request, res: Response) => { res.json({ name: 'Calculator SSE MCP Server (Deprecated)', version: '1.0.0', transport: 'sse', // Deprecated SSE transport endpoints: { sse: '/sse', // GET endpoint for SSE stream messages: '/messages', // POST endpoint for messages health: '/health', }, instructions: 'GET /sse to establish SSE stream, then POST to /messages?sessionId=<id>', }); }); // =================================================================================== // === SERVER STARTUP // =================================================================================== const httpServer: http.Server = app.listen(PORT, () => { console.warn(` ╔═══════════════════════════════════════════════════════════╗ ║ Calculator SSE MCP Server Started (Deprecated) ║ ╠═══════════════════════════════════════════════════════════╣ ║ Transport: SSE (Protocol version 2024-11-05) ║ ║ Port: ${PORT} ║ ║ SSE Endpoint: GET http://${HOST}:${PORT}/sse ║ ║ Messages: POST http://${HOST}:${PORT}/messages?sessionId=<id> ║ ║ Health: http://${HOST}:${PORT}/health ║ ║ ║ ║ This server uses the deprecated SSE transport for ║ ║ compatibility with MCP Inspector and older clients. ║ ╚═══════════════════════════════════════════════════════════╝ To test with MCP Inspector: npx @modelcontextprotocol/inspector sse http://localhost:${PORT}/sse `); }); // =================================================================================== // === GRACEFUL SHUTDOWN // =================================================================================== /** * Handles graceful shutdown of the server. This is essential for production to * ensure no requests are dropped and all connections are closed cleanly. */ const shutdown = () => { console.warn('\n[Server] Shutting down gracefully...'); // 1. Proactively close all active client transports. This sends a signal // to connected clients that the server is going down. console.warn(`[Server] Closing ${Object.keys(transports).length} active sessions...`); for (const sessionId in transports) { try { const transport = transports[sessionId]; if (transport) { const closeResult = transport.close(); if (closeResult instanceof Promise) { void closeResult.catch((error: unknown) => { console.error(`Failed to close session ${sessionId}:`, error); }); } } } catch (error: unknown) { console.error(`Failed to close session ${sessionId}:`, error); } } // 2. Stop the HTTP server from accepting any new incoming connections. httpServer.close(() => { console.warn('[Server] HTTP server closed. Exiting.'); process.exit(0); // Success }); // 3. Set a force-exit timer. If the server hangs during shutdown for any // reason, this ensures the process will still exit after a timeout. setTimeout(() => { console.error('[Server] Graceful shutdown timed out. Forcing exit.'); process.exit(1); // Error }, 5000); // 5-second timeout }; // Listen for termination signals from the operating system or container orchestrator. process.on('SIGINT', shutdown); // Sent by Ctrl+C process.on('SIGTERM', shutdown); // Sent by `kill` or Docker/Kubernetes

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/yigitkonur/example-mcp-server-sse'

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