FastMCP server exposing GS1 Belgium & Luxembourg My Product Manager Upload (v17) as a small, agent-safe MCP tool surface.
Project description
mcp/upload/
[!IMPORTANT] Unofficial. This project is a community effort and is not affiliated with, endorsed by, or supported by GS1 in any way. "GS1" and "My Product Manager" are the property of their respective owners. Use at your own risk.
The My Product Manager Upload MCP server — a standalone Python package (FastMCP)
exposing only the provider-role half of the API. It is one of two independent
successors to the deprecated combined server (see
../combined/README.md for the migration guide); its sibling
is ../download/. A data-supplier integrator installs this package
alone and never has to configure or expose Download credentials/tools it has no
business holding — see issue
#82.
It assembles its tools at runtime from the git-ignored
effective spec under
schemas/upload/ via FastMCP.from_openapi(). Unlike the
SDKs, this needs no code-generation step: just gen step 1 (the schema-prep
build) is the only prerequisite.
The 2-tool provider-role surface
The Upload API is 2 operations (upsert-tradeitem, get-tradeitem), but the MCP
surface is deliberately not a 1:1 map — a raw POST /tradeitems tool would be
actively dangerous for an agent (see "Sharp edges" below):
| Tool | Origin | Behavior |
|---|---|---|
upsert_and_await_validation(trade_item) |
hand-written composite | POST /tradeitems -> poll GET /tradeitems/{gtin} until settled -> returns the validation verdict |
get_trade_item_by_gtin(gtin) |
generated (upload get-tradeitem), renamed via mcp_names |
standalone single-item read |
The raw POST /tradeitems is excluded from from_openapi() via a RouteMap so the
fire-and-forget write (always 201, never a verdict) never reaches the agent.
Sharp edges this surface removes
- The write is fire-and-forget by shape.
POST /tradeitemsreturns201with an empty body — no validation verdict. The true accept/reject signal only arrives by pollingGET /tradeitems/{gtin}untilmetaData.statusleavespendingValidation(seedocs/research/gs1-api-facts.md§5). A raw POST tool would let an agent report success it never actually confirmed. - Errors are opaque unless parsed. Non-2xx bodies carry field-level signal
(
validationResulton 400, RFC-7807problemDetailson 401/403/404) that a bareraise_for_status()throws away. Every tool surfaces these as a structuredToolErrorinstead (see below).
Auth — the Python leg of the two-layer design
The server's httpx.AsyncClient carries one Gs1BeluAuth (httpx.Auth), vendored
byte-identically from ../shared/ (map #76 — the same class the
Download server uses, so a fix to this security-critical path is made once, not in two
hand-maintained copies that can drift):
- OAuth2 client-credentials Bearer token — fetched from the environment's token
endpoint, cached in memory, and proactively refreshed on a skew margin (default 60s)
before the token's runtime
expires_inelapses. Concurrent callers coalesce onto a single fetch via double-checked locking. Ocp-Apim-Subscription-Keyheader — stamped on every request, header only; the spec's query-param alternative is never used, so the key never lands in a URL or log.
Credentials are single-tenant, loaded from environment variables, one credential set
for this process (never GS1BELU_DOWNLOAD_* — that's the sibling server's role):
| Variable | Required | Meaning |
|---|---|---|
GS1BELU_ENVIRONMENT |
yes | uat or prod — the only knob; it derives the API host, token host, and OAuth audience (trailing slash baked in). There is no raw base-URL setting anywhere. |
GS1BELU_API_VERSION |
no (default v17) |
the contract version segment in the API path. |
GS1BELU_UPLOAD_CLIENT_ID / _CLIENT_SECRET / _SUBSCRIPTION_KEY |
yes | Upload API credential set. |
A RateLimiter (sliding window, 10 req/s), also vendored from ../shared/, is
attached to the same client alongside the auth and structured-error hooks — this
process's own limiter, never shared with the Download server (#77).
Sunset monitoring
GS1 can announce an API-version retirement in-band via the Sunset header (RFC
8594). warn_on_sunset (vendored from ../shared/) is a response hook that logs a
WARNING through logging.getLogger("gs1belu_mpm_upload._shared.sunset") whenever a
response carries the header. It never alters, retries, or fails a request —
observation only.
Quickstart
No clone, no build — the published wheel bundles the effective spec it needs at
runtime (see "Publishing" below). Point an MCP-capable client at
uvx gs1belu-mpm-upload-mcp with the documented env vars, e.g. in .mcp.json:
{
"mcpServers": {
"gs1belu-mpm-upload": {
"command": "uvx",
"args": ["gs1belu-mpm-upload-mcp"],
"env": {
"GS1BELU_ENVIRONMENT": "uat",
"GS1BELU_UPLOAD_CLIENT_ID": "...",
"GS1BELU_UPLOAD_CLIENT_SECRET": "...",
"GS1BELU_UPLOAD_SUBSCRIPTION_KEY": "..."
}
}
}
}
Running locally (for development)
Working in a checkout of this repo instead?
just gen # builds the git-ignored effective specs (step 1 only needed)
export GS1BELU_ENVIRONMENT=uat
export GS1BELU_UPLOAD_CLIENT_ID=... GS1BELU_UPLOAD_CLIENT_SECRET=... GS1BELU_UPLOAD_SUBSCRIPTION_KEY=...
just run-mcp-upload # launches the standalone Upload server over stdio
Or point an MCP-capable client at uv run --project mcp/upload gs1belu-mpm-upload-mcp
with the same environment variables set.
Testing
just test-mcp drives build_upload_server() through FastMCP's in-memory
Client, with a fake httpx transport and a mocked token endpoint injected under
the server's AsyncClient — no network, no live GS1, no subprocess. See
tests/:
test_tool_surface.py— pins the exact 2-tool list; the raw POST is absent.test_composite.py— the settled verdict is returned oncemetaData.statusleavespendingValidation, polling stops immediately, and the bounded timeout is honored.test_config.py— loads onlyGS1BELU_UPLOAD_*; a strayGS1BELU_DOWNLOAD_*value is never read.test_specs.py— pins the packaged-resource spec-resolution path.
Auth/rate-limit/sunset/error-mapping behavior is verified once, directly against
../shared/'s own test suite — not duplicated here (#79's Testing Decisions).
Layout
pyproject.toml—gs1belu-mpm-upload-mcp, thegs1belu-mpm-upload-mcpconsole entry point, the custom build hook that vendors../shared/and bundles the upload effective spec.hatch_build.py— build hook: copies../shared/src/_shared/intosrc/gs1belu_mpm_upload/_shared/(map #76) and../../schemas/upload/v17.effective.yamlinto the packaged_specs/resource home. Never hand-edit either — they're git-ignored build outputs this hook (re)populates.src/gs1belu_mpm_upload/config.py— env-var credential loading for this role only, built on the sharedparse_environment()+CredentialSet.from_env()seam.composite.py—upsert_and_await_validationas a plain, independently testable async function.server.py— assembles the server (build_upload_server()): RouteMap exclusion,mcp_namesrenaming, the composite tool.specs.py— locates and parses the upload effective spec.cli.py— thegs1belu-mpm-upload-mcpconsole entry point._shared/— vendored copy of../shared/src/_shared/; git-ignored, regenerated by the build hook.
tests/— the in-memoryClient+ fake-transport suite described above.
Publishing
Release-and-publish machinery — server.json, the mcp-name README marker above,
publish-mcp.yml's PyPI + MCP-registry OIDC publish (parameterized across all three
mcp* packages, #82), and the release-please mcp-upload component/tag
(mcp-upload-v*) — is configured per
#53 and
#82. See
docs/release-execution-checklist.md for
the human-run, prerequisite-gated steps below the first live publish.
Project details
Release history Release notifications | RSS feed
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 gs1belu_mpm_upload_mcp-0.2.0.tar.gz.
File metadata
- Download URL: gs1belu_mpm_upload_mcp-0.2.0.tar.gz
- Upload date:
- Size: 21.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
babc26ae0aae614679d6c6625d250f739c8d1a56f311db34b352e2ae1e372137
|
|
| MD5 |
257a393f940a2d63f0a06e1a92a115e3
|
|
| BLAKE2b-256 |
09badae87aac3fdea3d1c69a0d93f77aefa7740246f07e7968591ac49cbd8c70
|
Provenance
The following attestation bundles were made for gs1belu_mpm_upload_mcp-0.2.0.tar.gz:
Publisher:
publish-mcp.yml on WimSuenens/gs1belu.myproductmanager
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gs1belu_mpm_upload_mcp-0.2.0.tar.gz -
Subject digest:
babc26ae0aae614679d6c6625d250f739c8d1a56f311db34b352e2ae1e372137 - Sigstore transparency entry: 2261477317
- Sigstore integration time:
-
Permalink:
WimSuenens/gs1belu.myproductmanager@956969217d20ced0f1ba19178eb0abd1e276bbd2 -
Branch / Tag:
refs/tags/mcp-upload-v0.2.0 - Owner: https://github.com/WimSuenens
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-mcp.yml@956969217d20ced0f1ba19178eb0abd1e276bbd2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gs1belu_mpm_upload_mcp-0.2.0-py3-none-any.whl.
File metadata
- Download URL: gs1belu_mpm_upload_mcp-0.2.0-py3-none-any.whl
- Upload date:
- Size: 21.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
85e9b7fd7133a391f69e30b31c8deec441e6bcbf13543a6230e8447c261789d2
|
|
| MD5 |
348df8721f3c248c96871fbed0441229
|
|
| BLAKE2b-256 |
e5d40a367c45ea2de34cb17164e236d8d1e199a1eb3b9d2531e7958021a3b04c
|
Provenance
The following attestation bundles were made for gs1belu_mpm_upload_mcp-0.2.0-py3-none-any.whl:
Publisher:
publish-mcp.yml on WimSuenens/gs1belu.myproductmanager
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gs1belu_mpm_upload_mcp-0.2.0-py3-none-any.whl -
Subject digest:
85e9b7fd7133a391f69e30b31c8deec441e6bcbf13543a6230e8447c261789d2 - Sigstore transparency entry: 2261477373
- Sigstore integration time:
-
Permalink:
WimSuenens/gs1belu.myproductmanager@956969217d20ced0f1ba19178eb0abd1e276bbd2 -
Branch / Tag:
refs/tags/mcp-upload-v0.2.0 - Owner: https://github.com/WimSuenens
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-mcp.yml@956969217d20ced0f1ba19178eb0abd1e276bbd2 -
Trigger Event:
push
-
Statement type: