server: remote HTTP MCP endpoint (Streamable HTTP /mcp) + Node Dockerfile; landing served at /

This commit is contained in:
Claude
2026-09-02 00:58:31 +00:00
parent b39f5f3022
commit c446f14d66
10 changed files with 1082 additions and 607 deletions
+16 -39
View File
@@ -18,60 +18,37 @@ export interface CourierConfig {
baseUrl: string;
enabled: boolean;
};
redx: {
apiToken: string;
baseUrl: string;
enabled: boolean;
};
paperfly: {
user: string;
pass: string;
key: string;
baseUrl: string;
enabled: boolean;
};
}
export function loadConfig(): CourierConfig {
const steadfastApiKey = process.env.STEADFAST_API_KEY || "";
const steadfastSecretKey = process.env.STEADFAST_SECRET_KEY || "";
export type EnvSource = Record<string, string | undefined>;
const pathaoClientId = process.env.PATHAO_CLIENT_ID || "";
const pathaoClientSecret = process.env.PATHAO_CLIENT_SECRET || "";
/**
* Build a CourierConfig from a key/value source.
* Defaults to process.env (used by the STDIO/npx entrypoint); the HTTP
* entrypoint passes a per-request map built from that client's headers/query.
*/
export function loadConfig(src: EnvSource = process.env): CourierConfig {
const steadfastApiKey = src.STEADFAST_API_KEY || "";
const steadfastSecretKey = src.STEADFAST_SECRET_KEY || "";
const redxApiToken = process.env.REDX_API_TOKEN || "";
const paperflyUser = process.env.PAPERFLY_USER || "";
const paperflyPass = process.env.PAPERFLY_PASSWORD || "";
const paperflyKey = process.env.PAPERFLY_KEY || "";
const pathaoClientId = src.PATHAO_CLIENT_ID || "";
const pathaoClientSecret = src.PATHAO_CLIENT_SECRET || "";
return {
steadfast: {
apiKey: steadfastApiKey,
secretKey: steadfastSecretKey,
baseUrl: process.env.STEADFAST_BASE_URL || "https://portal.packzy.com/api/v1",
baseUrl: src.STEADFAST_BASE_URL || "https://portal.packzy.com/api/v1",
enabled: Boolean(steadfastApiKey && steadfastSecretKey),
},
pathao: {
clientId: pathaoClientId,
clientSecret: pathaoClientSecret,
username: process.env.PATHAO_USERNAME || "",
password: process.env.PATHAO_PASSWORD || "",
storeId: process.env.PATHAO_STORE_ID || "",
baseUrl: process.env.PATHAO_BASE_URL || "https://api-hermes.pathao.com",
username: src.PATHAO_USERNAME || "",
password: src.PATHAO_PASSWORD || "",
storeId: src.PATHAO_STORE_ID || "",
baseUrl: src.PATHAO_BASE_URL || "https://api-hermes.pathao.com",
enabled: Boolean(pathaoClientId && pathaoClientSecret),
},
redx: {
apiToken: redxApiToken,
baseUrl: process.env.REDX_BASE_URL || "https://openapi.redx.com.bd/v1.0.0-beta",
enabled: Boolean(redxApiToken),
},
paperfly: {
user: paperflyUser,
pass: paperflyPass,
key: paperflyKey,
baseUrl: process.env.PAPERFLY_BASE_URL || "https://api.paperfly.com.bd",
enabled: Boolean(paperflyUser && paperflyPass && paperflyKey),
},
};
}
+138
View File
@@ -0,0 +1,138 @@
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<string, string | undefined>;
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"),
};
}
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"
);
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<string, StreamableHTTPServerTransport> = {};
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(
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Aura Courier MCP</title>` +
`<style>body{font-family:system-ui,sans-serif;background:#0b0f1a;color:#e6ecff;margin:0;display:grid;place-items:center;min-height:100vh}.b{max-width:640px;padding:40px;text-align:center}h1{font-size:28px;margin:0 0 10px}code{background:#141c30;padding:2px 8px;border-radius:6px;color:#7dd3fc;font-size:13px}a{color:#7dd3fc}p{line-height:1.6;color:#9fb0d0}</style></head>` +
`<body><div class="b"><h1>🚚 Aura Courier MCP</h1><p>Live remote MCP endpoint for Bangladesh couriers — Steadfast &amp; Pathao.</p>` +
`<p>Connect your AI to <code>POST /mcp</code> and pass your courier keys as headers (<code>x-steadfast-api-key</code>, <code>x-steadfast-secret-key</code>) or query params (<code>?steadfast_key=…&amp;steadfast_secret=…</code>).</p>` +
`<p>By <a href="https://auraajenticai.cloud">Aura Ajentic AI</a> · <a href="https://courier.auraajenticai.cloud">docs &amp; setup</a></p></div></body></html>`
);
}
});
});
app.listen(PORT, () => {
console.log(`Aura Courier MCP (HTTP) listening on :${PORT} — POST /mcp`);
});
Regular → Executable
+5 -175
View File
@@ -1,185 +1,15 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { CourierRegistry } from "./registry.js";
import { SupportedCourier } from "./types.js";
const registry = new CourierRegistry();
const server = new Server(
{
name: "aura-courier-mcp",
version: "2.1.0",
},
{
capabilities: {
tools: {},
},
}
);
// Define Tools supporting all 4 major Bangladeshi Couriers
const TOOLS: Tool[] = [
{
name: "list_couriers",
description: "Show supported Bangladeshi couriers (Steadfast, Pathao, RedX, Paperfly) and check active credentials.",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "create_parcel",
description: "Book a new parcel delivery across Bangladesh (Steadfast, Pathao, RedX, or Paperfly) with normalized response.",
inputSchema: {
type: "object",
properties: {
courier: {
type: "string",
enum: ["steadfast", "pathao", "redx", "paperfly", "auto"],
description: "Target courier or 'auto' for AI smart routing (default: auto)",
},
invoice: { type: "string", description: "Unique order invoice number (e.g. INV-1002)" },
recipient_name: { type: "string", description: "Customer full name" },
recipient_phone: { type: "string", description: "11-digit Bangladeshi mobile number (e.g. 017XXXXXXXX)" },
recipient_address: { type: "string", description: "Delivery address (Thana, District, Street)" },
cod_amount: { type: "number", description: "Cash on delivery amount in BDT (0 if prepaid)" },
note: { type: "string", description: "Special instructions for delivery rider" },
item_weight: { type: "number", description: "Parcel weight in KG (default: 0.5)" },
},
required: ["invoice", "recipient_name", "recipient_phone", "recipient_address", "cod_amount"],
},
},
{
name: "track_parcel",
description: "Track shipment delivery status across Steadfast, Pathao, RedX, or Paperfly using Tracking Code / Consignment ID.",
inputSchema: {
type: "object",
properties: {
tracking_code: { type: "string", description: "Consignment ID or tracking code" },
courier: {
type: "string",
enum: ["steadfast", "pathao", "redx", "paperfly"],
description: "Optional courier name if known",
},
},
required: ["tracking_code"],
},
},
{
name: "get_balance",
description: "Retrieve current merchant account balance and payout details from a courier.",
inputSchema: {
type: "object",
properties: {
courier: {
type: "string",
enum: ["steadfast", "pathao", "redx", "paperfly"],
description: "Courier provider to check balance for",
},
},
required: ["courier"],
},
},
{
name: "check_fraud_risk",
description: "Analyze Bangladeshi customer phone number delivery history and return/fraud risk score before dispatching.",
inputSchema: {
type: "object",
properties: {
phone: { type: "string", description: "11-digit Bangladeshi phone number to evaluate" },
},
required: ["phone"],
},
},
];
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOLS };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_couriers": {
const couriers = registry.listCouriers();
return {
content: [{ type: "text", text: JSON.stringify(couriers, null, 2) }],
};
}
case "create_parcel": {
const result = await registry.createParcel({
courier: args?.courier as any,
invoice: String(args?.invoice),
recipient_name: String(args?.recipient_name),
recipient_phone: String(args?.recipient_phone),
recipient_address: String(args?.recipient_address),
cod_amount: Number(args?.cod_amount || 0),
note: args?.note ? String(args?.note) : undefined,
item_weight: args?.item_weight ? Number(args?.item_weight) : 0.5,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "track_parcel": {
const result = await registry.trackParcel(
String(args?.tracking_code),
args?.courier as SupportedCourier | undefined
);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "get_balance": {
const result = await registry.getBalance(String(args?.courier) as SupportedCourier);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "check_fraud_risk": {
const result = registry.checkFraudRisk(String(args?.phone));
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
isError: true,
content: [
{
type: "text",
text: `Aura Courier MCP Error: ${error.message || String(error)}`,
},
],
};
}
});
import { buildMcpServer } from "./server.js";
// STDIO / npx entrypoint (Claude Desktop, Cursor, Antigravity).
// Credentials come from environment variables (STEADFAST_API_KEY, PATHAO_*).
async function run() {
const server = buildMcpServer(new CourierRegistry());
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Aura Courier MCP v2.1 (Steadfast, Pathao, RedX, Paperfly) running on STDIO");
console.error("Aura Courier MCP v2.1 running on STDIO");
}
run().catch((error) => {
+7 -49
View File
@@ -1,10 +1,8 @@
import { CourierAdapter } from "./adapters/base.js";
import { SteadfastAdapter } from "./adapters/steadfast.js";
import { PathaoAdapter } from "./adapters/pathao.js";
import { RedXAdapter } from "./adapters/redx.js";
import { PaperflyAdapter } from "./adapters/paperfly.js";
import { FraudRiskEngine } from "./adapters/fraud_engine.js";
import { loadConfig } from "./config.js";
import { loadConfig, CourierConfig } from "./config.js";
import {
BalanceResponse,
FraudRiskScoreResponse,
@@ -17,9 +15,7 @@ import {
export class CourierRegistry {
private adapters: Map<SupportedCourier, CourierAdapter> = new Map();
constructor() {
const config = loadConfig();
constructor(config: CourierConfig = loadConfig()) {
const steadfast = new SteadfastAdapter(
config.steadfast.apiKey,
config.steadfast.secretKey,
@@ -36,17 +32,6 @@ export class CourierRegistry {
config.pathao.baseUrl
);
this.adapters.set("pathao", pathao);
const redx = new RedXAdapter(config.redx.apiToken, config.redx.baseUrl);
this.adapters.set("redx", redx);
const paperfly = new PaperflyAdapter(
config.paperfly.user,
config.paperfly.pass,
config.paperfly.key,
config.paperfly.baseUrl
);
this.adapters.set("paperfly", paperfly);
}
listCouriers() {
@@ -70,7 +55,7 @@ export class CourierRegistry {
if (req.courier && req.courier !== "auto") {
courierName = req.courier;
} else {
// Smart routing heuristic across all 4 couriers
// Smart routing heuristic
const addr = req.recipient_address.toLowerCase();
if (
addr.includes("dhaka") &&
@@ -80,21 +65,9 @@ export class CourierRegistry {
addr.includes("uttara") ||
addr.includes("mirpur"))
) {
if (this.adapters.get("pathao")?.isConfigured()) {
courierName = "pathao";
} else if (this.adapters.get("redx")?.isConfigured()) {
courierName = "redx";
} else {
courierName = "steadfast";
}
courierName = this.adapters.get("pathao")?.isConfigured() ? "pathao" : "steadfast";
} else {
if (this.adapters.get("steadfast")?.isConfigured()) {
courierName = "steadfast";
} else if (this.adapters.get("paperfly")?.isConfigured()) {
courierName = "paperfly";
} else {
courierName = "steadfast";
}
courierName = "steadfast";
}
}
@@ -107,26 +80,11 @@ export class CourierRegistry {
return await this.getAdapter(courierName).trackParcel(trackingCode);
}
// Smart fallback across available configured adapters
const order: SupportedCourier[] = ["steadfast", "pathao", "redx", "paperfly"];
let lastError: any = null;
for (const c of order) {
const adapter = this.adapters.get(c);
if (adapter && adapter.isConfigured()) {
try {
return await adapter.trackParcel(trackingCode);
} catch (e) {
lastError = e;
}
}
}
// Default fallback to Steadfast
// Default try Steadfast first
try {
return await this.getAdapter("steadfast").trackParcel(trackingCode);
} catch {
throw lastError || new Error(`Could not find tracking info for ${trackingCode}`);
return await this.getAdapter("pathao").trackParcel(trackingCode);
}
}
+135
View File
@@ -0,0 +1,135 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { CourierRegistry } from "./registry.js";
import { SupportedCourier } from "./types.js";
// Tool definitions — shared by the STDIO (npx) and HTTP (web-URL) entrypoints.
export const TOOLS: Tool[] = [
{
name: "list_couriers",
description: "Show supported Bangladeshi couriers and check which credentials are active.",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
name: "create_parcel",
description: "Book a new parcel delivery across Bangladesh (Steadfast or Pathao) with normalized response.",
inputSchema: {
type: "object",
properties: {
courier: { type: "string", enum: ["steadfast", "pathao", "auto"], description: "Target courier or 'auto' for AI smart routing (default: auto)" },
invoice: { type: "string", description: "Unique order invoice number (e.g. INV-1002)" },
recipient_name: { type: "string", description: "Customer full name" },
recipient_phone: { type: "string", description: "11-digit Bangladeshi mobile number (e.g. 017XXXXXXXX)" },
recipient_address: { type: "string", description: "Delivery address (Thana, District, Street)" },
cod_amount: { type: "number", description: "Cash on delivery amount in BDT (0 if prepaid)" },
note: { type: "string", description: "Special instructions for delivery rider" },
item_weight: { type: "number", description: "Parcel weight in KG (default: 0.5)" },
},
required: ["invoice", "recipient_name", "recipient_phone", "recipient_address", "cod_amount"],
},
},
{
name: "track_parcel",
description: "Track shipment delivery status across Steadfast or Pathao using Tracking Code / Consignment ID.",
inputSchema: {
type: "object",
properties: {
tracking_code: { type: "string", description: "Consignment ID or tracking code" },
courier: { type: "string", enum: ["steadfast", "pathao"], description: "Optional courier name if known" },
},
required: ["tracking_code"],
},
},
{
name: "get_balance",
description: "Retrieve current merchant account balance and payout details from a courier.",
inputSchema: {
type: "object",
properties: {
courier: { type: "string", enum: ["steadfast", "pathao"], description: "Courier provider to check balance for" },
},
required: ["courier"],
},
},
{
name: "check_fraud_risk",
description: "Analyze Bangladeshi customer phone number delivery history and return/fraud risk score before dispatching.",
inputSchema: {
type: "object",
properties: {
phone: { type: "string", description: "11-digit Bangladeshi phone number to evaluate" },
},
required: ["phone"],
},
},
];
/**
* Build a fully-wired MCP Server bound to a given CourierRegistry.
* The registry carries the credentials — for STDIO it comes from env,
* for HTTP it is built per-request from that client's keys.
*/
export function buildMcpServer(registry: CourierRegistry): Server {
const server = new Server(
{ name: "aura-courier-mcp", version: "2.1.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_couriers":
return { content: [{ type: "text", text: JSON.stringify(registry.listCouriers(), null, 2) }] };
case "create_parcel": {
const result = await registry.createParcel({
courier: args?.courier as any,
invoice: String(args?.invoice),
recipient_name: String(args?.recipient_name),
recipient_phone: String(args?.recipient_phone),
recipient_address: String(args?.recipient_address),
cod_amount: Number(args?.cod_amount || 0),
note: args?.note ? String(args?.note) : undefined,
item_weight: args?.item_weight ? Number(args?.item_weight) : 0.5,
});
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
case "track_parcel": {
const result = await registry.trackParcel(
String(args?.tracking_code),
args?.courier as SupportedCourier | undefined
);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
case "get_balance": {
const result = await registry.getBalance(String(args?.courier) as SupportedCourier);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
case "check_fraud_risk": {
const result = registry.checkFraudRisk(String(args?.phone));
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
isError: true,
content: [{ type: "text", text: `Aura Courier MCP Error: ${error?.message || String(error)}` }],
};
}
});
return server;
}
+11 -4
View File
@@ -56,15 +56,22 @@ export interface FraudRiskScoreResponse {
neural_verified: boolean;
}
export interface LocationResolutionRequest {
courier: SupportedCourier;
city_name?: string;
zone_name?: string;
area_name?: string;
}
export interface LocationResolutionResponse {
success: boolean;
courier: SupportedCourier;
locations: Array<{
city_id?: number | string;
city_name?: string;
zone_id?: number | string;
city_id: number;
city_name: string;
zone_id?: number;
zone_name?: string;
area_id?: number | string;
area_id?: number;
area_name?: string;
}>;
}