Mcp Walmart Ads — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Mcp Walmart Ads (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<!-- mcp-name: io.github.alyiox/mcp-walmart-ads -->
MCP server for three Walmart Inc. API families, behind one tool surface:
| Platform | APIs | Auth |
|---|---|---|
walmart:ads — Walmart Connect | Sponsored Products, Display | RSA-SHA256 signature + bearer token |
walmart:marketplace — Walmart Marketplace | 28 domains (orders, items, feeds, reports, …) | OAuth2 client_credentials |
samsclub:ads — Sam's Club | 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 hereAn operation id alone resolves to a host and an auth model. A matching <line>:<name> suffix means the same surface for another retailer — 13 shared operation ids of 90.
is simply unconfigured, and discovery works with no credentials at all.
they serve
Set up your config (see Configuration), then run the server:
# Run directly with uvx (no clone needed)
npx -y @modelcontextprotocol/inspector@latest uvx mcp-walmart-ads# 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-adsconfig.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%(typicallyC:\Users\<you>), making the full path%USERPROFILE%\.config\mcp-walmart-ads\config.json.
Create the directory and copy the example:
# Unix-like (macOS, Linux, WSL, …)
mkdir -p ~/.config/mcp-walmart-ads/keys/walmart-ads
cp config.example.json ~/.config/mcp-walmart-ads/config.json# 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"platforms.<platform>.regions.<region>.<environment> = <auth block><platform> 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):
{
"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.<api> | 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):
{
"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.
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/platforms; server-wide settings stay in config.json.*.json directly in config.d/ is read, so .bak and editor swap files are ignored.private_key paths resolve against config.json's directory either way, somoving a platform into config.d/ needs no path edits.
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.
| 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 |
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.
list_endpointsList 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_endpointOne 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_endpointExecute 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_fileDownload 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.
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.
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/<uuid> 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.
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:
# Installed with uvx (no clone)
uvx mcp-walmart-ads --refresh
# Or from a source checkout
uv run mcp-walmart-ads --refreshEither 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.
A maintainer step, and not the same thing as --refresh: that updates your cache, this updates the copies that ship in the wheel.
# 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| 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 |
<details> <summary><b>Cursor</b></summary>
Add to .cursor/mcp.json:
{
"mcpServers": {
"walmart": {
"command": "uvx",
"args": ["mcp-walmart-ads"]
}
}
}</details>
<details> <summary><b>Claude Code</b></summary>
Add to your Claude Code MCP config:
{
"mcpServers": {
"walmart": {
"command": "uvx",
"args": ["mcp-walmart-ads"]
}
}
}</details>
<details> <summary><b>Codex</b></summary>
[mcp_servers.walmart]
command = "uvx"
args = ["mcp-walmart-ads"]</details>
<details> <summary><b>OpenCode</b></summary>
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"walmart": {
"type": "local",
"enabled": true,
"command": ["uvx", "mcp-walmart-ads"]
}
}
}</details>
<details> <summary><b>GitHub Copilot</b></summary>
{
"inputs": [],
"servers": {
"walmart": {
"type": "stdio",
"command": "uvx",
"args": ["mcp-walmart-ads"]
}
}
}</details>
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/ -vIssues and pull requests are welcome. Keep changes focused; ruff check, ruff format --check, pyright, and pytest MUST all pass.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.