Skip to main content
Glama
Muhammadzainattiq

PostEx MCP Server

README.md
# PostEx MCP Server

A remote [MCP](https://modelcontextprotocol.io/) server that exposes the PostEx
merchant COD API (Integration Guide v4.1.9) as 16 tools, built with
[FastMCP](https://gofastmcp.com/) and deployable to Vercel as a FastAPI function.

Once connected, Claude can book shipments, track parcels, pull load sheets and
airway bills, and check settlement status.

It is **bring-your-own-token**: the server holds no credentials, so a single
deployment serves any number of merchants. Each user connects with their own
PostEx token, which is forwarded to PostEx and never stored.

## Tools

| Tool | PostEx endpoint | What it does |
| --- | --- | --- |
| `get_operational_cities` | `v2/get-operational-city` | Cities PostEx serves, with pickup/delivery flags |
| `get_pickup_addresses` | `v1/get-merchant-address` | Your registered warehouse addresses |
| `create_pickup_address` | `v2/create-merchant-address` | Register a pickup or return address |
| `get_order_types` | `v1/get-order-types` | Normal / Reversed / Replacement |
| `create_order` | `v3/create-order` | Book a COD order, returns a tracking number |
| `list_unbooked_orders` | `v2/get-unbooked-orders` | Orders created but not yet handed over |
| `generate_load_sheet` | `v2/generate-load-sheet` | Load sheet PDF (base64) |
| `track_order` | `v1/track-order/{tn}` | Status and full journey for one parcel |
| `track_bulk_orders` | `v1/track-bulk-order` | Status for many parcels at once |
| `get_airway_bill` | `v1/get-invoice` | Shipping label PDF, max 10 parcels (base64) |
| `save_shipper_advice` | `v2/save-shipper-advice` | Request return or retry on an attempted parcel |
| `get_shipper_advice` | `v1/get-shipper-advice/{tn}` | Read saved shipper advice remarks |
| `cancel_order` | `v1/cancel-order` | Cancel an order |
| `get_payment_status` | `v1/payment-status/{tn}` | Settlement, CPR numbers, payment dates |
| `get_order_statuses` | `v1/get-order-status` | The status vocabulary PostEx uses |
| `list_orders` | `v1/get-all-order` | Orders in a date range, filtered by status |

## How authentication works

This server is **bring-your-own-token**. It stores no PostEx credentials. Every
user connects with their own PostEx merchant token, the server forwards it to
PostEx unchanged on each call, and it is never cached, persisted, or logged.

That means one deployment serves everyone: you host it once, and any PostEx
merchant can point their MCP client at it using their own account.

Send the token in whichever form your client supports — they are checked in
this order:

| Form | Use when |
| --- | --- |
| `Authorization: Bearer <token>` | Preferred. Claude Code, and anything that lets you set headers |
| `token: <token>` | Matches PostEx's own header name |
| `x-postex-token: <token>` | Avoids collisions if a proxy already uses `token` |
| `x-api-key: <token>` | Convenience alias |
| `?token=<token>` | Last resort, for connector UIs that accept only a URL |

Listing tools works without a token, so clients can connect and discover the
tools before a token is supplied. Calling any tool without one returns a message
explaining how to provide it.

**Prefer a header over `?token=`.** Query strings get written to server logs,
proxy logs, and analytics; headers generally do not.

### Getting a PostEx token

The v4.1.9 integration guide describes the token as "a unique token specific to
identify the merchant" but does not document where to obtain one. Check your
PostEx merchant account for an API or integration section; if it is not there,
the guide's contacts are **support@postex.pk** and Babar Razzaq (CTO / Head of
Integration Services), **babar@postex.pk**.

## Run it yourself locally

Anyone can run their own copy — no account on someone else's deployment
required. You bring your own PostEx token; the server holds none.

**1. Get the code and install dependencies.**

```bash
git clone <this-repo-url> postex-mcp   # or download and unzip it
cd postex-mcp

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
```

**2. Start the server.**

```bash
uvicorn app:app --reload --port 8000
```

No configuration is required. `.env.example` documents the optional
`POSTEX_API_TOKEN` fallback, which is only for a **private, single-merchant**
setup — set it and any token-less request uses it. Leave it unset otherwise.

The server is now at `http://127.0.0.1:8000`:

- `GET /` — deployment info and tool list
- `GET /health` — health check
- `POST /mcp` — the MCP endpoint (Streamable HTTP)

**3. Confirm it works** (in a second terminal, venv activated):

```bash
python smoke_test.py http://127.0.0.1:8000/mcp <your-postex-token> --call
```

This lists the 16 tools and runs one read-only call against PostEx with your
token. Then connect Claude to it — see
[Connect it to Claude](#connect-it-to-claude) below and use the local URL.

If you would rather host it once for others instead of everyone running their
own copy, see [Deploy to Vercel](#deploy-to-vercel).

## Deploy to Vercel

```bash
npm i -g vercel        # if needed; CLI 48.1.8 or newer
vercel login
vercel                 # preview deploy, links the project
vercel --prod
```

Vercel's Python runtime picks up the `FastAPI` instance named `app` in `app.py`
automatically. There are no environment variables to set. Your MCP endpoint is:

```
https://<your-project>.vercel.app/mcp
```

The server runs stateless with JSON responses, which is what serverless needs —
each invocation is a fresh process with no session to resume.

## Connect it to Claude

Everyone substitutes **their own PostEx token** below, and their own server URL:

- Running locally? Use `http://127.0.0.1:8000/mcp`.
- Using a hosted deployment? Use `https://<the-project>.vercel.app/mcp`.

### Claude Code

```bash
claude mcp add --transport http postex \
  http://127.0.0.1:8000/mcp \
  --header "Authorization: Bearer <your-postex-token>"
```

Swap in the Vercel URL instead of `127.0.0.1:8000` if you are pointing at a
hosted copy. Then `/mcp` inside Claude Code to confirm it connected.

### Claude desktop / claude.ai (custom connector)

Settings → Connectors → Add custom connector. That UI has no field for custom
headers, so the token goes in the URL. This needs a **public** URL, so it works
against a hosted deployment, not `127.0.0.1`:

```
https://<the-project>.vercel.app/mcp?token=<your-postex-token>
```

To use a locally running server here, expose it with a tunnel (e.g.
`ngrok http 8000`) and use the tunnel's public URL. Prefer the header-based
Claude Code path above when you can — query-string tokens land in logs.

### Any stdio-only host

```json
{
  "mcpServers": {
    "postex": {
      "command": "uvx",
      "args": [
        "fastmcp-remote",
        "https://<your-project>.vercel.app/mcp",
        "--header",
        "Authorization: Bearer <your-postex-token>"
      ]
    }
  }
}
```

## Corrections to the integration guide

The PostEx API disagrees with the v4.1.9 PDF in four places. This server sends
what the API actually accepts, verified against the live endpoints:

| Guide says | API actually wants |
| --- | --- |
| `operationalCityType=Pickup` / `Delivery` | lowercase `pickup` / `delivery`; capitalised values 400 |
| Shipper advice under `/service/integration/...` | `/services/integration/...`; the singular form 404s |
| `track-bulk-order` takes a JSON body `{"trackingNumber": [...]}` | a comma-separated `TrackingNumbers` **query** parameter |
| `get-all-order` takes `fromDate`, `toDate`, `orderStatusID` | `startDate`, `endDate`, `orderStatusId` |

Tool arguments keep the friendlier documented names (`from_date`, `to_date`,
`order_status_id`); the translation happens inside the server.

## Operating a public deployment

Two things follow from having no gate on the endpoint:

**Users are trusting you with their token.** Every request routes their PostEx
credential through your infrastructure. This server does not log it, but anyone
who controls the deployment could. If you publish this for others, say so
plainly — and if you would rather not hold that trust, tell users to deploy
their own copy and set `POSTEX_API_TOKEN` privately.

**Anyone can invoke your function.** A caller without a valid token cannot touch
any PostEx account, but they can still burn your Vercel invocations. If that
becomes a problem, add a rate-limiting rule in Vercel's firewall
(`vercel firewall`) rather than reintroducing a shared secret.

## Safety note

`create_order`, `cancel_order`, and `save_shipper_advice` change real shipments
and real money on the caller's PostEx account. Their descriptions tell Claude to
confirm first, but that is a prompt-level guardrail, not an enforced one —
review what you approve.