import express, { Request, Response } from "express"; import { randomUUID } from "node:crypto"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import { CourierRegistry } from "./registry.js"; import { loadConfig, EnvSource } from "./config.js"; import { buildMcpServer } from "./server.js"; import path from "node:path"; import { fileURLToPath } from "node:url"; const PORT = Number(process.env.PORT || 8080); // Marketing landing (index.html) sits at the repo root, one level above dist/. const LANDING = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "index.html"); // Pull this client's courier keys from request headers or query params. // Captured once per session (on the initialize request) and bound to that session's server. function keysFromRequest(req: Request): EnvSource { const h = req.headers; const q = req.query as Record; const pick = (header: string, query: string): string | undefined => { const hv = h[header]; const v = (Array.isArray(hv) ? hv[0] : hv) ?? q[query]; return v ? String(v) : undefined; }; return { STEADFAST_API_KEY: pick("x-steadfast-api-key", "steadfast_key"), STEADFAST_SECRET_KEY: pick("x-steadfast-secret-key", "steadfast_secret"), STEADFAST_BASE_URL: pick("x-steadfast-base-url", "steadfast_base_url"), PATHAO_CLIENT_ID: pick("x-pathao-client-id", "pathao_client_id"), PATHAO_CLIENT_SECRET: pick("x-pathao-client-secret", "pathao_client_secret"), PATHAO_USERNAME: pick("x-pathao-username", "pathao_username"), PATHAO_PASSWORD: pick("x-pathao-password", "pathao_password"), PATHAO_STORE_ID: pick("x-pathao-store-id", "pathao_store_id"), PATHAO_BASE_URL: pick("x-pathao-base-url", "pathao_base_url"), REDX_API_TOKEN: pick("x-redx-api-token", "redx_token"), REDX_BASE_URL: pick("x-redx-base-url", "redx_base_url"), REDX_PICKUP_STORE_ID: pick("x-redx-pickup-store-id", "redx_pickup_store_id"), PAPERFLY_API_KEY: pick("x-paperfly-api-key", "paperfly_key"), PAPERFLY_USERNAME: pick("x-paperfly-username", "paperfly_username"), PAPERFLY_PASSWORD: pick("x-paperfly-password", "paperfly_password"), PAPERFLY_STORE_NAME: pick("x-paperfly-store-name", "paperfly_store"), }; } const app = express(); app.use(express.json({ limit: "1mb" })); // Allow browser-based and cross-origin MCP clients. app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header( "Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, mcp-session-id, mcp-protocol-version, x-steadfast-api-key, x-steadfast-secret-key, x-steadfast-base-url, x-pathao-client-id, x-pathao-client-secret, x-pathao-username, x-pathao-password, x-pathao-store-id, x-pathao-base-url, x-redx-api-token, x-redx-base-url, x-redx-pickup-store-id, x-paperfly-api-key, x-paperfly-username, x-paperfly-password, x-paperfly-store-name" ); res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.header("Access-Control-Expose-Headers", "mcp-session-id"); if (req.method === "OPTIONS") { res.sendStatus(204); return; } next(); }); // Active sessions: sessionId -> transport (each bound to one client's keys). const transports: Record = {}; app.post("/mcp", async (req: Request, res: Response) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; let transport: StreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Existing session โ€” reuse its server (already holds this client's keys). transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // New session โ€” capture THIS client's courier keys now. const registry = new CourierRegistry(loadConfig(keysFromRequest(req))); const server = buildMcpServer(registry); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sid) => { transports[sid] = transport; }, }); transport.onclose = () => { if (transport.sessionId) delete transports[transport.sessionId]; }; await server.connect(transport); } else { res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: missing or invalid session. Send an initialize request first." }, id: null, }); return; } try { await transport.handleRequest(req, res, req.body); } catch (err: any) { if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: `Internal error: ${err?.message || String(err)}` }, id: null, }); } } }); // GET (server->client SSE stream) and DELETE (end session) for an existing session. async function handleSessionRequest(req: Request, res: 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; } await transports[sessionId].handleRequest(req, res); } app.get("/mcp", handleSessionRequest); app.delete("/mcp", handleSessionRequest); app.get("/health", (_req, res) => res.json({ ok: true, service: "aura-courier-mcp", version: "2.1.0", sessions: Object.keys(transports).length }) ); app.get("/", (_req, res) => { // Serve the marketing landing page; fall back to a minimal page if it's missing. res.sendFile(LANDING, (err) => { if (err && !res.headersSent) { res .type("html") .send( `Aura Courier MCP` + `` + `

๐Ÿšš Aura Courier MCP

Live remote MCP endpoint for Bangladesh couriers โ€” Steadfast & Pathao.

` + `

Connect your AI to POST /mcp and pass your courier keys as headers (x-steadfast-api-key, x-steadfast-secret-key) or query params (?steadfast_key=โ€ฆ&steadfast_secret=โ€ฆ).

` + `

By Aura Ajentic AI ยท docs & setup

` ); } }); }); app.listen(PORT, () => { console.log(`Aura Courier MCP (HTTP) listening on :${PORT} โ€” POST /mcp`); });