parcelapp-mcp 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ # Allowlist: exclude everything, then admit only what the build needs.
2
+ #
3
+ # Chosen over a blocklist because the set is five entries and stable, while
4
+ # the working tree accumulates things that must never reach a build context:
5
+ # .venv/, .omc/ state, .agent.lock, editor droppings. A blocklist protects
6
+ # only against what someone remembered to list.
7
+ #
8
+ # Adding a source directory means adding its `!` line below.
9
+
10
+ *
11
+
12
+ !pyproject.toml
13
+ !uv.lock
14
+ !README.md
15
+ !LICENSE
16
+ !src/
17
+
18
+ # Re-exclude what would otherwise ride along inside src/.
19
+ **/__pycache__/
20
+ **/*.py[co]
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # The test suite mocks the upstream API and needs no credentials: PARCEL_TOKEN
9
+ # is set per-test by a fixture. Nothing here ever reaches api.parcel.app, which
10
+ # matters because add_delivery is capped at 20 requests per day upstream.
11
+
12
+ jobs:
13
+ lint:
14
+ name: Lint and type-check
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ with:
19
+ # hatch-vcs reads the version from the nearest tag, and uv sync
20
+ # builds the project. A shallow clone has no tags and yields a
21
+ # meaningless 0.1.devN.
22
+ fetch-depth: 0
23
+ - uses: astral-sh/setup-uv@v10.0.1
24
+ with:
25
+ enable-cache: true
26
+ - run: uv sync --locked
27
+ - run: uv run ruff check
28
+ - run: uv run ruff format --check
29
+ - run: uv run mypy
30
+
31
+ test:
32
+ name: Tests (Python ${{ matrix.python-version }})
33
+ runs-on: ubuntu-latest
34
+ strategy:
35
+ fail-fast: false
36
+ matrix:
37
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
38
+ steps:
39
+ - uses: actions/checkout@v7
40
+ with:
41
+ # hatch-vcs reads the version from the nearest tag, and uv sync
42
+ # builds the project. A shallow clone has no tags and yields a
43
+ # meaningless 0.1.devN.
44
+ fetch-depth: 0
45
+ - uses: astral-sh/setup-uv@v10.0.1
46
+ with:
47
+ enable-cache: true
48
+ python-version: ${{ matrix.python-version }}
49
+ - run: uv sync --locked
50
+ - run: uv run pytest
@@ -0,0 +1,156 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ # One tag drives the whole release: PyPI, then the GitHub release, then the
8
+ # changelog commit. It is deliberately not split across two workflows, because
9
+ # a release created with the GITHUB_TOKEN does not trigger anything else --
10
+ # GitHub suppresses that cascade on purpose -- so a `release: published` hook
11
+ # would simply never fire.
12
+ #
13
+ # Publishing is irreversible: PyPI never lets a version number be reused, even
14
+ # after a yank. So the build job runs the full check suite first, and only a
15
+ # later job gets an OIDC token.
16
+
17
+ permissions: {}
18
+
19
+ jobs:
20
+ build:
21
+ name: Build and verify
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ contents: read # the workflow-level default is nothing, and checkout needs this
25
+ steps:
26
+ - uses: actions/checkout@v7
27
+ with:
28
+ # hatch-vcs reads the version from the nearest tag and git-cliff walks
29
+ # the history for the notes. A shallow checkout has neither, and would
30
+ # quietly produce a dev version instead of failing.
31
+ fetch-depth: 0
32
+
33
+ - uses: astral-sh/setup-uv@v10.0.1
34
+ with:
35
+ enable-cache: true
36
+
37
+ - run: uv sync --locked
38
+ - run: uv run ruff check
39
+ - run: uv run ruff format --check
40
+ - run: uv run mypy
41
+ - run: uv run pytest
42
+
43
+ - name: Build the sdist and the wheel
44
+ run: uv build
45
+
46
+ # With hatch-vcs the version is derived from the tag, so this normally
47
+ # agrees by construction. It stays because the ways it can disagree are
48
+ # exactly the ways hatch-vcs fails quietly: a shallow fetch, or a tag that
49
+ # is not on the commit being built, both of which yield a .devN version.
50
+ - name: Check the release tag against the built version
51
+ env:
52
+ TAG: ${{ github.ref_name }}
53
+ run: |
54
+ version="${TAG#v}"
55
+ if [ ! -f "dist/parcelapp_mcp-${version}.tar.gz" ]; then
56
+ echo "Tag ${TAG} implies version ${version}, but uv build produced:" >&2
57
+ ls -1 dist >&2
58
+ exit 1
59
+ fi
60
+
61
+ - name: Check the metadata renders on PyPI
62
+ run: uvx twine check --strict dist/*
63
+
64
+ # The same cliff.toml that generates CHANGELOG.md, so the release page and
65
+ # the file cannot drift. The leading "## <version> - <date>" heading is
66
+ # dropped: the release is already titled with its tag.
67
+ - name: Generate the release notes
68
+ run: |
69
+ # sed, in order: drop the leading "## <version> - <date>" heading,
70
+ # then drop the blank lines it leaves behind at the top.
71
+ uvx git-cliff --latest --strip all \
72
+ | sed -e '1{/^## /d;}' -e '/./,$!d' > release-notes.md
73
+ cat release-notes.md
74
+
75
+ - uses: actions/upload-artifact@v7
76
+ with:
77
+ name: release
78
+ path: |
79
+ dist/
80
+ release-notes.md
81
+ if-no-files-found: error
82
+
83
+ pypi:
84
+ name: Publish to PyPI
85
+ needs: build
86
+ runs-on: ubuntu-latest
87
+ # Configured at https://pypi.org/manage/project/parcelapp-mcp/settings/publishing/
88
+ # as a GitHub publisher: obeone/parcelapp-mcp, workflow publish.yml,
89
+ # environment pypi. No API token lives in this repository.
90
+ environment:
91
+ name: pypi
92
+ url: https://pypi.org/p/parcelapp-mcp
93
+ permissions:
94
+ id-token: write # mints the short-lived OIDC token Trusted Publishing exchanges
95
+ steps:
96
+ - uses: actions/download-artifact@v8
97
+ with:
98
+ name: release
99
+ - uses: pypa/gh-action-pypi-publish@v1.14.2
100
+
101
+ release:
102
+ name: GitHub release and changelog
103
+ # Last on purpose: a release must not announce a package that failed to
104
+ # upload. The reverse order leaves a dangling release; this order at worst
105
+ # leaves a published package with no release notes, which is fixable.
106
+ needs: pypi
107
+ runs-on: ubuntu-latest
108
+ permissions:
109
+ contents: write # creates the release and pushes the changelog commit
110
+ steps:
111
+ - uses: actions/checkout@v7
112
+ with:
113
+ # main, not the tag: the changelog commit belongs on the branch. The
114
+ # full history still gives git-cliff the tag it needs.
115
+ ref: main
116
+ fetch-depth: 0
117
+
118
+ - uses: astral-sh/setup-uv@v10.0.1
119
+
120
+ - uses: actions/download-artifact@v8
121
+ with:
122
+ name: release
123
+ # Outside the checkout: the release notes are a build artifact, not
124
+ # something that should show up as an untracked file in the commit
125
+ # this job is about to make.
126
+ path: artifacts
127
+
128
+ - name: Create the GitHub release
129
+ env:
130
+ GH_TOKEN: ${{ github.token }}
131
+ TAG: ${{ github.ref_name }}
132
+ run: |
133
+ gh release create "$TAG" \
134
+ --title "$TAG" \
135
+ --notes-file artifacts/release-notes.md \
136
+ artifacts/dist/*
137
+
138
+ # Regenerated rather than appended, so a reworded history stays reflected.
139
+ # The commit is a chore(release), which cliff.toml skips, so it never shows
140
+ # up in the next changelog.
141
+ - name: Update CHANGELOG.md on main
142
+ env:
143
+ TAG: ${{ github.ref_name }}
144
+ run: |
145
+ uvx git-cliff -o CHANGELOG.md
146
+ if git diff --quiet -- CHANGELOG.md; then
147
+ echo "CHANGELOG.md already up to date."
148
+ exit 0
149
+ fi
150
+ git config user.name "github-actions[bot]"
151
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
152
+ git add CHANGELOG.md
153
+ git commit -m "chore(release): update the changelog for ${TAG}"
154
+ # main may have moved while the package was uploading.
155
+ git pull --rebase --autostash
156
+ git push
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ .agent.lock
6
+ PROMPT.md
7
+ .worktrees/
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ Generated from the Conventional Commits in the git history. Do not edit by
4
+ hand: run `uvx git-cliff -o CHANGELOG.md` instead.
5
+
6
+ ## Unreleased
7
+
8
+ ### Features
9
+
10
+ - Per-request API key, rate-limit budget, filtering and a resource
11
+ - Containerise the HTTP transport
12
+
13
+ ### Bug fixes
14
+
15
+ - Pin setup-uv to an existing ref
16
+
17
+ ### Refactoring
18
+
19
+ - Adopt src layout, rename to parcelapp-mcp, move live scripts
20
+ - Split the HTTP client out of the server module
21
+
22
+ ### Documentation
23
+
24
+ - Add CLAUDE.md with architecture and rate-limit invariants
25
+ - Add the MIT licence and rewrite the README for publication
26
+ - Rewrite the README for the two transports
27
+ - Record the HTTP-transport invariants in CLAUDE.md
28
+
29
+ ### Tests
30
+
31
+ - Add a pytest suite mocking the upstream API with respx
32
+
33
+ ### Build and CI
34
+
35
+ - Publish to PyPI on a released tag
36
+
37
+ ### Chores
38
+
39
+ - Add ruff, strict mypy and a CI workflow
40
+ - Untrack the design brief
41
+ - Fill in the package, image and repository metadata
42
+
@@ -0,0 +1,180 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ An MCP server wrapping the [Parcel](https://parcelapp.net) delivery-tracking API,
8
+ which premium users of the macOS/iOS app get access to. Two upstream endpoints plus a
9
+ public carrier-code list, exposed as three MCP tools and one resource template.
10
+
11
+ It serves two transports. stdio reads the key from the environment. HTTP takes it
12
+ per request, which turns process-global state into a leak and drives most of the
13
+ design below.
14
+
15
+ The upstream API is documented only by two help pages:
16
+
17
+ - <https://parcelapp.net/help/api-view-deliveries.html>
18
+ - <https://parcelapp.net/help/api-add-delivery.html>
19
+
20
+ Anything beyond those pages is undocumented. When behaviour is unclear, handle it
21
+ defensively and note the uncertainty in a comment rather than inventing a contract.
22
+
23
+ `PROMPT.md` holds the original project brief (restructuring roadmap and feature
24
+ backlog). Read it before starting work that touches project layout or scope.
25
+
26
+ ## Commands
27
+
28
+ The project uses `uv`. The API key comes from `envchain parcel` locally.
29
+
30
+ ```bash
31
+ uv sync # install deps and the dev group
32
+ uv run pytest # 80 tests, no network
33
+ uv run pytest tests/test_tools.py::test_add_delivery_demands_a_postcode_without_spending_a_request
34
+ uv run ruff check && uv run ruff format # lint, then format
35
+ uv run mypy # strict, and currently clean
36
+ uvx git-cliff --unreleased # preview the next changelog section
37
+ envchain parcel uv run parcelapp-mcp # run the server over stdio
38
+ envchain parcel uv run scripts/smoke_test.py # live, read-only check (in-process)
39
+ envchain parcel uv run scripts/stdio_test.py # live, end-to-end MCP client over stdio
40
+ envchain parcel uv run scripts/http_test.py # live, against a running HTTP server
41
+ docker build --build-arg VERSION=$(git describe --tags --abbrev=0 | sed s/^v//) \
42
+ -t parcelapp-mcp . && docker run -p 8000:8000 parcelapp-mcp
43
+ ```
44
+
45
+ `uv run pytest` is the loop to work in: it never touches the network, so it costs
46
+ nothing from either rate-limit budget. The two `scripts/` entries do hit the real API
47
+ and are manual checks, not part of the suite.
48
+
49
+ The suite mocks the upstream with `respx`. Every test runs under `@respx.mock`, so an
50
+ unmocked request fails the test rather than escaping to the real API.
51
+
52
+ CI (`.github/workflows/ci.yml`) runs those same three commands: lint and types once,
53
+ tests across 3.10 to 3.13. It uses `uv sync --locked`, so a dependency change means
54
+ committing the refreshed `uv.lock` alongside it.
55
+
56
+ ## Releasing
57
+
58
+ A tag is the whole trigger. `git tag v0.3.0 && git push --tags` runs
59
+ `.github/workflows/publish.yml`, which checks, builds, uploads to PyPI through
60
+ Trusted Publishing (no API token in the repository), creates the GitHub release with
61
+ generated notes, and commits the refreshed `CHANGELOG.md` back to `main`.
62
+
63
+ Nothing is versioned by hand. `hatch-vcs` derives the version from the nearest git
64
+ tag, which has three consequences worth knowing before debugging a wrong number:
65
+
66
+ - **Every checkout that builds this package needs `fetch-depth: 0`.** A shallow clone
67
+ has no tags, so the build quietly produces a `0.1.devN` version instead of failing.
68
+ The build job's tag check exists to catch exactly that.
69
+ - **A build with no `.git` at all fails outright.** `.dockerignore` keeps `.git` out of
70
+ the build context on purpose, so the Dockerfile passes the version in through
71
+ `--build-arg VERSION=…`, which it forwards as `SETUPTOOLS_SCM_PRETEND_VERSION`. A
72
+ `docker build` without that argument reports `0.0.0` rather than lying. Beware
73
+ the warm uv cache mount: it can hand back a wheel built under a previous
74
+ `VERSION`, so pass `--no-cache` when the number in the image actually matters.
75
+ - **A dirty working tree taints the version** with a `.dYYYYMMDD` suffix. Harmless
76
+ locally, but it means a release must be built from a clean checkout.
77
+
78
+ The changelog is generated from the Conventional Commits by `git-cliff`, configured in
79
+ `cliff.toml`. The same config produces `CHANGELOG.md` and the body of each GitHub
80
+ release, so the two cannot drift. Preview it locally with `uvx git-cliff --unreleased`.
81
+
82
+ The workflow is deliberately one file rather than a `release.yml` feeding a
83
+ `publish.yml`: GitHub suppresses the event cascade from anything the `GITHUB_TOKEN`
84
+ creates, so a release made by CI would never fire a `release: published` trigger.
85
+
86
+ ## Rate limits drive everything
87
+
88
+ | Endpoint | Limit |
89
+ | --- | --- |
90
+ | `deliveries/` | 20 per hour |
91
+ | `add-delivery/` | 20 per **day**, failed attempts included |
92
+ | `supported_carriers.json` | none |
93
+
94
+ Two consequences the code depends on:
95
+
96
+ - **`add_delivery` validates locally before touching the network.** It checks the
97
+ carrier code against the cached carrier list and enforces the carrier's
98
+ `extra_required` field (postcode or email) *before* issuing the POST. A
99
+ preventable error must never consume one of the 20 daily requests.
100
+ - **`add_delivery` must not be called live during development.** Test it against a
101
+ mock. `smoke_test.py` deliberately never touches it; `stdio_test.py` only exercises
102
+ the local guard rails, which fail before any request is sent.
103
+
104
+ ## Architecture
105
+
106
+ Two modules, split along one line: `client.py` knows nothing about MCP, `server.py`
107
+ knows nothing about HTTP. Resist adding a third for three tools.
108
+
109
+ `src/parcel_mcp/client.py` — the network:
110
+
111
+ - `request()` is the single HTTP chokepoint. It injects the `api-key` header,
112
+ maps 401 and 429 to explicit messages, and unwraps the upstream
113
+ `{"success": bool, "error_message": str}` envelope. Every upstream failure mode is
114
+ normalised here, so tools never inspect status codes themselves.
115
+ - `_cache` is a module-level `dict[str, tuple[float, Any]]`, with per-call TTLs via
116
+ `cached()` / `store()` and eviction via `clear_cache()`. Deliveries: 180 s (upstream
117
+ serves a cached view anyway, so this costs no freshness and protects the hourly
118
+ budget). Carriers: 24 h. It is process-global state, so tests must call
119
+ `clear_cache()` between cases.
120
+ - `_requests` holds the rate-limit counters, per bucket and per caller, pruned on
121
+ read. `clear_rate_limits()` resets them for tests.
122
+ - `ParcelError` lives here, next to what raises it, though it reaches into the SDK.
123
+ See the invariant below.
124
+
125
+ `src/parcel_mcp/server.py` — the MCP surface:
126
+
127
+ - The `MCPServer` instance, the three tools, the resource template, the argparse
128
+ transport selection, `main()`.
129
+ - `resolve_api_key(ctx)` decides where the key comes from: an `X-Parcel-Token` or
130
+ `Authorization: Bearer` header first, the environment as fallback. That fallback
131
+ is what lets one code path serve both transports.
132
+ - The two translation tables, `STATUS_CODES` and `EXTRA_REQUIRED`, exist because the
133
+ upstream API speaks in integers. Resolving them, along with carrier codes to names,
134
+ is the entire value this server adds. Do not strip it in favour of raw passthrough.
135
+ - `add_delivery`'s local validation stays here, beside the error messages it produces.
136
+
137
+ ### Non-obvious invariants
138
+
139
+ - **`ParcelError` subclasses the SDK's `ToolError`.** If it does not, mcp 2.x
140
+ replaces the message with a generic "Error executing tool" and the model learns
141
+ nothing. Do not reparent it to `Exception`.
142
+ - **mcp 2.x API: `MCPServer`, not `FastMCP`.** The v1 spelling is gone. Do not
143
+ reintroduce it from memory; check the installed version (`mcp` 2.1.x) when unsure.
144
+ - **Docstrings are the tool contracts.** They are the text the model actually reads.
145
+ Keep them describing behaviour, arguments and rate limits, not implementation.
146
+ - **Errors name the fix.** "Bpost requires a postcode; pass `postcode`" beats
147
+ "invalid request". Any new error path follows that shape.
148
+ - `carrier_name()` swallows `ParcelError` and falls back to the raw code, so a
149
+ carrier-list fetch failure degrades `list_deliveries` instead of breaking it.
150
+ - **`@mcp.tool()` returns the function unchanged.** It registers and hands back `fn`,
151
+ so tests and scripts can call `list_deliveries(...)` directly without going through
152
+ `mcp.call_tool`.
153
+ - **`ToolAnnotations` fields are snake_case in Python.** `read_only_hint`, not
154
+ `readOnlyHint`. The camelCase spelling is the wire alias; pydantic accepts it, but
155
+ mypy cannot check it. The serialised JSON is identical either way.
156
+ - **Everything cached or counted is keyed by `key_fingerprint(key)`.** Over HTTP one
157
+ process serves several people; an unpartitioned cache would hand one caller's
158
+ parcels to another. The carrier catalogue is the deliberate exception, being
159
+ public. Never introduce a cache key without the fingerprint in it.
160
+ - **`ctx: Context | None = None`, never a bare `Context`.** The SDK unwraps the union
161
+ and still injects it, while the default keeps the tools directly callable from the
162
+ tests and scripts. Verified, not assumed.
163
+ - **A static resource cannot receive a `Context`.** The SDK refuses it outright, which
164
+ is why `parcel://deliveries/{filter_mode}` is a URI template: only templated
165
+ resources get a context, and without one a resource cannot read a per-request key.
166
+ - **The client factory is `streamable_http_client`, and headers go through an
167
+ `httpx2.AsyncClient` passed as `http_client`.** Not `streamablehttp_client`, and not
168
+ a `headers=` argument; both are v1 spellings that no longer exist.
169
+
170
+ ## Auth
171
+
172
+ `PARCEL_TOKEN`, falling back to `PARCEL_API_KEY`, sent as the `api-key` header. Keys
173
+ are generated at <https://web.parcelapp.net>. Never put a realistic-looking key in a
174
+ test fixture.
175
+
176
+ ## Dependencies
177
+
178
+ Standard library plus `httpx` and `mcp`. Justify any third dependency before adding
179
+ it; `respx` or `httpx.MockTransport` for the future test suite is the expected
180
+ exception.
@@ -0,0 +1,72 @@
1
+ # syntax=docker/dockerfile:1
2
+
3
+ # Serves the streamable-http transport. No credential is baked in or expected
4
+ # at runtime: each request carries its own Parcel key in an X-Parcel-Token or
5
+ # Authorization: Bearer header, so one container can serve several people.
6
+
7
+ FROM python:3.13-slim
8
+
9
+ # uv is pinned like any other dependency.
10
+ COPY --from=ghcr.io/astral-sh/uv:0.12.9 /uv /uvx /bin/
11
+
12
+ WORKDIR /app
13
+
14
+ # Created first: a --chown naming a user this stage does not know yet is
15
+ # silently dropped, and the files land as root.
16
+ RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app app
17
+
18
+ # Dependencies as root, in their own cached layer. /app/.venv stays root-owned
19
+ # on purpose: the app runs it but cannot rewrite it.
20
+ RUN --mount=type=cache,target=/root/.cache/uv \
21
+ --mount=type=bind,source=uv.lock,target=uv.lock \
22
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
23
+ uv sync --locked --no-install-project --no-dev
24
+
25
+ # The package version comes from the nearest git tag, but .dockerignore keeps
26
+ # .git out of the build context on purpose, so hatchling cannot read it here.
27
+ # This ARG is the only way in, and a build without it honestly reports 0.0.0
28
+ # rather than a stale hardcoded number:
29
+ # docker build --build-arg VERSION=$(git describe --tags --abbrev=0 | sed s/^v//) .
30
+ # It feeds both the install below and the OCI label at the end of this stage.
31
+ ARG VERSION=0.0.0
32
+
33
+ COPY --chown=app:app . .
34
+ RUN --mount=type=cache,target=/root/.cache/uv \
35
+ SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} uv sync --locked --no-dev
36
+
37
+ # Hand over the workdir, not the dependency tree, then drop privileges last.
38
+ RUN chown app:app /app
39
+ USER app
40
+
41
+ ENV PATH="/app/.venv/bin:$PATH" \
42
+ PARCEL_TRANSPORT=streamable-http \
43
+ PARCEL_HOST=0.0.0.0 \
44
+ PARCEL_PORT=8000 \
45
+ PARCEL_PATH=/mcp
46
+
47
+ EXPOSE 8000
48
+
49
+ LABEL org.opencontainers.image.title="parcelapp-mcp" \
50
+ org.opencontainers.image.description="MCP server for the Parcel delivery tracking app: read and add deliveries over stdio or HTTP" \
51
+ org.opencontainers.image.source="https://github.com/obeone/parcelapp-mcp" \
52
+ org.opencontainers.image.documentation="https://github.com/obeone/parcelapp-mcp#readme" \
53
+ org.opencontainers.image.url="https://github.com/obeone/parcelapp-mcp" \
54
+ org.opencontainers.image.authors="Grégoire Compagnon <obeone@obeone.org>" \
55
+ org.opencontainers.image.licenses="MIT" \
56
+ org.opencontainers.image.version="${VERSION}"
57
+
58
+ # There is no /health endpoint, and a GET on /mcp opens an SSE stream that
59
+ # never returns, so neither would work. This performs a real MCP initialize
60
+ # handshake instead: it proves the session manager and the tool registry are
61
+ # alive, not merely that a socket is open. urlopen raises on a non-2xx, and the
62
+ # assert catches a 200 that is not a protocol response.
63
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
64
+ CMD python -c "import json,urllib.request as u; \
65
+ r=u.Request('http://127.0.0.1:8000/mcp', \
66
+ data=json.dumps({'jsonrpc':'2.0','id':1,'method':'initialize','params':{'protocolVersion':'2025-06-18','capabilities':{},'clientInfo':{'name':'healthcheck','version':'0'}}}).encode(), \
67
+ headers={'Content-Type':'application/json','Accept':'application/json, text/event-stream'}); \
68
+ assert b'\"result\"' in u.urlopen(r, timeout=4).read()" || exit 1
69
+
70
+ # Exec form, so the server is PID 1 and receives SIGTERM directly. uvicorn
71
+ # installs its own handler and drains, so no init shim is needed.
72
+ CMD ["parcelapp-mcp"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Grégoire Compagnon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.