Directory-submission readiness: add MCP tool annotations (title + readOnlyHint/destructiveHint), /privacy policy route + README Privacy Policy section, bump to 2.3.2

This commit is contained in:
Aura CI
2026-09-08 12:57:08 +06:00
parent c25409586c
commit 7fe26ecc70
18 changed files with 528 additions and 324 deletions
+5 -4
View File
@@ -2,12 +2,13 @@ import { CourierAdapter } from "./base.js";
import { BalanceResponse, ParcelCreateRequest, ParcelResponse, SupportedCourier, TrackingResponse } from "../types.js";
export declare class PaperflyAdapter implements CourierAdapter {
courierName: SupportedCourier;
private apiKey;
private username;
private password;
private storeName;
private client;
private user;
private pass;
private key;
private enabled;
constructor(user: string, pass: string, key: string, baseUrl?: string);
constructor(apiKey: string, username: string, password: string, storeName: string, baseUrl: string);
isConfigured(): boolean;
createParcel(req: ParcelCreateRequest): Promise<ParcelResponse>;
trackParcel(trackingCode: string): Promise<TrackingResponse>;
+56 -45
View File
@@ -1,25 +1,22 @@
import axios from "axios";
export class PaperflyAdapter {
courierName = "paperfly";
apiKey;
username;
password;
storeName;
client;
user;
pass;
key;
enabled;
constructor(user, pass, key, baseUrl = "https://api.paperfly.com.bd") {
this.user = user;
this.pass = pass;
this.key = key;
this.enabled = Boolean(user && pass && key);
const token = Buffer.from(`${user}:${pass}`).toString("base64");
constructor(apiKey, username, password, storeName, baseUrl) {
this.apiKey = apiKey;
this.username = username;
this.password = password;
this.storeName = storeName;
this.enabled = Boolean(apiKey && username && password);
this.client = axios.create({
baseURL: baseUrl,
headers: {
Authorization: `Basic ${token}`,
pap_key: key,
"Content-Type": "application/json",
},
timeout: 10000,
timeout: 15000,
headers: { "Content-Type": "application/json" },
});
}
isConfigured() {
@@ -27,59 +24,73 @@ export class PaperflyAdapter {
}
async createParcel(req) {
if (!this.enabled) {
throw new Error("Paperfly Courier credentials (User / Pass / Key) are not configured.");
throw new Error("Paperfly credentials (paperflykey + merchant username & password) are not configured.");
}
if (!this.storeName) {
throw new Error("Paperfly store name is required to create a parcel.");
}
const payload = {
merOrderRef: req.invoice,
pickMerchantName: "Aura AI Commerce",
pickMerchantAddress: "Dhaka, Bangladesh",
custName: req.recipient_name,
custPhone: req.recipient_phone,
custAddr: req.recipient_address,
customerDistrict: "Dhaka",
customerThana: "Gulshan",
packagePrice: req.cod_amount,
max_weight: req.item_weight || 0.5,
productBrief: req.item_type || "General Ecommerce item",
deliveryOption: "regular",
merchantOrderReference: req.invoice,
storeName: this.storeName,
productBrief: req.item_type || "Product",
packagePrice: String(req.value ?? req.cod_amount ?? 0),
max_weight: String(req.item_weight ?? 0.5),
customerName: req.recipient_name,
customerAddress: req.recipient_address,
customerPhone: req.recipient_phone,
};
const response = await this.client.post("/OrderPlacement", payload);
const trackingCode = response.data?.tracking_number || req.invoice;
const response = await this.client.post("/merchant/api/service/new_order_v2.php", payload, {
headers: { paperflykey: this.apiKey },
auth: { username: this.username, password: this.password },
});
const data = response.data;
const ok = data?.success;
if (!ok?.tracking_number) {
throw new Error(`Paperfly API error: ${JSON.stringify(data?.error || data?.message || data)}`);
}
return {
success: true,
courier: "paperfly",
tracking_code: trackingCode,
consignment_id: trackingCode,
tracking_code: ok.tracking_number,
consignment_id: ok.tracking_barcode || ok.tracking_number,
invoice: req.invoice,
status: "order_placed",
status: ok.message || "created",
cod_amount: req.cod_amount,
created_at: new Date().toISOString(),
raw_response: response.data,
raw_response: data,
};
}
async trackParcel(trackingCode) {
if (!this.enabled) {
throw new Error("Paperfly Courier credentials are not configured.");
throw new Error("Paperfly credentials are not configured.");
}
const response = await this.client.post("/Tracking", {
ReferenceNumber: trackingCode,
});
if (!this.username || !this.password) {
throw new Error("Paperfly tracking needs your merchant-panel username & password (Basic Auth). Add them to use track_parcel.");
}
// Paperfly tracks by your merchant order reference (merchantOrderReference), not its tracking number.
const response = await this.client.post("/API-Order-Tracking", { ReferenceNumber: trackingCode }, { headers: { paperflykey: this.apiKey }, auth: { username: this.username, password: this.password } });
const data = response.data;
const st = data?.success?.trackingStatus?.[0] || {};
const stages = [
[st.Delivered, "delivered"],
[st.Partial, "partial-delivery"],
[st.Returned, "returned"],
[st.PickedForDelivery, "out-for-delivery"],
[st.inTransit, "in-transit"],
[st.ReceivedAtPoint, "received-at-point"],
[st.Pick, "picked-up"],
];
const status = stages.find(([v]) => v && String(v).trim())?.[1] || data?.success?.message || "pending";
return {
success: true,
courier: "paperfly",
tracking_code: trackingCode,
status: data?.Status || "in_progress",
status,
updated_at: new Date().toISOString(),
raw_response: data,
};
}
async getBalance() {
return {
success: true,
courier: "paperfly",
current_balance: 0,
raw_response: { message: "Paperfly merchant settlements processed weekly via Wingman portal." },
};
throw new Error("Paperfly does not expose a merchant balance endpoint via its public API — check the Paperfly merchant panel.");
}
}
+5 -5
View File
@@ -1,14 +1,14 @@
import { CourierAdapter } from "./base.js";
import { BalanceResponse, LocationResolutionResponse, ParcelCreateRequest, ParcelResponse, SupportedCourier, TrackingResponse } from "../types.js";
export declare class RedXAdapter implements CourierAdapter {
import { BalanceResponse, ParcelCreateRequest, ParcelResponse, SupportedCourier, TrackingResponse } from "../types.js";
export declare class RedxAdapter implements CourierAdapter {
courierName: SupportedCourier;
private client;
private apiToken;
private enabled;
constructor(apiToken: string, baseUrl?: string);
private pickupStoreId;
constructor(apiToken: string, baseUrl: string, pickupStoreId?: string);
isConfigured(): boolean;
private resolveDeliveryArea;
createParcel(req: ParcelCreateRequest): Promise<ParcelResponse>;
trackParcel(trackingCode: string): Promise<TrackingResponse>;
getBalance(): Promise<BalanceResponse>;
getLocations(cityName?: string): Promise<LocationResolutionResponse>;
}
+62 -45
View File
@@ -1,94 +1,111 @@
import axios from "axios";
export class RedXAdapter {
export class RedxAdapter {
courierName = "redx";
client;
apiToken;
enabled;
constructor(apiToken, baseUrl = "https://openapi.redx.com.bd/v1.0.0-beta") {
this.apiToken = apiToken;
pickupStoreId;
constructor(apiToken, baseUrl, pickupStoreId = "") {
this.enabled = Boolean(apiToken);
this.pickupStoreId = pickupStoreId;
this.client = axios.create({
baseURL: baseUrl,
headers: {
Authorization: `Bearer ${apiToken}`,
"API-ACCESS-TOKEN": `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
timeout: 10000,
timeout: 15000,
});
}
isConfigured() {
return this.enabled;
}
// RedX's create endpoint needs a numeric delivery_area_id. If the caller did
// not pass one, best-effort resolve it from the address via GET /areas.
async resolveDeliveryArea(req) {
if (req.delivery_area_id) {
return { id: Number(req.delivery_area_id), name: req.delivery_area || "" };
}
const res = await this.client.get("/areas");
const areas = res.data?.areas || [];
const addr = (req.recipient_address || "").toLowerCase();
let best = null;
for (const a of areas) {
const base = String(a.name).split("(")[0].trim().toLowerCase();
if (base && addr.includes(base)) {
const bestLen = best ? String(best.name).split("(")[0].trim().length : 0;
if (base.length > bestLen)
best = a;
}
}
if (!best) {
throw new Error("RedX: could not match a delivery area from the address. Pass 'delivery_area_id' explicitly (look it up via RedX /areas).");
}
return { id: best.id, name: best.name };
}
async createParcel(req) {
if (!this.enabled) {
throw new Error("RedX Courier credentials (API Token) are not configured.");
throw new Error("RedX credentials (API access token) are not configured.");
}
const area = await this.resolveDeliveryArea(req);
const weightGrams = Math.max(1, Math.round((req.item_weight ?? 0.5) * 1000));
const declaredValue = String(req.value ?? req.cod_amount ?? 0);
const payload = {
customer_name: req.recipient_name,
customer_phone: req.recipient_phone,
delivery_area: req.recipient_address,
delivery_area_id: 1, // Default auto-resolved area
delivery_area: area.name || req.delivery_area || "",
delivery_area_id: area.id,
customer_address: req.recipient_address,
merchant_invoice_id: req.invoice,
cash_collection_amount: req.cod_amount,
parcel_weight: (req.item_weight || 0.5) * 1000, // RedX uses grams
cash_collection_amount: String(req.cod_amount ?? 0),
parcel_weight: weightGrams,
instruction: req.note || "Aura AI automated dispatch",
value: req.cod_amount || 500,
value: declaredValue,
};
const response = await this.client.post("/parcels", payload);
const trackingId = response.data.tracking_id || response.data.parcel_id;
const storeId = req.pickup_store_id || this.pickupStoreId;
if (storeId)
payload.pickup_store_id = Number(storeId);
if (req.item_type) {
payload.parcel_details_json = [
{ name: req.item_type, category: req.item_category || "general", value: Number(req.value ?? req.cod_amount ?? 0) },
];
}
const response = await this.client.post("/parcel", payload);
const data = response.data;
const trackingId = data?.tracking_id;
if (!trackingId) {
throw new Error(`RedX API error: ${JSON.stringify(data?.message || data)}`);
}
return {
success: true,
courier: "redx",
tracking_code: trackingId,
consignment_id: trackingId,
invoice: req.invoice,
status: "ready_for_pickup",
status: "pickup-pending",
cod_amount: req.cod_amount,
delivery_fee: response.data.delivery_fee || 60,
created_at: new Date().toISOString(),
raw_response: response.data,
raw_response: data,
};
}
async trackParcel(trackingCode) {
if (!this.enabled) {
throw new Error("RedX Courier credentials are not configured.");
throw new Error("RedX credentials are not configured.");
}
const response = await this.client.get(`/parcels/track/${encodeURIComponent(trackingCode)}`);
const response = await this.client.get(`/parcel/track/${encodeURIComponent(trackingCode)}`);
const data = response.data;
const events = data?.tracking || [];
const latest = events[events.length - 1];
return {
success: true,
courier: "redx",
tracking_code: trackingCode,
status: data.tracking?.status || "in_transit",
timeline: (data.tracking?.history || []).map((h) => ({
status: h.status,
time: h.created_at,
note: h.message,
})),
status: latest?.message_en || "unknown",
updated_at: latest?.time || new Date().toISOString(),
timeline: events.map((e) => ({ status: e.message_en || "", time: e.time || "", note: e.message_bn })),
raw_response: data,
};
}
async getBalance() {
return {
success: true,
courier: "redx",
current_balance: 0,
raw_response: { message: "RedX payouts are disbursed directly to connected merchant bank accounts." },
};
}
async getLocations(cityName) {
const response = await this.client.get("/areas");
const areas = response.data.areas || [];
return {
success: true,
courier: "redx",
locations: areas.map((a) => ({
city_id: a.id,
city_name: a.name,
zone_name: a.zone_name,
area_name: a.name,
})),
};
throw new Error("RedX does not expose a merchant balance endpoint via its public API — check your balance in the RedX merchant panel.");
}
}
+12 -4
View File
@@ -17,14 +17,22 @@ export interface CourierConfig {
redx: {
apiToken: string;
baseUrl: string;
pickupStoreId: string;
enabled: boolean;
};
paperfly: {
user: string;
pass: string;
key: string;
apiKey: string;
username: string;
password: string;
storeName: string;
baseUrl: string;
enabled: boolean;
};
}
export declare function loadConfig(): CourierConfig;
export type EnvSource = Record<string, string | undefined>;
/**
* 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 declare function loadConfig(src?: EnvSource): CourierConfig;
+25 -22
View File
@@ -1,41 +1,44 @@
import dotenv from "dotenv";
dotenv.config();
export function loadConfig() {
const steadfastApiKey = process.env.STEADFAST_API_KEY || "";
const steadfastSecretKey = process.env.STEADFAST_SECRET_KEY || "";
const pathaoClientId = process.env.PATHAO_CLIENT_ID || "";
const pathaoClientSecret = process.env.PATHAO_CLIENT_SECRET || "";
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 || "";
/**
* 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 = process.env) {
const steadfastApiKey = src.STEADFAST_API_KEY || "";
const steadfastSecretKey = src.STEADFAST_SECRET_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),
apiToken: src.REDX_API_TOKEN || "",
baseUrl: src.REDX_BASE_URL || "https://openapi.redx.com.bd/v1.0.0-beta",
pickupStoreId: src.REDX_PICKUP_STORE_ID || "",
enabled: Boolean(src.REDX_API_TOKEN),
},
paperfly: {
user: paperflyUser,
pass: paperflyPass,
key: paperflyKey,
baseUrl: process.env.PAPERFLY_BASE_URL || "https://api.paperfly.com.bd",
enabled: Boolean(paperflyUser && paperflyPass && paperflyKey),
apiKey: src.PAPERFLY_API_KEY || "",
username: src.PAPERFLY_USERNAME || "",
password: src.PAPERFLY_PASSWORD || "",
storeName: src.PAPERFLY_STORE_NAME || "",
baseUrl: src.PAPERFLY_BASE_URL || "https://api.paperfly.com.bd",
enabled: Boolean(src.PAPERFLY_API_KEY && src.PAPERFLY_STORE_NAME),
},
};
}
+1
View File
@@ -0,0 +1 @@
export {};
+155
View File
@@ -0,0 +1,155 @@
import express 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 } 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) {
const h = req.headers;
const q = req.query;
const pick = (header, query) => {
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 = {};
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"];
let transport;
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) {
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, res) {
const sessionId = req.headers["mcp-session-id"];
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.3.2", sessions: Object.keys(transports).length }));
const PRIVACY_HTML = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Aura Courier MCP — Privacy Policy</title>
<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;max-width:820px;margin:0 auto;padding:40px 20px;line-height:1.7;color:#1a2233;background:#fff}h1{font-size:26px}h2{font-size:18px;margin-top:28px}code{background:#f0f3f9;padding:1px 6px;border-radius:5px}a{color:#2563eb}.muted{color:#667}</style></head><body>
<h1>Aura Courier MCP — Privacy Policy</h1>
<p class="muted">Last updated: 8 September 2026 · Operated by Aura Ajentic AI (auraajenticai.cloud)</p>
<p>Aura Courier MCP is a Model Context Protocol server that lets an AI agent book and track parcels with Bangladeshi couriers (Steadfast, Pathao, RedX, Paperfly) and screen cash-on-delivery fraud risk. This policy explains what data it handles and how.</p>
<h2>1. What we process</h2>
<ul>
<li><b>Your courier credentials</b> (API keys/secrets, merchant username/password) — supplied by you per request as HTTP headers or environment variables.</li>
<li><b>Order details</b> you pass to book or track a parcel — recipient name, phone, address, COD amount, item info.</li>
<li><b>Phone numbers</b> submitted to <code>check_fraud_risk</code> to compute a delivery/return-risk score.</li>
</ul>
<h2>2. How we use it</h2>
<p>Solely to carry out the operation you request — forwarding the booking/tracking/balance call to the specific courier you select, or computing a risk score. We do not use your data for advertising or profiling.</p>
<h2>3. Storage &amp; retention</h2>
<p>Your courier credentials are used only for the duration of the request and are <b>never persisted</b> by Aura. Requests are processed transiently; Aura does not build a long-term store of your merchant keys. Data you send is retained only as long as needed to complete the request.</p>
<h2>4. Third-party sharing</h2>
<p>To fulfil your request, order data is transmitted to the courier provider you choose (Steadfast, Pathao, RedX or Paperfly), each governed by its own privacy terms. We do not sell your data or share it with anyone else.</p>
<h2>5. Security</h2>
<p>All traffic is over HTTPS. Credentials travel as request headers and are held only in memory for the active session.</p>
<h2>6. Your choices</h2>
<p>You control which couriers you connect and which credentials you provide. Disconnecting the connector stops all processing.</p>
<h2>7. Contact</h2>
<p>Questions or requests: <a href="mailto:ceo@auraajenticai.cloud">ceo@auraajenticai.cloud</a> · <a href="https://auraajenticai.cloud">auraajenticai.cloud</a></p>
</body></html>`;
app.get("/privacy", (_req, res) => res.type("html").send(PRIVACY_HTML));
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`);
});
+5 -150
View File
@@ -1,159 +1,14 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { CourierRegistry } from "./registry.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 = [
{
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,
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);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "get_balance": {
const result = await registry.getBalance(String(args?.courier));
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) {
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) => {
console.error("Fatal error running Aura Courier MCP:", error);
+2 -1
View File
@@ -1,8 +1,9 @@
import { CourierAdapter } from "./adapters/base.js";
import { CourierConfig } from "./config.js";
import { BalanceResponse, FraudRiskScoreResponse, ParcelCreateRequest, ParcelResponse, SupportedCourier, TrackingResponse } from "./types.js";
export declare class CourierRegistry {
private adapters;
constructor();
constructor(config?: CourierConfig);
listCouriers(): {
courier: SupportedCourier;
is_configured: boolean;
+10 -41
View File
@@ -1,20 +1,19 @@
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 { RedxAdapter } from "./adapters/redx.js";
import { PaperflyAdapter } from "./adapters/paperfly.js";
import { loadConfig } from "./config.js";
export class CourierRegistry {
adapters = new Map();
constructor() {
const config = loadConfig();
constructor(config = loadConfig()) {
const steadfast = new SteadfastAdapter(config.steadfast.apiKey, config.steadfast.secretKey, config.steadfast.baseUrl);
this.adapters.set("steadfast", steadfast);
const pathao = new PathaoAdapter(config.pathao.clientId, config.pathao.clientSecret, config.pathao.username, config.pathao.password, config.pathao.storeId, config.pathao.baseUrl);
this.adapters.set("pathao", pathao);
const redx = new RedXAdapter(config.redx.apiToken, config.redx.baseUrl);
const redx = new RedxAdapter(config.redx.apiToken, config.redx.baseUrl, config.redx.pickupStoreId);
this.adapters.set("redx", redx);
const paperfly = new PaperflyAdapter(config.paperfly.user, config.paperfly.pass, config.paperfly.key, config.paperfly.baseUrl);
const paperfly = new PaperflyAdapter(config.paperfly.apiKey, config.paperfly.username, config.paperfly.password, config.paperfly.storeName, config.paperfly.baseUrl);
this.adapters.set("paperfly", paperfly);
}
listCouriers() {
@@ -36,7 +35,7 @@ export class CourierRegistry {
courierName = req.courier;
}
else {
// Smart routing heuristic across all 4 couriers
// Smart routing heuristic
const addr = req.recipient_address.toLowerCase();
if (addr.includes("dhaka") &&
(addr.includes("gulshan") ||
@@ -44,26 +43,10 @@ export class CourierRegistry {
addr.includes("dhanmondi") ||
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";
}
}
const adapter = this.getAdapter(courierName);
@@ -73,26 +56,12 @@ export class CourierRegistry {
if (courierName) {
return await this.getAdapter(courierName).trackParcel(trackingCode);
}
// Smart fallback across available configured adapters
const order = ["steadfast", "pathao", "redx", "paperfly"];
let lastError = 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);
}
}
async getBalance(courierName) {
+10
View File
@@ -0,0 +1,10 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { CourierRegistry } from "./registry.js";
export declare const TOOLS: Tool[];
/**
* 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 declare function buildMcpServer(registry: CourierRegistry): Server;
+123
View File
@@ -0,0 +1,123 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
// Tool definitions — shared by the STDIO (npx) and HTTP (web-URL) entrypoints.
export const TOOLS = [
{
name: "list_couriers",
annotations: { title: "List couriers", readOnlyHint: true, destructiveHint: false, openWorldHint: true },
description: "Show supported Bangladeshi couriers and check which credentials are active.",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
name: "create_parcel",
annotations: { title: "Create parcel (book delivery)", readOnlyHint: false, destructiveHint: false, openWorldHint: true },
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)" },
item_type: { type: "string", description: "What's inside the parcel (used by RedX)" },
value: { type: "number", description: "Declared parcel value in BDT (used by RedX; defaults to the COD amount)" },
delivery_area_id: { type: "number", description: "RedX only: numeric delivery-area id (auto-resolved from the address if omitted)" },
pickup_store_id: { type: "number", description: "RedX only: your pickup store id (optional)" },
},
required: ["invoice", "recipient_name", "recipient_phone", "recipient_address", "cod_amount"],
},
},
{
name: "track_parcel",
annotations: { title: "Track parcel", readOnlyHint: true, destructiveHint: false, openWorldHint: true },
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", "redx", "paperfly"], description: "Optional courier name if known" },
},
required: ["tracking_code"],
},
},
{
name: "get_balance",
annotations: { title: "Get merchant balance", readOnlyHint: true, destructiveHint: false, openWorldHint: true },
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",
annotations: { title: "Check COD fraud risk", readOnlyHint: true, destructiveHint: false, openWorldHint: true },
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) {
const server = new Server({ name: "aura-courier-mcp", version: "2.3.2" }, { 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,
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);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
case "get_balance": {
const result = await registry.getBalance(String(args?.courier));
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) {
return {
isError: true,
content: [{ type: "text", text: `Aura Courier MCP Error: ${error?.message || String(error)}` }],
};
}
});
return server;
}
+15 -4
View File
@@ -9,6 +9,11 @@ export interface ParcelCreateRequest {
note?: string;
item_type?: string;
item_weight?: number;
item_category?: string;
value?: number | string;
delivery_area?: string;
delivery_area_id?: number | string;
pickup_store_id?: number | string;
}
export interface ParcelResponse {
success: boolean;
@@ -50,15 +55,21 @@ export interface FraudRiskScoreResponse {
recommendation: string;
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;
}>;
}