Skip to main content
Glama
index.ts6.86 kB
import express, { Request, Response, NextFunction } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { setSessionAuth } from './context.js'; import oauthRouter, { bearerValidator } from './support/oauth.js'; import { registerPaymentsTools } from './tools/payments.js'; import { register402client } from './tools/402client.js'; import { registerAuthTool } from './tools/auth.js'; import { initStore } from './support/store.js'; import { createPLinkMCPserver } from './support/mcp.js'; (async () => { await initStore(); // démarre redis OU fallback // puis démarrage du serveur MCP })(); const app = express(); // Monte /.well-known, /oauth/* app.use(await oauthRouter()); // CORS basique + exposition de l'en-tête de session pour les clients web (Inspector, etc.) app.use((req: Request, res: Response, next: NextFunction) => { res.setHeader('Access-Control-Allow-Origin', '*'); // ajuste en prod res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS'); res.setHeader( 'Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id, Mcp-Session-Id, x-api-key, x-apikey' ); // Crucial pour que les clients puissent LIRE l'ID de session renvoyé par initialize res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id'); if (req.method === 'OPTIONS') return res.sendStatus(204); next(); }); // IMPORTANT : parser le JSON AVANT le middleware d'auth POST /mcp pour lire req.body.method app.use(express.json()); // Types utilitaires type BearerValidatorResult = { apiKey: string }; app.post('/mcp', async (req, res, next) => { try { // 0) initialize passe sans auth if (req.body?.method === 'initialize') return next(); // 1) Bearer OAuth prioritaire const auth = req.get('authorization') ?? req.get('Authorization'); if (auth?.startsWith('Bearer ')) { const { apiKey } = await bearerValidator(auth); // RS256 + iss/aud/exp setSessionAuth({ ok: true, APIKEY: apiKey, scopes: ['mcp:invoke'], }); return next(); } // 2) Fallback optionnel x-api-key const xKey = req.get('x-api-key') ?? req.get('x-apikey'); if (xKey) { setSessionAuth({ ok: true, APIKEY: xKey, scopes: ['*'] }); return next(); } return next(); //return res.status(401).json({ error: 'unauthorized', detail: 'Missing Bearer or x-api-key' }); } catch (e: any) { return res.status(401).json({ error: 'invalid_token', detail: e?.message || 'bad bearer' }); } }); // Ton serveur MCP — ajoute ici tes tools/resources/prompts const mcpServer = createPLinkMCPserver(); registerAuthTool(mcpServer); registerPaymentsTools(mcpServer); register402client(mcpServer); // Map sessionId -> transport const transports: Map<string, StreamableHTTPServerTransport> = new Map(); /** * Récupère l'ID de session depuis les en-têtes, en gérant les variantes de casse. */ function getSessionId(req: Request): string | undefined { return req.get('Mcp-Session-Id') || req.get('mcp-session-id') || undefined; } /** * Helper pour capturer les erreurs async et les passer à `next()`. */ const asyncHandler = <T extends (req: Request, res: Response, next: NextFunction) => Promise<any>>(fn: T) => (req: Request, res: Response, next: NextFunction) => Promise.resolve(fn(req, res, next)).catch(next); // POST /mcp : requêtes client -> serveur (initialize, tools/*, resources/*, …) app.post( '/mcp', asyncHandler(async (req: Request, res: Response) => { const sessionId = getSessionId(req); let transport: StreamableHTTPServerTransport | undefined; if (sessionId) { transport = transports.get(sessionId); if (!transport) { return res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, id: null, }); } } else { // Première requête d'initialisation attendue const method = (req.body as any)?.method; if (method !== 'initialize') { return res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: Server not initialized' }, id: null, }); } // Crée un transport; le SDK génère et renvoie l’ID de session via l’en-tête "Mcp-Session-Id" transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId: string) => { transports.set(newSessionId, transport!); }, // Optionnel : // enableDnsRebindingProtection: true, // allowedHosts: ['127.0.0.1', 'localhost'], }); // Nettoyage à la fermeture transport.onclose = () => { const id = transport?.sessionId; if (id) transports.delete(id); }; await mcpServer.connect(transport); } // Délègue la requête JSON-RPC/Stream au transport await transport.handleRequest(req as any, res as any, (req as any).body); }) ); // GET /mcp : canal SSE pour une session donnée // DELETE /mcp : fermeture de session const handleSessionRequest = asyncHandler(async (req: Request, res: Response) => { const sessionId = getSessionId(req); if (!sessionId) { res.status(400).send('Invalid or missing session ID'); return; } const transport = transports.get(sessionId); if (!transport) { res.status(404).send('Unknown session'); return; } // Le même handleRequest gère SSE (GET) et fermeture (DELETE) await transport.handleRequest(req as any, res as any); }); app.get('/mcp', handleSessionRequest); app.delete('/mcp', handleSessionRequest); app.get('/', (_req: Request, res: Response) => { res.redirect('https://p-link.io/ApiDoc/Send'); }); // Lancement HTTP const port = Number(process.env.PORT || 8787); app .listen(port, () => { // eslint-disable-next-line no-console console.log(`MCP server running at http://localhost:${port}/mcp`); }) .on('error', (error: unknown) => { // eslint-disable-next-line no-console console.error('Server error:', error); process.exit(1); });

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/paracetamol951/P-Link-MCP'

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