# Walmart APIs [![CI](https://github.com/alyiox/mcp-walmart-ads/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/alyiox/mcp-walmart-ads/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/mcp-walmart-ads.svg)](https://pypi.org/project/mcp-walmart-ads/) [![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) MCP server for three Walmart Inc. API families, behind one tool surface: | Platform | APIs | Auth | |---|---|---| | `walmart:ads` — [Walmart Connect](https://developer.walmart.com/advertising-partners) | Sponsored Products, Display | RSA-SHA256 signature + bearer token | | `walmart:marketplace` — [Walmart Marketplace](https://developer.walmart.com/home/us-mp) | 28 domains (orders, items, feeds, reports, …) | OAuth2 `client_credentials` | | `samsclub:ads` — [Sam's Club](https://developer.samsclub.com) | Sponsored Products | RSA-SHA256 signature + bearer token | Four tools over 31 apis and 424 operations — discovery, a generic proxy, and a downloader. An agent finds endpoints in the bundled OpenAPI specs and calls them; the server signs, acquires tokens, and builds headers. ``` walmart:ads:sponsored-products:SBAProfileUpdateV2 └─ retailer ─┘└ line ┘└─── api name ───┘└── operationId ──┘ └────────── platform ─────────┘ credentials attach here ``` An operation id alone resolves to a host and an auth model. A matching `:` suffix means the same surface for another retailer — 13 shared operation ids of 90. ## Requirements - Python 3.13+ - Credentials for whichever platforms you use. Configure only those — an absent platform is simply unconfigured, and discovery works with no credentials at all. - **Walmart Connect / Sam's Club** — consumer ID, RSA key pair, bearer token - **Walmart Marketplace** — client ID + secret, and the advertiser (seller profile) ids they serve ## Quick start Set up your config (see [Configuration](#configuration)), then run the server: ```bash # Run directly with uvx (no clone needed) npx -y @modelcontextprotocol/inspector@latest uvx mcp-walmart-ads ``` ```bash # Or run from source git clone https://github.com/alyiox/mcp-walmart-ads.git cd mcp-walmart-ads uv sync npx -y @modelcontextprotocol/inspector@latest uv run mcp-walmart-ads ``` ## Configuration `config.json` MUST live at `~/.config/mcp-walmart-ads/config.json`. The server reads it once at startup, so a corrected file REQUIRES a restart. > **Windows:** `~` maps to `%USERPROFILE%` (typically `C:\Users\`), making the full > path `%USERPROFILE%\.config\mcp-walmart-ads\config.json`. Create the directory and copy the example: ```bash # Unix-like (macOS, Linux, WSL, …) mkdir -p ~/.config/mcp-walmart-ads/keys/walmart-ads cp config.example.json ~/.config/mcp-walmart-ads/config.json ``` ```powershell # Windows (PowerShell) New-Item -ItemType Directory -Force "$env:USERPROFILE\.config\mcp-walmart-ads\keys\walmart-ads" Copy-Item config.example.json "$env:USERPROFILE\.config\mcp-walmart-ads\config.json" ``` ### Shape ``` platforms..regions.. = ``` `` is the two-segment prefix an api id starts with, so a config key is literally the value you pass as the `platform` tool parameter — nothing to translate. The auth block's shape follows the platform's auth model. There is exactly one shape per platform, so no discriminator field is needed. **Signature platforms** (`walmart:ads`, `samsclub:ads`): ```json { "platforms": { "walmart:ads": { "regions": { "us": { "production": { "consumer_id": "your-consumer-id", "private_key": "./keys/walmart-ads/us-prod.pem", "private_key_version": "1", "bearer_token": "your-bearer-token", "base_urls": { "sponsored-products": "https://developer.api.walmart.com/api-proxy/service/WPA/Api/v1", "display": "https://developer.api.walmart.com/api-proxy/service/display/api/v1" } } } } } } } ``` | Field | Notes | |---|---| | `consumer_id` | Partner Network consumer ID | | `private_key` | Path to the RSA private key (PEM). Relative paths resolve against the config directory | | `private_key_version` | Key version string (default `"1"`) | | `bearer_token` | OAuth bearer token | | `base_urls.` | One per api in the platform's discovery surface. Keys MAY be bare (`sponsored-products`) or fully qualified (`walmart:ads:sponsored-products`); extra keys are allowed for the auxiliary specs reached by raw method+path | Environment names are free-form here — Walmart may issue a tenant only `production`, or `production` and `staging`. **OAuth2 platform** (`walmart:marketplace`): ```json { "platforms": { "walmart:marketplace": { "regions": { "us": { "production": { "credentials": [ { "client_id": "your-client-id", "client_secret": "your-client-secret", "advertisers": [ { "id": 7060158, "partner_id": "10001234" }, { "id": 7060159 } ] } ] } } } } } } ``` `environment` MUST be `production` or `sandbox`; base URLs are fixed by the server and absent from the file. Advertiser ids nest under the credential that serves them, so a secret appears exactly once and a dangling advertiser reference is structurally impossible. `partner_id` is per-seller because two `payments` operations require it as `WM_PARTNER_ID`; an all-zero value reads as absent, since that is what a generated config writes for a seller without one, and `scripts/backfill_partner_ids.py` fills the gaps from Walmart. Regions are a namespace, not a route — every `walmart:marketplace` region reaches the same hosts. The level exists because advertiser ids are unique only within a region. ### Splitting the config A populated `walmart:marketplace` block runs to tens of kilobytes of credentials, 88% of the file here, and a stray comma while editing it takes down every platform: a parse failure precedes per-platform validation. Platforms MAY therefore live in drop-in files under `config.d/`, merged over the base: ``` ~/.config/mcp-walmart-ads/ ├── config.json # server-wide settings, and any platforms you like ├── config.d/ │ ├── walmart-marketplace.json # only a "platforms" object │ └── samsclub-ads.json └── keys/ ``` - A drop-in MUST declare only `platforms`; server-wide settings stay in `config.json`. - A platform declared in two files is an **error naming both** — never silent precedence. - Only `*.json` directly in `config.d/` is read, so `.bak` and editor swap files are ignored. - A file that fails to parse costs **only its own platforms**; the rest keep working. - Relative `private_key` paths resolve against `config.json`'s directory either way, so moving a platform into `config.d/` needs no path edits. - No `config.d/` directory means no change in behavior. A malformed block for one platform MUST NOT stop the others loading. Read `wmt://platforms` to see which loaded and what regions and environments they declare: one that failed has no regions, and reading one of its environments returns the loader's own message — which file, which fields, and that a fix needs a restart. ### Top-level options | Field | Default | Notes | |---|---|---| | `response_cache_ttl` | `3600` | Seconds a truncated body or download stays readable at its resource URI | | `truncate_threshold` | `2048` | Response bytes returned inline before truncating to a preview | | `spec_refresh` | `{"auto": true, "interval": 7}` | Background spec refresh. `auto` turns the sweep on or off; `interval` is days between sweeps, and MAY be fractional | ### Market → tenant (`wap-tenant-id`) Pass `tenant` on `call_endpoint` and `download_file` for non-US `walmart:ads` markets (`WMT_CA`, `WMT_MX`, `WBD_OD`, …). Omit it for US and for `walmart:marketplace`. ## Tools ### `list_endpoints` List operations across every api, with optional filters. | Parameter | Notes | |---|---| | `query` | Case-insensitive substring on operation id, path, or summary | | `api` | Limit to one api, e.g. `walmart:marketplace:order-management` | | `platform` | Limit to one platform — `walmart:ads`, `walmart:marketplace`, `samsclub:ads` (schema enum) | | `tag` | Filter by OpenAPI tag | | `method` | Filter by HTTP verb — `GET`, `POST`, `PUT`, `PATCH`, `DELETE` (schema enum) | Returned operation ids are qualified (`api:operationId`) and pass straight to `describe_endpoint` or `call_endpoint`. ### `describe_endpoint` One operation plus every `components.schemas` entry reachable from it, so a request body can be built without the full spec. Server-managed auth and QoS headers are omitted. | Parameter | Notes | |---|---| | `operation_id` | Qualified (`api:operationId`), or bare when unambiguous | | `api` | Api to resolve a bare id in, e.g. `walmart:ads:sponsored-products` | ### `call_endpoint` Execute an authenticated request against any configured platform. Signing, token acquisition with per-credential caching and single-flight refresh, and one retry after a 401 all happen server-side. | Parameter | Notes | |---|---| | `region`, `environment` | Required. Src: config | | `operation_id` | Qualified or bare. Resolves api, platform, method, path, and required headers | | `api` | Required with raw `method` + `path`; otherwise inferred from `operation_id`. Accepts the two auxiliary `walmart:ads` specs | | `method`, `path` | Raw route, reaching alpha/beta/unpublished endpoints absent from the specs | | `path_params` | Values for `{placeholders}` in the path | | `params`, `body` | Query string and JSON body | | `file_path` | Send the file as `multipart/form-data` — Marketplace feed uploads. Pair with the `feedType` query parameter | | `advertiser_id` | **MUST be given on `walmart:marketplace`**, where it selects the credential. Optional on the ads platforms, where it is sent as `X-Advertiser-ID` | | `tenant` | WAP tenant for non-US `walmart:ads` regions | A response larger than `truncate_threshold` is previewed inline, with the full body at `wmt://responses/{request_id}` and a reproducible cURL at `wmt://curl/{request_id}` — bearer tokens, access tokens, and signatures replaced with placeholders. ### `download_file` Download a report, label, or snapshot from an authenticated endpoint. Give a full `url` (the `details` URL from a display snapshot poll, say), or `operation_id`, or `api` with `method` + `path`. `platform` is required only for a bare `url`. With `dest_path` the bytes are written there. Without it they are gunzipped when gzipped and cached, and the result carries `cached_at` — a binary payload with no `dest_path` asks for one instead. Redirects are followed, keeping auth headers on a relative or same-host `Location` and dropping credentials cross-host; the result includes `urls`, the hop path. ## Specs 33 OpenAPI documents ship in the wheel. A refresh writes updated copies to `~/.cache/mcp-walmart-ads/specs/`, which outranks the bundle on read. The bundle is never written at runtime, so it stays the floor a damaged or missing cache falls back to. ### Where they come from Walmart publishes no OpenAPI files, but each ReadMe reference page hydrates its HTML with the registry UUIDs of its documents, and `https://dash.readme.com/api/v1/api-registry/` serves the full spec unauthenticated. That covers Walmart Connect and all 28 Marketplace domains. Sam's Club publishes neither, so its spec is hand-authored from the developer docs: `scripts/build_samsclub_spec.py` regenerates a *candidate* from those docs, and the scheduled `spec drift` workflow opens a PR when they change, as a human review gate. The candidate is never shipped and never loaded at runtime. Documents are stored verbatim as upstream served them, so a refresh diff shows exactly what changed. Oversized inline examples and `x-readme` metadata are stripped on load rather than on disk, which keeps that reduction retunable without re-downloading anything. ### Keeping them current Specs refresh in the background: once at startup, then every `spec_refresh.interval` days (7 by default). Set `auto` to `false` to stop the sweep — the interval is remembered for whenever you turn it back on, and the manual refresh still works. To refresh now, having hit an endpoint the bundled spec does not have: ```bash # Installed with uvx (no clone) uvx mcp-walmart-ads --refresh # Or from a source checkout uv run mcp-walmart-ads --refresh ``` Either form sweeps every spec regardless of the interval and writes the same user cache, printing one row per document: `written`, `unchanged`, or `error`. There is deliberately no tool for this. A stale spec is indistinguishable from a current one from inside a session — it simply lacks an endpoint — so an agent asked to decide would either never refresh or refresh superstitiously after an unrelated failure. The trigger belongs outside the session. A document MUST yield at least one operation before it is installed, so an upstream answering `200` with an error body cannot poison the cache, and a byte-identical document is left alone. A cached file that will not load is discarded and the read falls back to the bundle. Servers coordinate through `spec-state.json` at the cache root, which records when each spec was last tried and holds a lease so one process sweeps at a time — every client session runs its own server process, and without it each would re-download all 33. ### Rebuilding the bundle A maintainer step, and not the same thing as `--refresh`: that updates your cache, this updates the copies that ship in the wheel. ```bash # Registry-sourced specs only, by default uv run python scripts/fetch_specs.py uv run python scripts/fetch_specs.py walmart:ads:sponsored-products walmart:marketplace:order-management # Regenerate the Sam's Club candidate spec for review uv run --group spec-build python scripts/build_samsclub_spec.py ``` ## MCP resources | Resource URI | Description | |---|---| | `wmt://platforms` | Every platform, its auth model, and the regions and environments it declares | | `wmt://platforms/{platform}/apis` | That platform's api ids | | `wmt://platforms/{platform}/apis/{name}` | One api: title, version, operation count, and its tags with a count each — the legal `list_endpoints(tag=…)` values | | `wmt://platforms/walmart:marketplace/regions/{region}/{environment}/advertisers` | Advertiser ids mapped to their Walmart Partner ID (`null` when unset) | | `wmt://platforms/{platform}/regions/{region}/{environment}/hosts` | Api ids mapped to the base URL a call reaches, or `*` for every api whose host the server owns | | `wmt://responses/{request_id}` | Full body of a truncated response or a cached download (in memory, TTL from config) | | `wmt://curl/{request_id}` | Reproducible cURL for a previous request, credentials replaced with placeholders | ## MCP host examples
Cursor Add to `.cursor/mcp.json`: ```json { "mcpServers": { "walmart": { "command": "uvx", "args": ["mcp-walmart-ads"] } } } ```
Claude Code Add to your Claude Code MCP config: ```json { "mcpServers": { "walmart": { "command": "uvx", "args": ["mcp-walmart-ads"] } } } ```
Codex ```toml [mcp_servers.walmart] command = "uvx" args = ["mcp-walmart-ads"] ```
OpenCode ```json { "$schema": "https://opencode.ai/config.json", "mcp": { "walmart": { "type": "local", "enabled": true, "command": ["uvx", "mcp-walmart-ads"] } } } ```
GitHub Copilot ```json { "inputs": [], "servers": { "walmart": { "type": "stdio", "command": "uvx", "args": ["mcp-walmart-ads"] } } } ```
## Development ```bash uv sync --group dev uv run ruff check src/ tests/ scripts/ uv run ruff format --check src/ tests/ scripts/ uv run pyright uv run pytest tests/ -v ``` ## Contributing Issues and pull requests are welcome. Keep changes focused; `ruff check`, `ruff format --check`, `pyright`, and `pytest` MUST all pass. ## License [LICENSE](MIT).