server: remote HTTP MCP endpoint (Streamable HTTP /mcp) + Node Dockerfile; landing served at /
This commit is contained in:
+16
-39
@@ -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
@@ -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 & 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=…&steadfast_secret=…</code>).</p>` +
|
||||
`<p>By <a href="https://auraajenticai.cloud">Aura Ajentic AI</a> · <a href="https://courier.auraajenticai.cloud">docs & setup</a></p></div></body></html>`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Aura Courier MCP (HTTP) listening on :${PORT} — POST /mcp`);
|
||||
});
|
||||
Regular → Executable
+5
-175
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user