Skip to main content
Glama

Fibonacci MCP Server

by clarkeg02
mcp-server.js11.8 kB
/** * server.js * * Express MCP Server (Streamable HTTP, Stateful) * * This code: * - Declares MCP capabilities (tools, resources, prompts). * - Creates resources: config://app (static) and users://{userId}/profile (dynamic). * - Creates tools: calculate-bmi and fibonacci. * - Creates a prompt: review-code. * - Captures the client's headers at initialization time. * - Manages per-session MCP server instances in memory. * - Serves the index.html home page. * - Includes security middleware and proper error handling. */ import express from 'express'; import { randomUUID } from 'crypto'; import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { readFileSync } from 'fs'; import { join } from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; // Get __dirname equivalent for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); // Security middleware app.use(express.json({ limit: '10mb' })); // Limit request size app.use(express.urlencoded({ extended: true, limit: '10mb' })); // CORS configuration for MCP clients app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Authorization'); res.header('Access-Control-Max-Age', '86400'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); // Basic rate limiting (simple in-memory implementation) const rateLimitStore = new Map(); const RATE_LIMIT_WINDOW = 15 * 60 * 1000; // 15 minutes const RATE_LIMIT_MAX_REQUESTS = 1000; // 1000 requests per window app.use((req, res, next) => { const clientId = req.ip || req.connection.remoteAddress; const now = Date.now(); if (!rateLimitStore.has(clientId)) { rateLimitStore.set(clientId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW }); } else { const client = rateLimitStore.get(clientId); if (now > client.resetTime) { client.count = 1; client.resetTime = now + RATE_LIMIT_WINDOW; } else { client.count++; } if (client.count > RATE_LIMIT_MAX_REQUESTS) { return res.status(429).json({ jsonrpc: '2.0', error: { code: -32029, message: 'Rate limit exceeded' }, id: null }); } } next(); }); // Clean up rate limit store periodically setInterval(() => { const now = Date.now(); for (const [clientId, client] of rateLimitStore.entries()) { if (now > client.resetTime) { rateLimitStore.delete(clientId); } } }, RATE_LIMIT_WINDOW); /** * In-memory session store: * sessions[sessionId] = { * server: McpServer instance for this session, * transport: StreamableHTTPServerTransport bound to this session, * createdAt: timestamp for session cleanup * } */ const sessions = {}; // Clean up old sessions (older than 1 hour) setInterval(() => { const now = Date.now(); const SESSION_TIMEOUT = 60 * 60 * 1000; // 1 hour for (const [sessionId, session] of Object.entries(sessions)) { if (now - session.createdAt > SESSION_TIMEOUT) { delete sessions[sessionId]; } } }, 15 * 60 * 1000); // Check every 15 minutes /** * Calculate the nth Fibonacci number */ function calculateFibonacci(n) { if (n < 0) { throw new Error('Input must be a non-negative integer'); } if (n <= 1) { return n; } let prev = 0; let current = 1; for (let i = 2; i <= n; i++) { const temp = current; current = prev + current; prev = temp; } return current; } /** * Factory to create and configure a new McpServer (tools/resources/prompts) * Note: We explicitly pass `capabilities` so that the client knows we support tools. */ function createMcpServer() { const server = new McpServer({ name: 'fibonacci-mcp-server', version: '1.0.0', description: 'A secure MCP server providing mathematical tools including Fibonacci calculations', // Declare that this server supports tools, resources, and prompts capabilities: { tools: { listChanged: true }, resources: { listChanged: true }, prompts: { listChanged: true } } }); // --- Register a static resource at URI "config://app" --- server.resource( 'config', 'config://app', async (uri) => { return { contents: [ { uri: uri.href, text: 'App configuration here' } ] }; } ); // --- Register a dynamic user-profile resource --- server.resource( 'user-profile', new ResourceTemplate('users://{userId}/profile', { list: undefined }), async (uri, { userId }) => { // Basic input validation if (!userId || typeof userId !== 'string' || userId.length > 100) { throw new Error('Invalid userId provided'); } return { contents: [ { uri: uri.href, text: `Profile data for user ${userId}` } ] }; } ); // --- Register a "calculate-bmi" tool --- // The client can call this tool via "mcp/callTool" method once initialized. server.tool( 'calculate-bmi', { weightKg: z.number().min(0).max(1000), heightM: z.number().min(0.1).max(3.0) }, async ({ weightKg, heightM }) => { const bmi = weightKg / (heightM * heightM); return { content: [ { type: 'text', text: `BMI: ${bmi.toFixed(2)}` } ] }; } ); // --- Register a "fibonacci" tool --- server.tool( "fibonacci", "Calculate the nth Fibonacci number", { n: z.number().int().min(0).max(1000).describe("The position in the Fibonacci sequence (0-indexed, max 1000)"), }, async ({ n }) => { try { const result = calculateFibonacci(n); return { content: [ { type: "text", text: `The ${n}th Fibonacci number is: ${result}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error.message}`, }, ], }; } }, ); // --- Register a "review-code" prompt --- server.prompt( 'review-code', { code: z.string().max(10000) }, ({ code }) => { return { messages: [ { role: 'user', content: { type: 'text', text: `Please review this code:\n\n${code}` } } ] }; } ); return server; } // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), sessions: Object.keys(sessions).length }); }); // Handle home page app.get('/', (req, res) => { try { const htmlPath = join(__dirname, 'index.html'); const htmlContent = readFileSync(htmlPath, 'utf8'); res.send(htmlContent); } catch (error) { res.status(500).send('Error loading home page'); } }); /** * Handler for POST /mcp: * 1. If "mcp-session-id" header exists and matches a stored session, reuse that session. * 2. If no "mcp-session-id" and request is initialize, create new session and handshake. * 3. Otherwise, return a 400 error. */ app.post('/mcp', async (req, res) => { try { const sessionIdHeader = req.headers['mcp-session-id']; let sessionEntry = null; // Case 1: Existing session found if (sessionIdHeader && sessions[sessionIdHeader]) { sessionEntry = sessions[sessionIdHeader]; // Case 2: Initialization request → create new transport + server } else if (!sessionIdHeader && isInitializeRequest(req.body)) { const newSessionId = randomUUID(); // Create a new transport for this session const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => newSessionId, onsessioninitialized: (sid) => { // Store the Transport and Server instance once session is initialized sessions[sid] = { server, transport, createdAt: Date.now() }; } }); // When this transport closes, clean up the session entry transport.onclose = () => { if (transport.sessionId && sessions[transport.sessionId]) { delete sessions[transport.sessionId]; } }; // Create and configure the new McpServer const server = createMcpServer(); await server.connect(transport); // After `onsessioninitialized` fires, `sessions[newSessionId]` is set. // But we can also assign it here for immediate access. sessions[newSessionId] = { server, transport, createdAt: Date.now() }; sessionEntry = sessions[newSessionId]; } else { // Neither a valid session nor an initialize request → return error res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, id: null }); return; } // Forward the request to the transport of the retrieved/created session await sessionEntry.transport.handleRequest(req, res, req.body); } catch (error) { console.error('Error in POST /mcp:', error); res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null }); } }); /** * Handler for GET/DELETE /mcp: * Used for server-to-client notifications (SSE) and session termination. */ async function handleSessionRequest(req, res) { try { const sessionIdHeader = req.headers['mcp-session-id']; if (!sessionIdHeader || !sessions[sessionIdHeader]) { res.status(400).send('Invalid or missing session ID'); return; } const { transport } = sessions[sessionIdHeader]; await transport.handleRequest(req, res); } catch (error) { console.error('Error in handleSessionRequest:', error); res.status(500).send('Internal server error'); } } app.get('/mcp', handleSessionRequest); app.delete('/mcp', handleSessionRequest); // Global error handler app.use((error, req, res, next) => { console.error('Unhandled error:', error); res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null }); }); // 404 handler app.use((req, res) => { res.status(404).json({ jsonrpc: '2.0', error: { code: -32601, message: 'Method not found' }, id: null }); }); // Start the server on the port provided by Render or default to 7171 const PORT = process.env.PORT || 7171; const HOST = process.env.NODE_ENV === 'production' ? '0.0.0.0' : 'localhost'; // Validate environment if (process.env.NODE_ENV === 'production') { console.log('Running in production mode'); // In production, you might want to enforce HTTPS // app.use((req, res, next) => { // if (req.headers['x-forwarded-proto'] !== 'https') { // return res.redirect(`https://${req.headers.host}${req.url}`); // } // next(); // }); } app.listen(PORT, HOST, () => { console.log(`Fibonacci MCP Server listening on ${HOST}:${PORT}`); console.log(`MCP endpoint: http://${HOST}:${PORT}/mcp`); console.log(`Home page: http://${HOST}:${PORT}/`); console.log(`Health check: http://${HOST}:${PORT}/health`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); });

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/clarkeg02/fibonacci-mcp'

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