Sign inSign up

obeoneorg/parcelapp-mcp

By obeoneorg

β€’Updated 11 days ago

MCP server for Parcel delivery tracking: read and add parcels over stdio or HTTP

Image
0

92

obeoneorg/parcelapp-mcp repository overview

⁠parcelapp-mcp

Python PyPI CI Docker License

An MCP server for Parcel⁠, the iOS/macOS delivery tracking app. It wraps Parcel's external API (a premium feature of the app) so an LLM client can list your deliveries, add new ones to track, and look up carrier codes, over either stdio or streamable HTTP.

This project is not affiliated with or endorsed by Parcel.

β πŸš€ Features

FeatureNotes
πŸ“¦List deliveriesRecent or active, with filtering and sorting, served from a 3-minute cache
βž•Add a deliveryValidated locally first, so a typo never spends part of the daily quota
πŸ”Search carriersFree, unrated lookup from code or name to the internal carrier code
πŸ“‘Resource templateparcel://deliveries/{filter_mode} for passive reads alongside the tools
πŸ”Two auth modesEnvironment variable over stdio, per-request header over HTTP
🚦Rate-limit awarenessEvery result reports an honest, locally-tracked budget estimate
🐳Container-readyShips a Dockerfile for the HTTP transport, no key baked in

⁠⚑ Quickstart

Run it directly with uv⁠, no install step:

PARCEL_TOKEN=your-parcel-api-key uvx parcelapp-mcp

Get a key at https://web.parcelapp.net⁠ (requires a Parcel premium subscription). Pin a version with uvx [email protected], or track the latest release with uvx parcelapp-mcp@latest.

To wire it into an MCP client such as Claude Desktop, add:

{
  "mcpServers": {
    "parcel": {
      "command": "uvx",
      "args": ["parcelapp-mcp"],
      "env": { "PARCEL_TOKEN": "your-parcel-api-key" }
    }
  }
}

β πŸ“¦ Installation

⁠Local
# Persistent install, on your PATH
uv tool install parcelapp-mcp

# Or, inside a virtualenv (a bare `pip install` fails outside one on
# externally-managed pythons such as Homebrew's or Debian's)
python -m venv .venv
source .venv/bin/activate
pip install parcelapp-mcp
⁠🐳 Docker

The image serves the streamable-http transport only; it holds no credential, since the key travels with each request instead.

docker pull ghcr.io/obeone/parcelapp-mcp
docker run -p 8000:8000 ghcr.io/obeone/parcelapp-mcp

Images are published on every v* tag, for linux/amd64 and linux/arm64, to both ghcr.io/obeone/parcelapp-mcp and docker.io/obeoneorg/parcelapp-mcp, with an SBOM and a provenance attestation attached.

Every image is signed with cosign⁠, keylessly: there is no public key to distribute, the signature is bound to the workflow that produced it and recorded in Sigstore's public transparency log. Check one before you run it:

cosign verify ghcr.io/obeone/parcelapp-mcp:0.2.0 \
  --certificate-identity-regexp '^https://github.com/obeone/parcelapp-mcp/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

To build it yourself instead:

docker build -t parcelapp-mcp \
  --build-arg VERSION="$(git describe --tags --abbrev=0 | sed 's/^v//')" .
docker run -p 8000:8000 parcelapp-mcp

The VERSION build argument matters: .dockerignore excludes .git, and the package version is derived from the git tag, so without it the build still succeeds but reports version 0.0.0.

Call it with your key in a header (use 127.0.0.1, not localhost: on macOS localhost resolves to ::1 first, and -p 8000:8000 only publishes IPv4):

curl http://127.0.0.1:8000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "X-Parcel-Token: your-parcel-api-key" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

The HTTP transport does not authenticate callers: it partitions its cache and rate-limit counters by a fingerprint of the key so two callers never see each other's parcels, but anyone who can reach the port can relay a request using their own key. Put it behind a network boundary you control, or add your own auth in front of it.

β βš™οΈ Configuration

VariableDefaultMeaning
PARCEL_TOKENnoneAPI key, stdio transport. Falls back to PARCEL_API_KEY
PARCEL_API_KEYnoneAlternate name for the key above
PARCEL_TRANSPORTstdiostdio or streamable-http
PARCEL_HOST127.0.0.1Bind address for streamable-http
PARCEL_PORT8000Port for streamable-http
PARCEL_PATH/mcpURL path for streamable-http
PARCEL_LOG_LEVELINFOPython logging level

Over HTTP, a request's own key always wins: send it as X-Parcel-Token or Authorization: Bearer <key>. The environment variables are only the stdio path and the HTTP fallback.

Each CLI flag mirrors one of these: --transport, --host, --port, --path.

β πŸ› οΈ Development

CommandPurpose
uv syncInstall dependencies, including the dev group
uv run pytestRun the 80-test suite (mocked, no network)
uv run ruff checkLint
uv run ruff formatFormat
uv run mypyStrict type-check
envchain parcel uv run parcelapp-mcpRun over stdio with a local key
envchain parcel uv run scripts/smoke_test.pyLive, read-only in-process check
envchain parcel uv run scripts/stdio_test.pyLive end-to-end check over stdio
envchain parcel uv run scripts/http_test.pyLive check against a running HTTP server

CI runs lint, type-check and the test matrix (Python 3.10 to 3.13) on every push and pull request against main.

β πŸ“‘ API

Three MCP tools, all documented in detail through their own docstrings (the text a model actually reads at call time):

ToolUpstream limitSummary
list_deliveries20 / hourRecent or active deliveries, with status, carrier name and events resolved
add_delivery20 / dayAdds a tracking number; rejects an unknown carrier or a missing required field before spending a request
search_carriersnoneLooks up a carrier code by name or code, including whether it needs a postcode or email

Plus one resource template, parcel://deliveries/{filter_mode} (active or recent), for reading the same listing without a tool call.

Every result carries a rate_limit block. Parcel publishes no rate-limit headers, so the figures are counted locally by this server and reported as remaining_at_most: the Parcel app itself, or another client sharing the same key, can spend from the same budget invisibly.

⁠Upstream quirks

Three behaviours that look like bugs here and are not:

  • A listing is never fresh. It returns Parcel's own cached view of your deliveries. Nothing in this API asks a carrier for an update on demand.
  • A newly added delivery looks empty. It shows "No data available" until Parcel's next update cycle reaches it, and nothing can force that.
  • A tracking number must match a format a carrier recognises. For a placeholder with no real number behind it, pass carrier_code: "pholder".

β πŸš€ Releases

Pushing a v* tag drives the whole pipeline: build and test, publish to PyPI via Trusted Publishing, create the GitHub release, and regenerate CHANGELOG.md with git-cliff⁠ from the Conventional Commit history, then build and push the multi-architecture image to both registries. Nothing here is versioned by hand.

β πŸ—οΈ Architecture

The architecture diagram renders on the GitHub README⁠.

β πŸ“ License

MIT, see LICENSE⁠.

Made by GrΓ©goire Compagnon (obeone)

Tag summary

Content type

Image

Digest

sha256:7073131db…

Size

75.3 MB

Last updated

11 days ago

docker pull obeoneorg/parcelapp-mcp