Skip to main content
Glama
server-remote.ts10.2 kB
import * as fs from "fs"; import * as path from "path"; import fetch from "node-fetch"; import express from "express"; import cors from "cors"; import { randomUUID } from "node:crypto"; import jwt, { JwtHeader, SigningKeyCallback } from "jsonwebtoken"; import jwksClient from "jwks-rsa"; import { mcpAuthMetadataRouter, getOAuthProtectedResourceMetadataUrl, } from "@modelcontextprotocol/sdk/server/auth/router.js"; import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js" import { loadTools } from "./toolLoader.js"; // Load configuration const configPath = path.resolve( path.dirname(new URL(import.meta.url).pathname), "../config/config.json" ); const configData = JSON.parse(fs.readFileSync(configPath, "utf-8")); // Always read the latest config from the file system function getConfigData() { return JSON.parse(fs.readFileSync(configPath, "utf-8")); } const MCP_SERVER_BASE_URL = configData.MCP_SERVER_BASE_URL; const AUTHZ_SERVER_BASE_URL = configData.AUTHZ_SERVER_BASE_URL; const SCOPES_SUPPORTED = configData.SCOPES_SUPPORTED; export const BACKEND_API_BASE = configData.BACKEND_API_BASE; export const BACKEND_AUTH_TOKEN = () => getConfigData().BACKEND_AUTH_TOKEN; export const USER_AGENT = "OFBiz-MCP-server"; // Server configuration const SERVER_PORT = configData.SERVER_PORT; /* const USE_HTTPS = configData.USE_HTTPS || false; const SSL_KEY_PATH = configData.SSL_KEY_PATH; const SSL_CERT_PATH = configData.SSL_CERT_PATH; */ const enableAuth = (MCP_SERVER_BASE_URL && AUTHZ_SERVER_BASE_URL); // Function to fetch JWKS URI from OpenID Connect metadata async function getJwksUri(issuer: string): Promise<string> { const res = await fetch(`${issuer}/.well-known/openid-configuration`); if (!res.ok) throw new Error(`Failed to fetch metadata: ${res.statusText}`); const metadata = await res.json() as Record<string, unknown>; const jwks = metadata["jwks_uri"]; if (typeof jwks !== "string") { throw new Error("Invalid OpenID metadata: 'jwks_uri' missing or not a string"); } return jwks; } // Create a JWKS client to retrieve the public key const client = !enableAuth ? null : jwksClient({ jwksUri: await getJwksUri(AUTHZ_SERVER_BASE_URL), cache: true, // enable local caching cacheMaxEntries: 5, // maximum number of keys stored cacheMaxAge: 10 * 60 * 1000, // 10 minutes }); // Function to get the public key from the JWT's kid function getKey(header: JwtHeader, callback: SigningKeyCallback) { if (!client) { return callback(new Error("JWKS client not initialized")); } if (!header.kid) { return callback(new Error("Missing 'kid' in token header")); } client.getSigningKey(header.kid, (err, key) => { if (err) return callback(err, undefined); const signingKey = key?.getPublicKey(); callback(null, signingKey); }); } async function validateAccessToken(token: string): Promise<{ valid: boolean; clientId?: string; scopes?: string[]; userId?: string; audience?: string; }> { try { // Using JWT tokens, validate locally const result = await new Promise<any>((resolve, reject) => { jwt.verify( token, getKey, { algorithms: ["RS256"], // FIXME: adjust based on token's algorithm audience: MCP_SERVER_BASE_URL, issuer: AUTHZ_SERVER_BASE_URL, }, (err, decoded) => { if (err) return reject(err); resolve(decoded); } ); }); return { valid: true, clientId: result.client_id, scopes: result.scope?.split(' '), userId: result.sub, audience: result.aud }; } catch (error) { console.error('Token validation error:', error); return { valid: false }; } } // Middleware to check for valid access token const authenticateRequest = async (req: express.Request, res: express.Response, next: express.NextFunction) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { // Return 401 with WWW-Authenticate header pointing to metadata res.status(401) .set('WWW-Authenticate', `Bearer realm="mcp", as_uri="${resourceMetadataUrl}"`) .json({ jsonrpc: '2.0', error: { code: -32001, message: 'Authorization required' }, id: null }); return; } const token = authHeader.substring(7); try { // Validate the access token const validationResult = await validateAccessToken(token); if (!validationResult.valid) { res.status(401) .set('WWW-Authenticate', `Bearer realm="mcp", error="invalid_token", as_uri="${resourceMetadataUrl}"`) .json({ jsonrpc: '2.0', error: { code: -32001, message: 'Invalid or expired token' }, id: null }); return; } // Attach user/token info to request for use in handlers (req as any).auth = validationResult; next(); } catch (error) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error during authentication' }, id: null }); } }; const handleMcpRequest = async (req: express.Request, res: express.Response) => { // Check for existing session ID const sessionId = req.headers['mcp-session-id'] as string | undefined; let transport: StreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId) => { // Store the transport by session ID transports[sessionId] = transport; }, // FIXME: // DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server // locally, make sure to set: // enableDnsRebindingProtection: true, // allowedHosts: ['127.0.0.1'], }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { delete transports[transport.sessionId]; } }; const server = new McpServer({ name: "Apache OFBiz MCP Server (Streamable HTTP)", version: "0.1.0" }); // Load and register tools from external files async function registerTools() { try { const tools = await loadTools(); for (const tool of tools) { server.registerTool( tool.name, tool.metadata, tool.handler ); console.error(`Registered tool: ${tool.name}`); } } catch (error) { console.error("Error loading tools:", error); throw error; } } // Set up server resources, tools, and prompts await registerTools(); // Connect to the MCP server await server.connect(transport); } else { // Invalid request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }); return; } // Handle the request console.log(`Processing request for session ${sessionId}`); await transport.handleRequest(req, res, req.body); console.log(`Completed request for session ${sessionId}`); }; // Reusable handler for GET and DELETE requests const handleSessionRequest = async (req: express.Request, res: express.Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !transports[sessionId]) { res.status(400).send('Invalid or missing session ID'); return; } const transport = transports[sessionId]; await transport.handleRequest(req, res); }; // Map to store transports by session ID const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; // Precompute resource metadata URL const resourceMetadataUrl = (enableAuth ? getOAuthProtectedResourceMetadataUrl(new URL(MCP_SERVER_BASE_URL)) : ""); // ====================================================================== // Initialization of Express app // ====================================================================== const app = express(); app.use(express.json()); // Allow CORS all domains, expose the Mcp-Session-Id header app.use( cors({ origin: '*', // Allow all origins exposedHeaders: ['Mcp-Session-Id'] }) ); if (enableAuth) { // Handle OAuth Protected Resource Metadata endpoint (RFC9728) app.use( mcpAuthMetadataRouter({ oauthMetadata: { issuer: new URL(AUTHZ_SERVER_BASE_URL).toString(), introspection_endpoint: "", authorization_endpoint: "", token_endpoint: "", registration_endpoint: "", // optional response_types_supported: ["code"] }, resourceServerUrl: new URL(MCP_SERVER_BASE_URL), scopesSupported: SCOPES_SUPPORTED, resourceName: "Apache OFBiz MCP Server", // optional }), ); // Handle POST requests for authenticated client-to-server communication app.post('/mcp', authenticateRequest, handleMcpRequest); } else { // Handle POST requests for unauthenticated client-to-server communication app.post('/mcp', handleMcpRequest); } // Handle GET requests for server-to-client notifications via SSE app.get('/mcp', handleSessionRequest); // Handle DELETE requests for session termination app.delete('/mcp', handleSessionRequest); app.listen(SERVER_PORT, () => { console.log(`MCP stateful Streamable HTTP Server listening on port ${SERVER_PORT} with ${enableAuth ? 'authentication' : 'no authentication'}.`); });

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/jacopoc/mcp-for-apache-ofbiz'

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