Skip to main content

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 /tradeitems returns 201 with an empty body — no validation verdict. The true accept/reject signal only arrives by polling GET /tradeitems/{gtin} until metaData.status leaves pendingValidation (see docs/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 (validationResult on 400, RFC-7807 problemDetails on 401/403/404) that a bare raise_for_status() throws away. Every tool surfaces these as a structured ToolError instead (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_in elapses. Concurrent callers coalesce onto a single fetch via double-checked locking.
  • Ocp-Apim-Subscription-Key header — 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 once metaData.status leaves pendingValidation, polling stops immediately, and the bounded timeout is honored.
  • test_config.py — loads only GS1BELU_UPLOAD_*; a stray GS1BELU_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.tomlgs1belu-mpm-upload-mcp, the gs1belu-mpm-upload-mcp console entry point, the custom build hook that vendors ../shared/ and bundles the upload effective spec.
  • hatch_build.py — build hook: copies ../shared/src/_shared/ into src/gs1belu_mpm_upload/_shared/ (map #76) and ../../schemas/upload/v17.effective.yaml into 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 shared parse_environment() + CredentialSet.from_env() seam.
    • composite.pyupsert_and_await_validation as a plain, independently testable async function.
    • server.py — assembles the server (build_upload_server()): RouteMap exclusion, mcp_names renaming, the composite tool.
    • specs.py — locates and parses the upload effective spec.
    • cli.py — the gs1belu-mpm-upload-mcp console entry point.
    • _shared/ — vendored copy of ../shared/src/_shared/; git-ignored, regenerated by the build hook.
  • tests/ — the in-memory Client + 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gs1belu_mpm_upload_mcp-0.2.0.tar.gz (21.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gs1belu_mpm_upload_mcp-0.2.0-py3-none-any.whl (21.8 kB view details)

Uploaded Python 3

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

Hashes for gs1belu_mpm_upload_mcp-0.2.0.tar.gz
Algorithm Hash digest
SHA256 babc26ae0aae614679d6c6625d250f739c8d1a56f311db34b352e2ae1e372137
MD5 257a393f940a2d63f0a06e1a92a115e3
BLAKE2b-256 09badae87aac3fdea3d1c69a0d93f77aefa7740246f07e7968591ac49cbd8c70

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gs1belu_mpm_upload_mcp-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for gs1belu_mpm_upload_mcp-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 85e9b7fd7133a391f69e30b31c8deec441e6bcbf13543a6230e8447c261789d2
MD5 348df8721f3c248c96871fbed0441229
BLAKE2b-256 e5d40a367c45ea2de34cb17164e236d8d1e199a1eb3b9d2531e7958021a3b04c

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page