Skip to main content
Glama
http-mcp-server.ts8.62 kB
import http from 'http'; import { URL } from 'url'; // Note: include .js extension so emitted ESM imports resolve correctly at runtime import { SimplicateServiceExtended } from './simplicate/services-extended.js'; const PORT = Number(process.env.PORT || 3002); const service = new SimplicateServiceExtended(); function writeJson(res: http.ServerResponse, code: number, obj: any) { const body = JSON.stringify(obj); res.writeHead(code, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'Access-Control-Allow-Origin': '*', }); res.end(body); } function toMethodName(toolName: string) { // convert snake_case to camelCase: get_projects -> getProjects return toolName.split('_').map((p, i) => i === 0 ? p : p[0].toUpperCase() + p.slice(1)).join(''); } async function dispatchTool(toolName: string, args: any) { const method = toMethodName(toolName); // heuristic param extraction const params: any[] = []; if (args && typeof args === 'object') { // common id patterns const idKeys = ['project_id','organization_id','person_id','task_id','service_id','invoice_id','id']; let foundId = false; for (const k of idKeys) { if (k in args) { params.push(args[k]); foundId = true; break; } } if (args.data) params.push(args.data); if (!foundId && params.length === 0) params.push(args); } else if (args !== undefined) { params.push(args); } // @ts-ignore - dynamic call if (typeof (service as any)[method] === 'function') { // call and return // Some methods expect a single primitive id; spread params return await (service as any)[method](...params); } throw new Error(`Unknown tool/method: ${toolName} -> ${method}`); } // Track connected SSE clients for MCP HTTP flow const sseClients = new Set<http.ServerResponse>(); function broadcastMcpMessage(message: any) { const payload = JSON.stringify(message); for (const client of Array.from(sseClients)) { try { client.write('event: message\n'); client.write(`data: ${payload}\n\n`); } catch { // if write fails, drop client try { client.end(); } catch {} sseClients.delete(client); } } } const server = http.createServer(async (req, res) => { try { const url = new URL(req.url || '/', `http://localhost`); // Basic CORS preflight support if (req.method === 'OPTIONS') { res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authentication-Key, Authentication-Secret, Accept', 'Access-Control-Max-Age': '86400', }); res.end(); return; } // Simple SSE endpoint to satisfy clients that require an SSE URL (like n8n MCP node) if (req.method === 'GET' && url.pathname === '/sse') { // Set required SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache, no-transform', Connection: 'keep-alive', 'X-Accel-Buffering': 'no', 'Access-Control-Allow-Origin': '*', }); // Flush headers immediately when supported (helps some proxies) // @ts-ignore if (typeof res.flushHeaders === 'function') res.flushHeaders(); // Track this client for MCP push messages sseClients.add(res); // send initial comment and event console.error('[SSE] client connected'); res.write(': connected\n\n'); res.write('event: connected\n'); res.write('data: {"status":"ok"}\n\n'); // recommend client retry delay res.write('retry: 30000\n\n'); // send keepalive every 20s const keepAlive = setInterval(() => { try { // send both a comment and a lightweight ping event res.write(': keep-alive\n\n'); res.write('event: ping\n'); res.write('data: {}\n\n'); } catch (e) { /* ignore */ } }, 20000); // cleanup when client disconnects req.on('close', () => { clearInterval(keepAlive); sseClients.delete(res); console.error('[SSE] client disconnected'); }); return; } if (req.method === 'GET' && url.pathname === '/health') { return writeJson(res, 200, { status: 'ok', uptime: process.uptime(), node: process.version }); } if (req.method === 'GET' && url.pathname === '/tools') { // minimal list derived from README and available service methods const tools = [ 'get_projects','get_project','create_project','update_project','delete_project','get_project_services', 'get_organizations','get_organization','create_organization','update_organization','get_persons','get_person','create_person','update_person', 'get_services','get_service','create_service','get_default_services','get_tasks','get_task','create_task','update_task', 'get_documents','get_document','get_contracts','get_contract','create_contract','get_custom_fields','search' ]; return writeJson(res, 200, { tools }); } if (req.method === 'POST' && url.pathname === '/call') { let body = ''; for await (const chunk of req) body += chunk; let payload: any; try { payload = JSON.parse(body || '{}'); } catch (err) { return writeJson(res, 400, { error: 'invalid_json' }); } // If request looks like MCP (method + id), route accordingly and push result over SSE if (payload && typeof payload === 'object' && payload.method && payload.id) { const requestId = payload.id; const method = String(payload.method); const params = payload.params || {}; try { if (method === 'tools/list' || method === 'tools/listTools') { const tools = [ 'get_projects','get_project','create_project','update_project','delete_project','get_project_services', 'get_organizations','get_organization','create_organization','update_organization','get_persons','get_person','create_person','update_person', 'get_services','get_service','create_service','get_default_services','get_tasks','get_task','create_task','update_task', 'get_documents','get_document','get_contracts','get_contract','create_contract','get_custom_fields','search' ].map(t => ({ name: t })); broadcastMcpMessage({ type: 'response', id: requestId, result: { tools } }); res.writeHead(202, { 'Access-Control-Allow-Origin': '*' }); res.end(); return; } if (method === 'tools/call' || method === 'callTool') { const toolName = params.name || params.toolName || params.tool; const toolArgs = params.arguments || params.args || {}; if (!toolName) return writeJson(res, 400, { error: 'missing_name' }); const result = await dispatchTool(toolName, toolArgs); broadcastMcpMessage({ type: 'response', id: requestId, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } }); res.writeHead(202, { 'Access-Control-Allow-Origin': '*' }); res.end(); return; } // Unknown method broadcastMcpMessage({ type: 'response', id: requestId, error: { message: `Unknown method: ${method}` } }); res.writeHead(202, { 'Access-Control-Allow-Origin': '*' }); res.end(); return; } catch (err: any) { broadcastMcpMessage({ type: 'response', id: requestId, error: { message: err?.message || String(err) } }); res.writeHead(202, { 'Access-Control-Allow-Origin': '*' }); res.end(); return; } } // Fallback: simple JSON flow used by curl const { name, arguments: args } = payload; if (!name) return writeJson(res, 400, { error: 'missing_name' }); try { const result = await dispatchTool(name, args || {}); return writeJson(res, 200, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }); } catch (err: any) { return writeJson(res, 500, { error: true, message: err.message || String(err) }); } } // default writeJson(res, 404, { error: 'not_found' }); } catch (err: any) { writeJson(res, 500, { error: true, message: err.message || String(err) }); } }); server.listen(PORT, () => { // eslint-disable-next-line no-console console.error(`HTTP MCP server listening on port ${PORT}`); }); export {};

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/daanno/simplicate-mcp'

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