Skip to main content
Glama
README.md
<div align="center">

<img src="assets/banner.svg" alt="alopeyk-mcp" width="100%" />

<h1>
  alopeyk-mcp
</h1>

**A self-hostable [Model Context Protocol](https://modelcontextprotocol.io) server for [Alopeyk](https://alopeyk.com) (الوپیک) — an Iranian on-demand delivery & logistics company.**

[![CI](https://github.com/alopeyk-mcp/alopeyk-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/alopeyk-mcp/alopeyk-mcp/actions/workflows/ci.yml)
[![Docker](https://github.com/alopeyk-mcp/alopeyk-mcp/actions/workflows/docker.yml/badge.svg)](https://github.com/alopeyk-mcp/alopeyk-mcp/actions/workflows/docker.yml)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

</div>

---

> **Unofficial integration.** This is a community-built MCP server. The
> "Alopeyk" and "Alonomic" names and logos belong to their owner. Not affiliated
> with, endorsed by, or sponsored by Alopeyk. Creating real deliveries/parcels
> incurs real charges — use the order tools at your own risk.


`alopeyk-mcp` exposes Alopeyk's APIs as MCP **tools** and **prompts** so an LLM
agent (Claude, etc.) can geocode addresses, quote and track deliveries, manage
business parcels, and — when you explicitly allow it — create and cancel real
deliveries/parcels. It is a small, stateless, async Python process you run
yourself.

It mirrors the structure and quality of the sibling `ramzinex-mcp` / `roshan-*`
servers: typed `httpx` client, `pydantic-settings` configuration, full test
suite, Docker / Helm / Kubernetes / Terraform deploy assets, and generated
architecture diagrams.

## Two services in one server

Alopeyk runs **two distinct API services**, and this server wraps both behind one
process, routing each call to the right base automatically:

| Service | Base URL | Auth | Tool prefix |
| --- | --- | --- | --- |
| **On-Demand** | `https://api.alopeyk.com/api/v2/` (sandbox: `https://sandbox-api.alopeyk.com/api/v2/`) | `Authorization: Bearer <access_token>` (pre-issued JWT) | `alopeyk_` |
| **Alonomic** (الونومیک) | `https://api.alopeyk.com/business-service/api/v1/` | email/password → token (cached, re-login on 401), or pre-issued `alonomic_token` | `alonomic_` |

> ### 🔐 Required header
> **Every** call to **both** services carries:
> ```
> X-Requested-With: XMLHttpRequest
> Content-Type: application/json; charset=utf-8
> ```
> The server adds these automatically.

> ### ⚠️ Ordering safety
> Creating, updating, cancelling, or finishing **real** deliveries/parcels (and
> buying loyalty products) is gated by a single per-instance flag, default
> **`false`**:
> - **`enable_ordering`** gates `alopeyk_create_order`, `alopeyk_cancel_order`,
>   `alopeyk_finish_order`, `alopeyk_loyalty_products(buy=true)`,
>   `alonomic_create_parcel`, `alonomic_update_parcel`, and
>   `alonomic_cancel_parcel`.
>
> A master **`read_only`** flag forces ordering off regardless of its value. When
> the gate is off the tool returns a structured refusal and **never calls the
> API**:
> ```json
> {"error": "ordering_disabled", "message": "Set enable_ordering=true (and read_only=false) to create/modify real deliveries/parcels."}
> ```
> Read-only tools (geocoding, pricing, viewing orders/parcels, days, sizes,
> address book) are always allowed. The access token, the Alonomic login token,
> and the password are never logged and are redacted from every error message.


> Production vs. sandbox — selected per instance by `environment`:

![environments](assets/diagrams/environments.png)

## Install

```bash
pip install -e .            # from a checkout
# or, for development:
pip install -e ".[dev]"
```

Requires Python 3.11+.

## Quick start

The offline/static tools (docs, transport types, cities, URL helpers, parcel
statuses) work with zero configuration:

```bash
python -m alopeyk_mcp          # stdio transport (default)
```

To use On-Demand tools, provide the pre-issued JWT; to use Alonomic tools,
provide an email + password (or a pre-issued `alonomic_token`):

```bash
export ALOPEYK_ENVIRONMENT=production              # or sandbox
# On-Demand (/api/v2/):
export ALOPEYK_ACCESS_TOKEN=your-ondemand-jwt
# Alonomic (/business-service/): email + password login (cached, re-login on 401)
export ALOPEYK_EMAIL=ops@example.com
export ALOPEYK_PASSWORD=your-password
# ...or a pre-issued Alonomic token instead:
# export ALOPEYK_ALONOMIC_TOKEN=your-alonomic-token

# Opt in to the gated mutation tools (default false):
export ALOPEYK_ENABLE_ORDERING=true                # allow create/cancel/finish + parcels
python -m alopeyk_mcp
```

Run over HTTP for networked clients:

```bash
python -m alopeyk_mcp --transport streamable-http --host 0.0.0.0 --port 8000
```


> Order lifecycle — creating/cancelling is gated by `enable_ordering`:

![order-lifecycle](assets/diagrams/order-lifecycle.png)

## Configuration (multi-account / multi-instance)

Configuration is read from environment variables via `pydantic-settings`. One
process can front **many Alopeyk accounts/environments** — each is a named
*instance* with its own credentials and safety policy. Every API tool accepts an
optional `instance` argument; omit it to use the default.

### Shorthand (single default instance)

| Variable | Default | Description |
| --- | --- | --- |
| `ALOPEYK_ENVIRONMENT` | `production` | `production` or `sandbox` (selects default base/tracking URLs). |
| `ALOPEYK_ACCESS_TOKEN` | — | On-Demand JWT (`Authorization: Bearer`). |
| `ALOPEYK_EMAIL` | — | Alonomic login email (with `ALOPEYK_PASSWORD`). |
| `ALOPEYK_PASSWORD` | — | Alonomic login password. |
| `ALOPEYK_ALONOMIC_TOKEN` | — | Pre-issued Alonomic token (wins over email/password). |
| `ALOPEYK_ENABLE_ORDERING` | `false` | Allow creating/modifying real deliveries/parcels. |
| `ALOPEYK_READ_ONLY` | `false` | Master switch: forces ordering off. |
| `ALOPEYK_ONDEMAND_BASE_URL` | derived | Override the On-Demand base URL. |
| `ALOPEYK_BUSINESS_BASE_URL` | derived | Override the Alonomic base URL. |
| `ALOPEYK_TRACKING_URL` | derived | Override the tracking web-app base URL. |
| `ALOPEYK_VERIFY_SSL` | `true` | Verify TLS certificates. |
| `ALOPEYK_TIMEOUT` | `30` | Per-request timeout (seconds). |
| `ALOPEYK_DEFAULT_INSTANCE` | `default` | Instance used when `instance` is omitted. |
| `ALOPEYK_LOG_LEVEL` | `INFO` | `DEBUG` / `INFO` / `WARNING` / ... |

### Nested (named instances)

| Variable | Description |
| --- | --- |
| `ALOPEYK__INSTANCES__<NAME>__ENVIRONMENT` | `production` / `sandbox` for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__ACCESS_TOKEN` | On-Demand JWT for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__EMAIL` | Alonomic login email for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__PASSWORD` | Alonomic login password for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__ALONOMIC_TOKEN` | Pre-issued Alonomic token for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__ENABLE_ORDERING` | Ordering switch for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__READ_ONLY` | Master switch (forces ordering off) for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__ONDEMAND_BASE_URL` | On-Demand base for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__BUSINESS_BASE_URL` | Alonomic base for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__TRACKING_URL` | Tracking base for `<NAME>`. |
| `ALOPEYK__INSTANCES__<NAME>__VERIFY_SSL` | TLS verification (default `true`). |
| `ALOPEYK__INSTANCES__<NAME>__TIMEOUT` | Per-request timeout seconds (default `30`). |
| `ALOPEYK__DEFAULT_INSTANCE` | Instance used when `instance` is omitted. |
| `ALOPEYK__LOG_LEVEL` | `DEBUG` / `INFO` / `WARNING` / ... |

Example: a production business account, a sandbox account, and a strictly
read-only viewer.

```bash
ALOPEYK__INSTANCES__BIZ__ENVIRONMENT=production
ALOPEYK__INSTANCES__BIZ__ACCESS_TOKEN=prod-ondemand-jwt
ALOPEYK__INSTANCES__BIZ__EMAIL=ops@example.com
ALOPEYK__INSTANCES__BIZ__PASSWORD=secret
ALOPEYK__INSTANCES__BIZ__ENABLE_ORDERING=true
ALOPEYK__INSTANCES__SANDBOX__ENVIRONMENT=sandbox
ALOPEYK__INSTANCES__SANDBOX__ACCESS_TOKEN=sandbox-jwt
ALOPEYK__INSTANCES__SANDBOX__ENABLE_ORDERING=true
ALOPEYK__INSTANCES__READONLY__ACCESS_TOKEN=viewer-jwt
ALOPEYK__INSTANCES__READONLY__READ_ONLY=true
ALOPEYK__DEFAULT_INSTANCE=biz
```

See [`.env.example`](.env.example) for a complete, annotated file. Use
`list_instances` to see what's configured (names, environment, base URLs,
`has_ondemand_credentials`, `has_alonomic_credentials`, `alonomic_auth_method`,
`enable_ordering`, `read_only`, `ordering_allowed`) — it never reveals credential
values.

## Control flags & safety

| Flag | Default | Gates | Effective when |
| --- | --- | --- | --- |
| `enable_ordering` | `false` | `alopeyk_create_order`, `alopeyk_cancel_order`, `alopeyk_finish_order`, `alopeyk_loyalty_products(buy=true)`, `alonomic_create_parcel`, `alonomic_update_parcel`, `alonomic_cancel_parcel` | `enable_ordering=true` **and** `read_only=false` |
| `read_only` | `false` | forces `enable_ordering` off | — |

When the gate is off, the tool returns
`{"error": "ordering_disabled", "message": ...}` and **never contacts the API**.

### Authentication

- **On-Demand** — a pre-issued `access_token` (JWT), sent as
  `Authorization: Bearer <token>`. Required for all `alopeyk_*` network tools.
- **Alonomic** — either a pre-issued `alonomic_token`, or `email` + `password`
  which the server POSTs to `login`, caching the returned token in memory per
  instance, reusing it, and re-authenticating once on a `401`. Call
  `alonomic_login` to trigger/verify the login explicitly (it never returns the
  token).

The access token, the Alonomic token, and the password are never logged or
echoed (`Authorization` / `access_token` / `alonomic_token` / `token` /
`password` are all redacted from errors).


> The `read_only` master switch overrides the `enable_ordering` gate:

![safety-gates](assets/diagrams/safety-gates.png)

## Use with an MCP client

Add it to your client's MCP server config (stdio):

```json
{
  "mcpServers": {
    "alopeyk": {
      "command": "python",
      "args": ["-m", "alopeyk_mcp"],
      "env": {
        "ALOPEYK_ENVIRONMENT": "production",
        "ALOPEYK_ACCESS_TOKEN": "your-ondemand-jwt",
        "ALOPEYK_EMAIL": "ops@example.com",
        "ALOPEYK_PASSWORD": "your-password",
        "ALOPEYK_ENABLE_ORDERING": "false"
      }
    }
  }
}
```

## Tools

All 34 tools (17 On-Demand `alopeyk_*` + 14 Alonomic `alonomic_*` + 3 meta)
accept an optional `instance` except the two purely-local meta tools
(`list_instances`, `alopeyk_docs`).

### On-Demand (`alopeyk_`) — `/api/v2/`, JWT Bearer

| Tool | Method · Endpoint | Notes |
| --- | --- | --- |
| `alopeyk_get_address(lat, lng)` | `GET locations?latlng=<lat>,<lng>` | reverse geocode |
| `alopeyk_location_suggestion(input, latlng?)` | `GET locations?input=&location=` | autocomplete |
| `alopeyk_get_price(transport_type, addresses, has_return?, cashed?, optimize?)` | `POST orders/price/calc` | read-only quote |
| `alopeyk_get_batch_price(orders)` | `POST orders/batch-price` | ≤15 orders |
| `alopeyk_create_order(transport_type, addresses, has_return?, cashed?, scheduled_at?, discount_coupon?)` | `POST orders` | ⚠️ `enable_ordering` |
| `alopeyk_get_order(order_id)` | `GET orders/{id}?columns=...` | read-only |
| `alopeyk_cancel_order(order_id, comment)` | `GET orders/{id}/cancel?comment=` | ⚠️ `enable_ordering` |
| `alopeyk_finish_order(order_id, comment?, rate?)` | `POST orders/{id}/finish` | ⚠️ `enable_ordering` |
| `alopeyk_get_config()` | `GET config` | read-only |
| `alopeyk_get_profile()` | `GET show-profile?columns=*,credit` | read-only |
| `alopeyk_validate_coupon(code)` | `POST coupons` | read-only |
| `alopeyk_loyalty_products(product_id?, buy?)` | `GET\|POST loyalty/customer/products/{id?}` | buy ⚠️ `enable_ordering` |
| `alopeyk_tracking_url(order_token)` | _local_ `<tracking>#/<token>` | no network |
| `alopeyk_print_invoice_url(order_id, order_token)` | _local_ | no network |
| `alopeyk_payment_route(user_id, amount, gateway=saman)` | _local_ (saman\|zarinpal) | no network |
| `alopeyk_transport_types()` | _local_ | motorbike, motor_taxi, cargo, cargo_s, car |
| `alopeyk_cities()` | _local_ | tehran, shemiranat, rey, karaj, isfahan, tabriz, mashhad, shiraz |

### Alonomic (`alonomic_`) — `/business-service/api/v1/`, email/password login

| Tool | Method · Endpoint | Notes |
| --- | --- | --- |
| `alonomic_login()` | `POST login` | caches token (never returns it) |
| `alonomic_get_me()` | `GET me` | read-only |
| `alonomic_get_days()` | `GET days` | read-only |
| `alonomic_get_days_index()` | `GET days/index` | 7 days, each `{same, next}` |
| `alonomic_create_parcel(drop, parcel, pickup, delivery_proofs?, parcel_extra_params?)` | `POST parcels` | ⚠️ `enable_ordering` |
| `alonomic_get_parcel(parcel_id)` | `GET parcels/{id}` | read-only |
| `alonomic_update_parcel(parcel_id, ...)` | `PUT parcels/{id}` | ⚠️ `enable_ordering` |
| `alonomic_cancel_parcel(parcel_id)` | `DELETE parcels/{id}` | ⚠️ `enable_ordering` (204) |
| `alonomic_calc_parcel(drop, parcel, pickup?)` | `POST parcels/calc` | read-only estimate |
| `alonomic_get_parcel_sizes()` | `GET parcels/size` | 12 box sizes |
| `alonomic_get_saved_addresses()` | `GET pickup-saved-addresses` | read-only |
| `alonomic_add_saved_address(title, lat, lng, address, ...)` | `POST pickup-saved-addresses` | address book |
| `alonomic_delete_saved_address(address_id)` | `DELETE pickup-saved-addresses/{id}` | address book (204) |
| `alonomic_parcel_statuses()` | _local_ | static status → Persian label map |

### Meta

| Tool | Description |
| --- | --- |
| `healthcheck` | Calls the On-Demand `config` endpoint and reports reachability. |
| `list_instances` | Lists configured instances (environment, base URLs, which creds set, `enable_ordering`, `read_only`, `ordering_allowed`) — never credential values. |
| `alopeyk_docs(topic?)` | Offline reference for both services + links to https://docs.alopeyk.com. |

⚠️ = creates/modifies a real delivery/parcel; gated by `enable_ordering` (forced
off by `read_only`).


> All 34 tools at a glance, grouped by service:

![endpoint-map](assets/diagrams/endpoint-map.png)

## Prompts

The server also ships MCP **prompts** — safety-aware workflows your client can
list and run (details in [`prompts/README.md`](prompts/README.md)):

- **`quote_delivery`** — geocode origin/destination, then quote with
  `alopeyk_get_price`.
- **`create_delivery_safely`** — a careful checklist that verifies
  `enable_ordering`, confirms addresses/type/price, then creates the order.
- **`track_order`** — `alopeyk_get_order` + `alopeyk_tracking_url`.
- **`alonomic_send_parcel`** — login → `days/index` → `calc` → create the parcel
  safely.
- **`address_book`** — review Alonomic saved addresses + parcel box sizes.

## Skill

A ready-to-use Claude/agent skill lives at
[`skills/alopeyk/SKILL.md`](skills/alopeyk/SKILL.md). It describes when and how to
use these tools across both services — quoting/tracking, the safe delivery flow,
and the Alonomic parcel flow — with example tool sequences.

## Architecture

The MCP client talks to one `alopeyk-mcp` process, which routes `alopeyk_*` calls
to the On-Demand API (`/api/v2/`) and `alonomic_*` calls to the Alonomic business
API (`/business-service/api/v1/`).

![architecture](assets/diagrams/architecture.png)

One process can serve many accounts/environments; the `instance` argument selects
which credentials / ordering policy to use.

![self-hosting](assets/diagrams/self-hosting-multi-instance.png)

A typical On-Demand flow: suggest/geocode an address, quote the price, check the
`enable_ordering` gate, then create and track the order.

![request-flow](assets/diagrams/request-flow.png)

The full tool surface, grouped by service:
The order lifecycle and the Alonomic parcel flow:
![alonomic-parcel-flow](assets/diagrams/alonomic-parcel-flow.png)

The `read_only` master switch over `enable_ordering`, and the two environments:
Regenerate the diagrams with `make diagrams` (uses the `diagrams` package +
Graphviz `dot` and `cairosvg`).

## Self-hosting & scaling

`alopeyk-mcp` is stateless, so you can run as many replicas as you like behind a
load balancer. One process fronts multiple accounts via
`ALOPEYK__INSTANCES__<NAME>__*` — no code change. Back off on HTTP 429 if Alopeyk
rate-limits you. See [`deploy/`](deploy/) for Docker Compose, Kubernetes (raw +
Kustomize), a Helm chart, and a Terraform module.

## Testing

```bash
make smoke          # no-network smoke test (tools + prompts + descriptions)
make test           # full pytest suite (offline; all HTTP incl. Alonomic login mocked with respx)
make lint           # ruff
```

Live tests against the real API are skipped unless `ALOPEYK_LIVE=1` is set (and
the relevant credentials provided):

```bash
ALOPEYK_LIVE=1 ALOPEYK_ACCESS_TOKEN=... pytest tests/live -q
```

## License

[MIT](LICENSE). `alopeyk-mcp` is an unofficial, community-built integration; the
Alopeyk and Alonomic names and logos belong to their owner.

TDQS

A3.9/5.0

Scored across 34 tools

Disambiguation5/5

Tools are clearly separated by prefix (alopeyk_ vs alonomic_) and each has a distinct purpose. Even within groups, tools like alopeyk_get_price and alopeyk_get_batch_price are well-differentiated. Overlap is minimal.

Naming Consistency4/5

The majority follow a verb_noun pattern with consistent prefixes. However, a few tools (healthcheck, list_instances, alopeyk_payment_route, alopeyk_print_invoice_url) break the pattern, causing minor inconsistency.

Tool Count4/5

34 tools is on the higher side but reasonable for a server covering two distinct service APIs with full CRUD and utility operations. Each tool earns its place without unnecessary duplication.

Completeness3/5

The tool set covers core operations for both Alopeyk and Alonomic services, but lacks listing tools for orders and parcels. Agents cannot retrieve a list of existing orders or parcels, which is a notable gap.

Maintenance

ActivityInactive
ResponsivenessNo issues