Skip to main content
Glama
server.ts46.5 kB
#!/usr/bin/env node // ================================================================================= // Module: src/server.ts // // Purpose: // The single, authoritative source file for the MCP STDIO Calculator Server. // // Key Error Handling Ideas: // - Fail-Fast Validation: Business logic functions (e.g., `performBasicCalculation`) // aggressively validate inputs and throw specific `McpError` types on failure. // - Protocol Compliance: All thrown errors are instances of `McpError`, ensuring that // the client receives a standard JSON-RPC error response. This prevents leaking // internal stack traces. // - Graceful Shutdown: The `main` function implements robust signal and exception // handlers to ensure the process always exits cleanly with a specific exit code, // even on catastrophic failure. This is critical for a process-based tool. // - Clear Failure Paths: Every public-facing function (tools, resources) documents // its specific failure conditions using `@throws` TSDoc tags. // ================================================================================= // === SECTION: IMPORTS === import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { completable } from '@modelcontextprotocol/sdk/server/completable.js'; import { z } from 'zod'; import { log, SERVER_INFO, MATH_CONSTANTS, FORMULAS_LIBRARY, EXIT_CODES } from './types.js'; import type { HistoryEntry } from './types.js'; // === SECTION: GLOBAL STATE & CONFIGURATION === /** * Configuration limits for various server operations. * These values control resource usage and prevent abuse. */ const LIMITS = { maxHistorySize: 50, maxBatchSize: 100, HISTORY_DISPLAY_LIMIT: 50, }; /** * In-memory calculation history using a simple array. * WHY: For STDIO transport, this process-based isolation means state * naturally resets when the process exits, providing clean separation. */ const calculationHistory: HistoryEntry[] = []; const MAX_HISTORY = LIMITS.maxHistorySize; /** * Server statistics tracked in-memory. * These provide insights into server usage and uptime. */ let requestCount = 0; const serverStartTime = Date.now(); // === SECTION: CORE BUSINESS LOGIC === /** * Calculates factorial of a non-negative integer. * PURE FUNCTION: No side effects, deterministic output for given input. * * @param n - The number to calculate factorial for * @returns The factorial of n * @throws {RangeError} if n is negative or too large to be represented safely. */ function factorial(n: number): number { if (n < 0) throw new RangeError('Factorial is not defined for negative numbers'); if (n === 0 || n === 1) return 1; if (n > 170) throw new RangeError('Input for factorial is too large to be represented safely.'); let result = 1; for (let i = 2; i <= n; i++) { result *= i; } return result; } /** * Calculates combinations C(n, k) = n! / (k! * (n-k)!) * PURE FUNCTION: Mathematical combination formula implementation. * * @param n - Total number of items * @param k - Number of items to choose * @returns Number of combinations */ function combinations(n: number, k: number): number { if (k > n) return 0; if (k === 0 || k === n) return 1; return factorial(n) / (factorial(k) * factorial(n - k)); } /** * Calculates permutations P(n, k) = n! / (n-k)! * PURE FUNCTION: Mathematical permutation formula implementation. * * @param n - Total number of items * @param k - Number of items to arrange * @returns Number of permutations */ function permutations(n: number, k: number): number { if (k > n) return 0; return factorial(n) / factorial(n - k); } /** * Formats mathematical expressions in human-readable form. * PURE FUNCTION: Takes operation details and returns formatted string. * * @param operation - The mathematical operation performed * @param a - First operand * @param b - Second operand (optional for unary operations) * @param result - The calculated result * @returns Human-readable expression string */ function formatExpression( operation: string, a: number, b: number | undefined, result: number, ): string { switch (operation) { case 'add': return `${a} + ${b} = ${result}`; case 'subtract': return `${a} - ${b} = ${result}`; case 'multiply': return `${a} × ${b} = ${result}`; case 'divide': return `${a} ÷ ${b} = ${result}`; case 'factorial': return `${a}! = ${result}`; case 'log': return b ? `log${b}(${a}) = ${result}` : `ln(${a}) = ${result}`; case 'combinations': return `C(${a}, ${b}) = ${result}`; case 'permutations': return `P(${a}, ${b}) = ${result}`; default: return `${operation}(${a}${b !== undefined ? `, ${b}` : ''}) = ${result}`; } } /** * Creates a new history entry with generated ID and timestamp. * PURE FUNCTION: Generates deterministic output based on inputs (except for ID/timestamp). * * @param operation - The operation that was performed * @param a - First operand * @param b - Second operand (optional) * @param result - The calculation result * @returns A complete HistoryEntry object */ function createHistoryEntry( operation: string, a: number, b: number | undefined, result: number, ): HistoryEntry { const id = Math.random().toString(36).substring(7); const expression = formatExpression(operation, a, b, result); return { id, timestamp: new Date().toISOString(), operation: operation, input_1: a, input_2: b, result, expression, }; } /** * Adds a calculation entry to the in-memory history with automatic cleanup. * SIDE EFFECT: Modifies global calculationHistory array. * * @param entry - The history entry to add */ function addToHistory(entry: HistoryEntry): void { calculationHistory.push(entry); if (calculationHistory.length > MAX_HISTORY) { calculationHistory.shift(); // O(n) but simple and fast for small arrays } } /** * @summary Performs basic arithmetic operations with protocol-compliant error handling. * @remarks This is a pure function that forms the core of the calculator's logic. * It is designed to fail fast with a specific, client-consumable error for * predictable failure conditions like division by zero. * * @param op The operation to perform ('add', 'subtract', 'multiply', 'divide'). * @param a The first operand. * @param b The second operand. * * @throws {McpError} with code `InvalidParams` if the operation is unknown. * @throws {McpError} with code `InvalidParams` on an attempt to divide by zero. * * @returns The calculated result. */ function performBasicCalculation(op: string, a: number, b: number): number { switch (op) { case 'add': return a + b; case 'subtract': return a - b; case 'multiply': return a * b; case 'divide': if (b === 0) throw new McpError(ErrorCode.InvalidParams, 'Division by zero'); return a / b; default: throw new McpError(ErrorCode.InvalidParams, `Unknown operation: ${op}`); } } // === SECTION: MCP WIRING === /** * Registers core calculation tools with the MCP server. * These are the fundamental arithmetic operations that form the foundation * of the calculator functionality. * * @param server - The MCP server instance to register tools with */ function registerCoreTools(server: McpServer): void { // Check for optional SAMPLE_TOOL_NAME environment variable const sampleToolName = process.env.SAMPLE_TOOL_NAME; if (sampleToolName) { // Register the sample tool with the name from environment variable server.registerTool( sampleToolName, { title: sampleToolName, description: 'Test tool for educational purposes', inputSchema: { value: z.string().describe('String value to process'), }, }, async ({ value }) => { log.info(`Executing ${sampleToolName} with value: ${value}`); const result = `test string print: ${value}`; return { content: [ { type: 'text', text: result, }, ], }; }, ); } // --- TOOL: calculate --- // The most fundamental tool. Demonstrates basic SDK usage. // WHY: Co-locating the schema with the tool registration makes the tool's // entire definition (its contract) visible in one place. It is self-contained. const calculateInputSchema = { a: z.number().describe('First operand'), b: z.number().describe('Second operand'), op: z.enum(['add', 'subtract', 'multiply', 'divide']).describe('Operation to perform'), stream: z.boolean().optional().describe('If true, emit progress notifications'), }; const calculateOutputSchema = { value: z.number(), meta: z.object({ calculationId: z.string(), timestamp: z.string(), }), }; server.registerTool( 'calculate', { title: 'Calculate', description: 'Perform a basic arithmetic calculation', inputSchema: calculateInputSchema, outputSchema: calculateOutputSchema, }, // HANDLER LOGIC: // - Async function, receives validated & typed parameters. // - The SDK automatically parses the input against `inputSchema`. // - If validation fails, the SDK sends the error; this code doesn't run. async ({ a, b, op, stream }, { sendNotification }) => { log.info(`Executing calculate: ${a} ${op} ${b}`); requestCount++; try { // FEATURE: Progress notifications for streaming calculations // WHY: Demonstrates real-time feedback using sendNotification if (stream) { const progressId = `calc-${Date.now()}`; await sendNotification({ method: 'notifications/progress', params: { progressToken: progressId, progress: 33, message: `Calculating ${a} ${op} ${b}`, }, }); await new Promise((resolve) => setTimeout(resolve, 100)); await sendNotification({ method: 'notifications/progress', params: { progressToken: progressId, progress: 66 }, }); await new Promise((resolve) => setTimeout(resolve, 100)); await sendNotification({ method: 'notifications/progress', params: { progressToken: progressId, progress: 100, message: 'Complete' }, }); await new Promise((resolve) => setTimeout(resolve, 100)); } // Core business logic: perform the calculation const result = performBasicCalculation(op, a, b); // Create and store history entry const historyEntry = createHistoryEntry(op, a, b, result); addToHistory(historyEntry); // SDK-NOTE: The return object must match the `outputSchema`. // The SDK will validate this before sending the response. // We provide both a simple text `content` for basic UIs and a // `structuredContent` for richer clients. return { content: [ { type: 'text', text: historyEntry.expression, }, ], structuredContent: { value: result, meta: { calculationId: historyEntry.id, timestamp: historyEntry.timestamp, }, }, }; } catch (error) { // CAVEAT: We catch the error from our business logic and re-throw it. // While `performBasicCalculation` already returns an McpError, this pattern // is crucial for wrapping any *unexpected* errors that might occur, // preventing stack traces from leaking to the client. log.error(`Calculation failed: ${error instanceof Error ? error.message : String(error)}`); throw new McpError( ErrorCode.InvalidParams, `Calculation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { operation: op, a, b }, ); } }, ); } /** * Registers extended tools that provide advanced functionality. * These tools demonstrate more complex SDK patterns like batch processing, * advanced mathematics, and interactive features. * * @param server - The MCP server instance to register tools with */ function registerExtendedTools(server: McpServer): void { // --- TOOL: batch_calculate --- // Demonstrates array processing and error handling for individual items const batchCalculateInputSchema = { calculations: z .array( z.object({ a: z.number(), b: z.number(), op: z.enum(['add', 'subtract', 'multiply', 'divide']), }), ) .min(1) .max(LIMITS.maxBatchSize), }; const batchCalculateOutputSchema = { results: z.array( z.discriminatedUnion('success', [ z.object({ success: z.literal(true), expression: z.string(), value: z.number(), calculationId: z.string(), }), z.object({ success: z.literal(false), expression: z.string(), error: z.string(), }), ]), ), }; server.registerTool( 'batch_calculate', { title: 'Batch Calculate', description: 'Perform multiple calculations in a single request', inputSchema: batchCalculateInputSchema, outputSchema: batchCalculateOutputSchema, }, async ({ calculations }) => { log.info(`Executing batch calculations: ${calculations.length} operations`); requestCount++; const results = []; for (const calc of calculations) { try { const result = performBasicCalculation(calc.op, calc.a, calc.b); const historyEntry = createHistoryEntry(calc.op, calc.a, calc.b, result); addToHistory(historyEntry); results.push({ success: true as const, expression: historyEntry.expression, value: result, calculationId: historyEntry.id, }); } catch (error) { // NOTE: This is an example of APPLICATION-LEVEL error handling. // Instead of throwing and failing the entire batch (a protocol-level error), // we catch the error for a single item. The tool itself still succeeds, // but its structured output contains the specific error information. // This gives the client detailed feedback on a per-item basis. results.push({ success: false as const, expression: `${calc.a} ${calc.op} ${calc.b}`, error: error instanceof Error ? error.message : String(error), }); } } return { content: [ { type: 'text', text: results .map((r) => (r.success ? r.expression : `Error in ${r.expression}: ${r.error}`)) .join('\n'), }, ], structuredContent: { results }, }; }, ); // --- TOOL: advanced_calculate --- // Demonstrates complex mathematical operations and conditional parameter handling const advancedCalculateInputSchema = { operation: z.enum(['factorial', 'log', 'combinations', 'permutations']), n: z.number().describe('Primary input'), k: z.number().optional().describe('Secondary input for combinations/permutations'), base: z.number().optional().describe('Base for logarithm (default: e)'), }; const advancedCalculateOutputSchema = { value: z.number(), expression: z.string(), calculationId: z.string(), }; /** * @summary Handler for the `advanced_calculate` tool. * @remarks Implements complex mathematical operations and validates conditional parameters. * This handler demonstrates throwing specific errors for both invalid parameters (`k` missing) * and underlying business logic failures (e.g., `factorial` out of range). * * @throws {McpError} with code `InvalidParams` if the `k` parameter is required for an operation but is not provided. * @throws {RangeError} Propagated from the `factorial` function if inputs are out of the representable range. */ async function handleAdvancedCalculate({ operation, n, k, base, }: { operation: 'factorial' | 'log' | 'combinations' | 'permutations'; n: number; k?: number | undefined; base?: number | undefined; }) { log.info(`Executing advanced calculation: ${operation}`); requestCount++; try { let result: number; let expression: string; switch (operation) { case 'factorial': result = factorial(n); expression = `${n}! = ${result}`; break; case 'log': if (base) { result = Math.log(n) / Math.log(base); expression = `log${base}(${n}) = ${result}`; } else { result = Math.log(n); expression = `ln(${n}) = ${result}`; } break; case 'combinations': if (k === undefined) throw new McpError( ErrorCode.InvalidParams, 'The parameter "k" is required for the "combinations" operation', ); result = combinations(n, k); expression = `C(${n}, ${k}) = ${result}`; break; case 'permutations': if (k === undefined) throw new McpError( ErrorCode.InvalidParams, 'The parameter "k" is required for the "permutations" operation', ); result = permutations(n, k); expression = `P(${n}, ${k}) = ${result}`; break; default: throw new McpError(ErrorCode.InvalidParams, `Unknown operation: ${operation}`); } const historyEntry = createHistoryEntry(operation, n, k, result); addToHistory(historyEntry); return { content: [ { type: 'text' as const, text: expression, }, ], structuredContent: { value: result, expression, calculationId: historyEntry.id, }, }; } catch (error) { log.error( `Advanced calculation failed: ${error instanceof Error ? error.message : String(error)}`, ); throw new McpError( ErrorCode.InvalidParams, `Advanced calculation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { operation, n, k, base }, ); } } server.registerTool( 'advanced_calculate', { title: 'Advanced Calculate', description: 'Perform advanced mathematical operations (factorial, log, combinations, permutations)', inputSchema: advancedCalculateInputSchema, outputSchema: advancedCalculateOutputSchema, }, handleAdvancedCalculate, ); // --- TOOL: demo_progress --- // Demonstrates progress notifications with multiple updates const demoProgressOutputSchema = { message: z.string(), progressSteps: z.array(z.number()), }; server.registerTool( 'demo_progress', { title: 'Demo Progress', description: 'Demonstrate progress notifications with 5 updates', inputSchema: {}, outputSchema: demoProgressOutputSchema, }, async (_, { sendNotification }) => { log.info('Executing demo_progress'); requestCount++; const progressId = 'demo-progress-task'; for (let i = 1; i <= 5; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); await sendNotification({ method: 'notifications/progress', params: { progressToken: progressId, progress: i * 20, message: `Step ${i} of 5 - Processing complex calculations...`, }, }); } return { content: [ { type: 'text', text: 'Progress demonstration completed', }, ], structuredContent: { message: 'Progress demonstration completed', progressSteps: [20, 40, 60, 80, 100], }, }; }, ); // --- TOOL: solve_math_problem --- // Demonstrates elicitInput for interactive problem solving const solveMathProblemInputSchema = { problem: z.string().describe('The math problem to solve'), showSteps: z.boolean().optional().describe('Show step-by-step solution'), }; server.registerTool( 'solve_math_problem', { title: 'Solve Math Problem', description: 'Solve a word problem or mathematical expression (may request user input)', inputSchema: solveMathProblemInputSchema, }, async ({ problem, showSteps }) => { log.info('Solving math problem'); requestCount++; // Check for ambiguous area problems if (problem.toLowerCase().includes('area') && !problem.match(/\d+/)) { // Determine shape type let shape = 'unknown'; if (problem.toLowerCase().includes('rectangle')) shape = 'rectangle'; else if (problem.toLowerCase().includes('circle')) shape = 'circle'; else if (problem.toLowerCase().includes('triangle')) shape = 'triangle'; if (shape === 'rectangle') { try { const result = await server.server.elicitInput({ message: 'I need the dimensions to calculate the area of a rectangle.', requestedSchema: { type: 'object', properties: { length: { type: 'number', description: 'Length of the rectangle' }, width: { type: 'number', description: 'Width of the rectangle' }, }, required: ['length', 'width'], }, }); if (result.action === 'accept') { const { length, width } = result.content as { length: number; width: number }; const area = length * width; const steps = showSteps ? `\n### Steps:\n1. Formula: Area = length × width\n2. Substitution: Area = ${length} × ${width}\n3. Calculation: Area = ${area}\n` : ''; return { content: [ { type: 'text', text: `## Area of Rectangle\n\nThe area is ${length} × ${width} = ${area} square units.${steps}`, }, ], }; } else { return { content: [{ type: 'text', text: 'Area calculation cancelled by user.' }] }; } } catch (e) { // NOTE: `elicitInput` can fail for several reasons: the transport // was closed, the request timed out, or the client explicitly // rejected the elicitation. We wrap this in a generic InternalError // because from the tool's perspective, its interactive flow was // unexpectedly interrupted. throw new McpError( ErrorCode.InternalError, 'Failed to complete interactive input with the user.', ); } } else if (shape === 'circle') { try { const result = await server.server.elicitInput({ message: 'I need the radius to calculate the area of a circle.', requestedSchema: { type: 'object', properties: { radius: { type: 'number', description: 'Radius of the circle' }, }, required: ['radius'], }, }); if (result.action === 'accept') { const { radius } = result.content as { radius: number }; const area = Math.PI * radius * radius; const steps = showSteps ? `\n### Steps:\n1. Formula: Area = π × r²\n2. Substitution: Area = π × ${radius}²\n3. Calculation: Area = ${area.toFixed(2)}\n` : ''; return { content: [ { type: 'text', text: `## Area of Circle\n\nThe area is π × ${radius}² = ${area.toFixed(2)} square units.${steps}`, }, ], }; } else { return { content: [{ type: 'text', text: 'Area calculation cancelled by user.' }] }; } } catch (e) { // NOTE: `elicitInput` can fail for several reasons: the transport // was closed, the request timed out, or the client explicitly // rejected the elicitation. We wrap this in a generic InternalError // because from the tool's perspective, its interactive flow was // unexpectedly interrupted. throw new McpError( ErrorCode.InternalError, 'Failed to complete interactive input with the user.', ); } } } // Default response for non-area or non-ambiguous problems return { content: [ { type: 'text', text: `## Solving: ${problem}\n\n` + `This problem would be solved using appropriate mathematical methods.\n` + `${showSteps ? '\n### Steps:\n1. Analyze the problem\n2. Identify key values\n3. Apply appropriate formula\n4. Calculate result\n' : ''}`, }, ], }; }, ); // --- TOOL: explain_formula --- // Demonstrates informational tools with optional parameters const explainFormulaInputSchema = { formula: z.string().describe('The formula to explain'), examples: z.boolean().optional().describe('Include examples'), }; server.registerTool( 'explain_formula', { title: 'Explain Formula', description: 'Explain a mathematical formula interactively', inputSchema: explainFormulaInputSchema, }, async ({ formula, examples }) => { log.info('Explaining formula'); requestCount++; return { content: [ { type: 'text', text: `## Formula Explanation: ${formula}\n\n` + `This tool provides interactive explanations of mathematical formulas.\n` + `${examples ? '\n### Examples:\n- Example calculations would be shown here\n- Visual representations might be included\n' : ''}\n` + `**Note**: This tool may use elicitInput for interactive learning.`, }, ], }; }, ); // --- TOOL: calculator_assistant --- // Demonstrates context-aware assistance const calculatorAssistantInputSchema = { query: z.string().describe('The user query or question'), context: z.string().optional().describe('Additional context'), }; server.registerTool( 'calculator_assistant', { title: 'Calculator Assistant', description: 'Interactive calculator assistance with context-aware help', inputSchema: calculatorAssistantInputSchema, }, async ({ query, context }) => { log.info('Calculator assistant activated'); requestCount++; return { content: [ { type: 'text', text: `## Calculator Assistant\n\n` + `**Query**: ${query}\n` + `${context ? `**Context**: ${context}\n` : ''}\n` + `I can help you with:\n` + `- Basic arithmetic operations\n` + `- Advanced calculations (factorials, logarithms, etc.)\n` + `- Formula explanations\n` + `- Step-by-step problem solving\n\n` + `**Note**: This assistant may use elicitInput for clarification.`, }, ], }; }, ); } /** * The main factory for the MCP Calculator Server. * This function creates a new McpServer instance and then composes it by * calling a series of dedicated registration functions for tools, resources, * and prompts. This pattern keeps the setup logic organized and scannable. */ export async function createCalculatorServer() { // 1. Create the server instance with metadata and instructions. const server = new McpServer( { name: SERVER_INFO.name, version: SERVER_INFO.version, }, { instructions: `${SERVER_INFO.description} This learning-edition server provides: 1. **Core Tools**: calculate, explain-calculation, generate-problems 2. **Extended Tools**: batch_calculate, advanced_calculate, demo_progress, solve_math_problem, explain_formula, calculator_assistant 3. **Core Resources**: calculator://constants 4. **Extended Resources**: calculator://history/{id}, calculator://stats, formulas://library 5. **Prompts**: Interactive mathematical assistance with potential elicitInput requests All operations support the golden standard feature set.`, }, ); // 2. Register all capabilities by calling dedicated functions. // This composition pattern is easier to read and maintain. registerCoreTools(server); registerExtendedTools(server); registerResources(server); registerPrompts(server); registerManagementTools(server); // 3. Return the fully configured server instance. return server; } /** * Registers all resource endpoints with the MCP server. * Resources provide read-only access to data and content. * * @param server - The MCP server instance to register resources with */ function registerResources(server: McpServer): void { // --- RESOURCE: Mathematical Constants --- // Demonstrates static data serving with JSON format server.registerResource( 'math-constants', 'calculator://constants', { title: 'Mathematical Constants', description: 'Common mathematical constants with high precision', mimeType: 'application/json', }, async (uri) => { log.info('Serving mathematical constants'); return { contents: [ { uri: uri.href, mimeType: 'application/json', text: JSON.stringify(MATH_CONSTANTS, null, 2), }, ], }; }, ); // --- RESOURCE: Calculation History (Template) --- // Demonstrates dynamic resources with completion support and parameterized URIs server.registerResource( 'calculation-history', new ResourceTemplate('calculator://history/{calculationId}', { list: async () => { const sortedHistory = [...calculationHistory] .sort((a, b) => b.timestamp.localeCompare(a.timestamp)) .slice(0, LIMITS.HISTORY_DISPLAY_LIMIT); return { resources: sortedHistory.map((calc) => ({ uri: `calculator://history/${calc.id}`, name: calc.expression, description: `Calculation performed at ${new Date(calc.timestamp).toLocaleString()}`, mimeType: 'application/json', })), }; }, complete: { calculationId: async (value: string) => { return calculationHistory .map((h) => h.id) .filter((id) => id.startsWith(value)) .slice(0, 10); }, }, }), { title: 'Calculation History', description: 'Access the last 50 calculations by ID', }, async (uri, { calculationId }) => { const calculation = calculationHistory.find((h) => h.id === calculationId); if (!calculation) { // NOTE: This is a client error. The client requested a resource with an ID // that does not exist. `InvalidParams` is the correct code because the // `{calculationId}` parameter in the URI template is invalid. throw new McpError( ErrorCode.InvalidParams, `Resource not found. No calculation exists with ID '${calculationId}'.`, ); } log.info(`Serving calculation history for ${calculationId}`); return { contents: [ { uri: uri.href, mimeType: 'application/json', text: JSON.stringify(calculation, null, 2), }, ], }; }, ); // --- RESOURCE: Statistics --- // Demonstrates dynamic data generation from server state server.registerResource( 'calculator-stats', 'calculator://stats', { title: 'Calculator Statistics', description: 'Server uptime and request count', mimeType: 'application/json', }, async (uri) => { const stats = { uptimeMs: Date.now() - serverStartTime, requestCount, }; log.info('Serving calculator statistics'); return { contents: [ { uri: uri.href, mimeType: 'application/json', text: JSON.stringify(stats, null, 2), }, ], }; }, ); // --- RESOURCE: Formulas Library --- // Demonstrates serving structured knowledge data server.registerResource( 'formulas-library', 'formulas://library', { title: 'Formulas Library', description: 'Collection of common mathematical formulas', mimeType: 'application/json', }, async (uri) => { log.info('Serving formulas library'); return { contents: [ { uri: uri.href, mimeType: 'application/json', text: JSON.stringify(FORMULAS_LIBRARY, null, 2), }, ], }; }, ); } /** * Registers prompt templates that can be used to generate context for LLMs. * Prompts demonstrate the completable pattern for argument autocompletion. * * @param server - The MCP server instance to register prompts with */ function registerPrompts(server: McpServer): void { // --- PROMPT: explain-calculation --- // Demonstrates prompt generation with completable arguments server.registerPrompt( 'explain-calculation', { title: 'Explain Calculation', description: 'Generate detailed explanations of mathematical calculations', argsSchema: { expression: z.string().describe('The calculation to explain'), level: completable(z.enum(['elementary', 'intermediate', 'advanced']), async (value) => { const options: ('elementary' | 'intermediate' | 'advanced')[] = [ 'elementary', 'intermediate', 'advanced', ]; if (!value) return options; return options.filter((l) => l.startsWith(value)); }).optional(), }, }, ({ expression, level }) => { log.info('Generated explanation prompt'); const levelText = level || 'intermediate'; const prompt = `Please explain the calculation "${expression}" at an ${levelText} level. Include: - What the operation means - Step-by-step breakdown - Why this calculation might be useful - Common mistakes to avoid Make the explanation clear and educational.`; return { messages: [ { role: 'user', content: { type: 'text', text: prompt, }, }, ], }; }, ); // --- PROMPT: generate-problems --- // Demonstrates dynamic prompt generation with multiple completable parameters server.registerPrompt( 'generate-problems', { title: 'Generate Problems', description: 'Create practice math problems', argsSchema: { difficulty: completable( z.enum(['easy', 'medium', 'hard']).describe('The difficulty of the problems'), async (value) => { const options: ('easy' | 'medium' | 'hard')[] = ['easy', 'medium', 'hard']; if (!value) return options; return options.filter((d) => d.startsWith(value)); }, ).optional(), count: z.string().optional(), operations: completable( z.string().describe('Comma-separated list of operations'), async (value) => { const ops = ['add', 'subtract', 'multiply', 'divide']; if (!value) return ops; return ops.filter((op) => op.includes(value.toLowerCase())); }, ).optional(), }, }, ({ difficulty, count, operations }) => { log.info('Generated problems prompt'); const difficultyLevel = difficulty || 'medium'; const problemCount = count ? parseInt(count, 10) : 5; const ops = operations || 'add, subtract, multiply, divide'; const prompt = `Generate ${problemCount} math practice problems at ${difficultyLevel} difficulty level. Use these operations: ${ops} For each problem: 1. Provide the problem statement 2. Leave space for the student to work 3. Include the answer (marked clearly) Make the problems progressively challenging and educational.`; return { messages: [ { role: 'user', content: { type: 'text', text: prompt, }, }, ], }; }, ); // --- PROMPT: calculator-tutor --- // Demonstrates interactive tutoring with topic-based completion server.registerPrompt( 'calculator-tutor', { title: 'Calculator Tutor', description: 'Interactive mathematics tutoring', argsSchema: { topic: completable( z.string().describe('The mathematical topic to tutor'), async (value) => { const topics = [ 'algebra', 'geometry', 'calculus', 'arithmetic', 'statistics', 'trigonometry', ]; if (!value) return topics; return topics.filter((t) => t.toLowerCase().includes(value.toLowerCase())); }, ), level: completable(z.enum(['beginner', 'intermediate', 'advanced']), async (value) => { const options: ('beginner' | 'intermediate' | 'advanced')[] = [ 'beginner', 'intermediate', 'advanced', ]; if (!value) return options; return options.filter((l) => l.startsWith(value)); }).optional(), }, }, ({ topic, level }) => { log.info('Generated tutor prompt'); const studentLevel = level || 'beginner'; const prompt = `Act as a friendly and patient math tutor helping a ${studentLevel} student with "${topic}". Your approach should: - Start by assessing what the student already knows - Break down complex concepts into simple steps - Use relatable examples and analogies - Encourage the student with positive reinforcement - Provide practice problems appropriate to their level Begin the tutoring session by introducing yourself and the topic.`; return { messages: [ { role: 'user', content: { type: 'text', text: prompt, }, }, ], }; }, ); } /** * Registers management and administrative tools. * These tools demonstrate advanced SDK features like tool management. * * @param server - The MCP server instance to register management tools with */ function registerManagementTools(server: McpServer): void { // NOTE: At this point, the core tools have already been registered by registerCoreTools // and registerExtendedTools. This function adds management and administrative tools. // For this demo, we implement a simplified maintenance mode that simulates tool lifecycle management. // --- TOOL: maintenance_mode --- // Demonstrates a management tool (simplified version for this demo) const maintenanceModeInputSchema = { toolName: z .enum(['calculate', 'batch_calculate', 'advanced_calculate']) .describe('Tool to simulate managing'), enable: z.boolean().describe('Enable (true) or disable (false) the tool'), }; const maintenanceModeOutputSchema = { status: z.string(), toolName: z.string(), enabled: z.boolean(), message: z.string(), }; server.registerTool( 'maintenance_mode', { title: 'Maintenance Mode', description: 'Simulate enabling or disabling tools for maintenance (demo only)', inputSchema: maintenanceModeInputSchema, outputSchema: maintenanceModeOutputSchema, }, async ({ toolName, enable }) => { log.info(`Simulating maintenance mode for ${toolName}: ${enable ? 'enabled' : 'disabled'}`); requestCount++; // NOTE: In a real implementation, this would use tool handles to actually // enable/disable tools. For this educational demo, we simulate the action. const message = enable ? `Tool '${toolName}' would be enabled and available for use.` : `Tool '${toolName}' would be disabled for maintenance and temporarily unavailable.`; return { content: [ { type: 'text', text: `## Maintenance Mode (Demo)\n\n${message}\n\n**Note**: This is a demonstration tool. In a production server, this would use actual tool lifecycle management.`, }, ], structuredContent: { status: enable ? 'enabled' : 'disabled', toolName, enabled: enable, message, }, }; }, ); } // === SECTION: EXECUTION === // Protocol Error Flow for STDIO Transport (high-level): // 1. Client (Parent Process) sends a JSON-RPC request string to the server's `stdin`. // 2. The `StdioServerTransport` reads and parses the JSON. // - On malformed JSON -> throws McpError(ErrorCode.ParseError). // 3. The SDK routes the request to the appropriate handler (`registerTool`, etc.). // - On unknown method -> throws McpError(ErrorCode.MethodNotFound). // 4. The SDK validates the request `params` against the tool's `inputSchema`. // - On schema mismatch -> throws McpError(ErrorCode.InvalidParams). // 5. The registered handler function is executed. // - On business logic failure (e.g., divide by zero) -> handler throws McpError. // - On unexpected internal failure (e.g., bug) -> caught and wrapped in McpError(ErrorCode.InternalError). // 6. The `StdioServerTransport` serializes the success or error response to JSON // and writes it to `stdout`, where the client reads it. /** * Sets up signal handlers for graceful process termination. * This is critical for a process-based tool to ensure it cleans up * and exits correctly when its parent process sends a signal. */ function setupGracefulShutdown(): void { process.on('SIGINT', () => { log.info('Received SIGINT, shutting down gracefully'); process.exit(EXIT_CODES.SUCCESS); }); process.on('SIGTERM', () => { log.info('Received SIGTERM, shutting down gracefully'); process.exit(EXIT_CODES.SUCCESS); }); } /** * Sets up global "last-resort" error handlers. * If these are triggered, it signifies a bug in the application that was * not caught by local try/catch blocks. They prevent the process from * hanging and ensure it exits with a non-zero error code. */ function setupGlobalErrorHandlers(): void { process.on('uncaughtException', (error) => { log.error(`[FATAL] Uncaught exception: ${error.message}`); log.error(`Stack trace: ${error.stack}`); process.exit(EXIT_CODES.SOFTWARE_ERROR); }); process.on('unhandledRejection', (reason) => { log.error(`[FATAL] Unhandled promise rejection: ${String(reason)}`); process.exit(EXIT_CODES.SOFTWARE_ERROR); }); } /** * Main entry point for the MCP STDIO Calculator Server. * * This function orchestrates the complete server lifecycle: * 1. Command-line argument processing for help and debug modes * 2. Server instantiation and configuration via createCalculatorServer() * 3. Transport connection using the standard STDIO transport * 4. Signal handling for graceful shutdown (SIGINT, SIGTERM) * 5. Global error handling for uncaught exceptions and promise rejections * * WHY this approach: * - STDIO transport is ideal for process-based tools and IDE integrations * - Signal handling ensures clean shutdown when parent process terminates * - Error boundaries prevent the server from crashing unexpectedly * - Command-line args provide essential debugging and help functionality */ async function main() { try { log.info('Starting calculator server'); // STEP 1: Process command-line arguments // WHY: Essential for debugging and user guidance if (process.argv.includes('--help')) { console.error(` ${SERVER_INFO.name} v${SERVER_INFO.version} ${SERVER_INFO.description} Usage: node server.js [options] Options: --debug Enable debug logging to stderr --help Show this help message and exit `); process.exit(EXIT_CODES.SUCCESS); } // STEP 2: Create and configure the MCP server // This calls our modular createCalculatorServer function which registers // all tools, resources, and prompts through dedicated registration functions const server = await createCalculatorServer(); // STEP 3: Connect to STDIO transport // WHY: STDIO transport provides the simplest and most secure communication // channel for process-based MCP servers. stdin/stdout handle JSON-RPC. const transport = new StdioServerTransport(); await server.connect(transport); log.info('Calculator server ready (SDK STDIO transport)'); // STEP 4: Set up process lifecycle handlers setupGracefulShutdown(); setupGlobalErrorHandlers(); // At this point, the server is fully operational and will run until // it receives a termination signal or encounters an unrecoverable error. } catch (error) { // This block catches errors during the server's INITIALIZATION phase. // If `createCalculatorServer()` or `server.connect()` fails, this ensures // the failure is logged and the process exits with a software error code. log.error(`Failed to start server: ${error instanceof Error ? error.message : String(error)}`); if (error instanceof Error && error.stack) { log.error(`Stack trace: ${error.stack}`); } process.exit(EXIT_CODES.SOFTWARE_ERROR); } } // ENTRY POINT CHECK // Only run main() if this file is executed directly (not imported as a module) // WHY: This allows the server to be imported for testing without auto-starting if (import.meta.url === `file://${process.argv[1]}`) { void main(); }

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

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