parcelapp-mcp
An MCP server for Parcel, the macOS and iOS delivery tracking app. It lets a model read your deliveries, add new ones, and look up the carrier codes the API needs, over the small external API that Parcel premium accounts get.
The server is deliberately thin. The upstream API is tiny and heavily rate limited, so the value added here is not abstraction, it is legibility: carrier codes resolved to names, numeric status codes resolved to text, required fields validated locally before a request is spent, and every result carrying how much of the rate-limit budget is left.
It runs two ways. Over stdio it is a personal server reading your key from the environment. Over HTTP the key travels with each request, so one deployment serves several people without ever holding anyone's credential.
✨ Features
| Feature | Why it matters | |
|---|---|---|
| 📦 | Lists recent or active deliveries | Status codes and carrier codes come back as words, not integers |
| ➕ | Adds a delivery | Validated locally first, so a typo never costs one of your 20 daily requests |
| 🔍 | Searches the carrier catalogue | Finds the internal code, and says whether it needs a postcode or an email |
| 🎚️ | Filters and sorts server-side | Narrowing happens after the fetch, so it costs no extra request |
| 📊 | Reports the rate-limit budget | Every result says what has been spent, so a model can pace itself |
| 🔑 | Per-request keys over HTTP | One container serves several people; it stores no credential |
| ⏱️ | Caches locally | 3 minutes for deliveries, 24 hours for carriers, partitioned per key |
| 💬 | Errors that name the fix | "Bpost requires a postcode; pass postcode" rather than "invalid request" |
🚀 Quickstart
You need a Parcel premium account and an API key from web.parcelapp.net.
export PARCEL_TOKEN="your-key"
uvx --from git+https://github.com/obeone/parcelapp-mcp parcelapp-mcp
That runs the server on stdio. In practice you point an MCP client at it rather than running it by hand: see Configuration.
📦 Installation
With uvx, no checkout
uvx --from git+https://github.com/obeone/parcelapp-mcp parcelapp-mcp
From a local clone
git clone https://github.com/obeone/parcelapp-mcp
cd parcelapp-mcp
uv sync
uv run parcelapp-mcp
With Docker, for the HTTP transport
The image holds no key. Every caller sends their own, so the container is safe to share.
docker build -t parcelapp-mcp --build-arg VERSION="$(uv version --short)" .
docker run -d --name parcel -p 8000:8000 parcelapp-mcp
VERSION only stamps the OCI label; the build works without it.
curl -sS -X POST http://127.0.0.1:8000/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'X-Parcel-Token: your-key' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
127.0.0.1 rather than localhost on purpose: localhost resolves to ::1
first on macOS, and -p 8000:8000 publishes on IPv4 only, so the connection is
refused for a reason that has nothing to do with this server.
⚙️ Configuration
Environment
| Variable | Required | Purpose |
|---|---|---|
PARCEL_TOKEN |
on stdio | API key from web.parcelapp.net, sent upstream as the api-key header |
PARCEL_API_KEY |
no | Accepted as a fallback if PARCEL_TOKEN is unset |
PARCEL_TRANSPORT |
no | stdio (default) or streamable-http |
PARCEL_HOST |
no | Bind address for HTTP. Default 127.0.0.1, 0.0.0.0 in the image |
PARCEL_PORT |
no | Port for HTTP. Default 8000 |
PARCEL_PATH |
no | URL path for HTTP. Default /mcp |
PARCEL_LOG_LEVEL |
no | Python log level. Default INFO |
Every variable has a command-line equivalent: --transport, --host, --port,
--path.
Sending the key over HTTP
Two headers are accepted, checked in this order:
X-Parcel-Token: your-key
Authorization: Bearer your-key
If neither is present the server falls back to its own environment, which is what makes the same code work on stdio. When nothing supplies a key, the error names both routes rather than saying "unauthorised".
Claude Code
claude mcp add parcel -e PARCEL_TOKEN=your-key -- \
uvx --from git+https://github.com/obeone/parcelapp-mcp parcelapp-mcp
Claude Desktop
In claude_desktop_config.json:
{
"mcpServers": {
"parcel": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/obeone/parcelapp-mcp",
"parcelapp-mcp"
],
"env": { "PARCEL_TOKEN": "your-key" }
}
}
}
Keeping the key out of a config file
On macOS, envchain stores it in the Keychain and injects it only into the wrapped command:
envchain --set parcel PARCEL_TOKEN
{
"mcpServers": {
"parcel": {
"command": "envchain",
"args": [
"parcel",
"uvx",
"--from",
"git+https://github.com/obeone/parcelapp-mcp",
"parcelapp-mcp"
]
}
}
}
🧰 Tools
| Tool | Arguments | Upstream rate limit |
|---|---|---|
list_deliveries |
filter_mode, status, sort_by |
20 per hour |
add_delivery |
tracking_number, carrier_code, description, plus optional language, send_push_confirmation, postcode, email |
20 per day, failed attempts included |
search_carriers |
query, limit |
none |
search_carriers is the one to call first: add_delivery needs an internal
carrier code such as lp (La Poste) or chrono (Chronopost), and it reports
which carriers additionally require a postcode or an email.
Resource
parcel://deliveries/{filter_mode} exposes the same listing for passive reading,
with active or recent. It shares the cache and the hourly budget with
list_deliveries, so consulting it cannot quietly drain the allowance.
The rate-limit block
Every delivery result carries one:
{
"limit": 20,
"per": "hour",
"spent_by_this_server": 3,
"remaining_at_most": 17,
"note": "Counted locally: Parcel returns no rate-limit headers. ..."
}
It says remaining_at_most rather than remaining on purpose. Parcel publishes
no counter and returns no rate-limit headers, so this is counted here: it sees
only the requests this process sent since it started. The Parcel app on your
phone spends from the same budget, invisibly.
⚠️ Limitations
These come from the upstream API, not from this server.
| Limitation | Detail |
|---|---|
| Two endpoints, that is all | Reading deliveries and adding one. No delete, no edit, no refresh. |
| Tight budgets | 20 listings per hour. 20 additions per day, and a rejected addition still counts. |
| No rate-limit headers | The API returns none, so any budget figure, including this server's, is an estimate. |
| Reads are never fresh | Listing returns the Parcel server's cached view; it does not ask the carrier. |
| New deliveries look empty | One added through the API shows "No data available" until the server's first update, and nothing can force it. |
| One at a time | add_delivery takes a single delivery, and the tracking number must match a recognised format. Use carrier_code: "pholder" for a placeholder. |
| Premium only | The API is available to Parcel premium accounts. |
| Barely documented | Two help pages, reading and adding. Everything else here was established by observation and is handled defensively. |
The HTTP mode has one of its own: it does not authenticate callers. It forwards whatever key it is given and partitions its cache by a fingerprint of that key, so callers cannot see each other's parcels, but anyone who can reach the port can use it as a relay with their own key. Put it behind something that decides who may connect before exposing it beyond localhost.
🧪 Development
uv sync
uv run pytest # 80 tests, no network
uv run ruff check
uv run ruff format
uv run mypy # strict
The suite mocks the upstream with respx, so it costs nothing from either rate
limit. Three live scripts remain as manual checks against the real API, all
read-only, none of which calls add_delivery:
envchain parcel uv run scripts/smoke_test.py # in process
envchain parcel uv run scripts/stdio_test.py # through a real MCP client
envchain parcel uv run scripts/http_test.py # against a running HTTP server
CI runs lint and type checks once, then the suite on Python 3.10 through 3.13.
🏗️ Architecture
flowchart TB
subgraph clients["Clients"]
A["Claude Desktop, Claude Code"]
B["Any MCP client over HTTP"]
end
subgraph server["parcelapp-mcp"]
C["server.py: 3 tools, 1 resource,<br/>key resolution, code tables"]
D["client.py: HTTP, per-key cache,<br/>rate-limit counters"]
C --> D
end
subgraph upstream["api.parcel.app"]
E["deliveries<br/>20 per hour"]
F["add-delivery<br/>20 per day"]
G["supported_carriers.json<br/>no limit"]
end
A -- "stdio, key from env" --> C
B -- "HTTP, key per request" --> C
D --> E
D --> F
D --> G
Two rules hold this together. add_delivery checks the carrier code and any
required postcode or email against the cached catalogue before touching the
network, because a rejected request costs the same as a successful one. And
everything cached or counted is keyed by a fingerprint of the caller's API key,
so a shared HTTP deployment cannot serve one person's parcels to another.
📝 License
MIT, see LICENSE. Not affiliated with Parcel or its developer.
Made by Grégoire Compagnon (obeone)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file parcelapp_mcp-0.2.0.tar.gz.
File metadata
- Download URL: parcelapp_mcp-0.2.0.tar.gz
- Upload date:
- Size: 126.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1130fae4e5ace86cbce269d6374c58d96f2dc6e55164ad0302a59fb1c9575450
|
|
| MD5 |
4eda4162bdbc851e7836f1b89191f136
|
|
| BLAKE2b-256 |
3f6c589600b12f4ea77b76b7bd96bcb2052303ede00afaf28e3e1756dae3aef4
|
Provenance
The following attestation bundles were made for parcelapp_mcp-0.2.0.tar.gz:
Publisher:
publish.yml on obeone/parcelapp-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
parcelapp_mcp-0.2.0.tar.gz -
Subject digest:
1130fae4e5ace86cbce269d6374c58d96f2dc6e55164ad0302a59fb1c9575450 - Sigstore transparency entry: 2732835897
- Sigstore integration time:
-
Permalink:
obeone/parcelapp-mcp@bbf805703ddfe7d32689fa509b8ba51b3c461bf1 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/obeone
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bbf805703ddfe7d32689fa509b8ba51b3c461bf1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file parcelapp_mcp-0.2.0-py3-none-any.whl.
File metadata
- Download URL: parcelapp_mcp-0.2.0-py3-none-any.whl
- Upload date:
- Size: 17.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
930126e74a0ed202a5ef6e3e268808d70f32b41fdf4542af1ef4a87c5d12d80e
|
|
| MD5 |
13f62e0b337e726f033f8be58b22e89f
|
|
| BLAKE2b-256 |
b3a265c41a2b64e363dc5ba5653df90d0ea55f36311d19facb6de6407044963f
|
Provenance
The following attestation bundles were made for parcelapp_mcp-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on obeone/parcelapp-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
parcelapp_mcp-0.2.0-py3-none-any.whl -
Subject digest:
930126e74a0ed202a5ef6e3e268808d70f32b41fdf4542af1ef4a87c5d12d80e - Sigstore transparency entry: 2732836070
- Sigstore integration time:
-
Permalink:
obeone/parcelapp-mcp@bbf805703ddfe7d32689fa509b8ba51b3c461bf1 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/obeone
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bbf805703ddfe7d32689fa509b8ba51b3c461bf1 -
Trigger Event:
push
-
Statement type: