StoreLink MCP Server
Python MCP server that exposes the hypothetical StoreLink grocery stocking API as agent tools over stdio.
StoreLink is used by a European grocer with locations across Europe, primarily in Czechia (also Slovakia and Austria in the mock data).
Tools
Built for a category buyer workflow: check on-hand vs last-24h POS, apply the delta rule, then replenish.
| Tool | Description |
|---|---|
list_stores |
List stores |
get_store |
Store details |
get_on_hand_vs_pos |
Primary check: on-hand + POS + delta + product/case_pack |
create_replenishment |
Raise a replenishment order |
get_replenishment |
Order status |
get_on_hand_vs_pos defaults the POS window to the last 24 hours when since is omitted. It returns delta = units_sold − available and a product block (case_pack, lead_time_days, etc.).
Buyer rule: if delta >= 6, call create_replenishment (prefer multiples of product.case_pack).
Butter test scenario
| Field | Value |
|---|---|
| SKU | 8847291 — Madeta butter 250g |
| Store 47 | Praha Budějovická — available 4, ~12 sold / 24h → delta 8 → replenish |
| Store 102 | Brno Vaňkovka — available 18, ~5 sold / 24h → delta −13 → do not replenish |
Setup
Requires Python 3.10+.
# From the project root
uv sync
Or with pip:
pip install -e .
Configuration
| Variable | Default | Description |
|---|---|---|
STORELINK_MODE |
mock |
mock = in-process fixtures; http = call the real API |
STORELINK_BASE_URL |
https://storelink.com |
Base URL used in http mode |
STORELINK_API_KEY |
(unset) | Optional Bearer for non-store endpoints |
KORRAL_GCP_PROJECT |
(required in http) | GCP project where Korral stores weekly-rotated store keys |
KORRAL_SECRET_PREFIX |
korral-store- |
Secret id = {prefix}{store_id} (e.g. korral-store-47) |
STORELINK_AUDIT_LOG |
logs/storelink-audit.ndjson |
NDJSON audit log file path |
STORELINK_THREAD_ID |
(session id) | Optional agent/thread id for correlating a chat |
STORELINK_BUYER_LOG |
logs/buyer-activity.ndjson |
Category-buyer activity (JSON lines) |
STORELINK_BUYER_MD |
logs/buyer-activity.md |
Category-buyer activity (readable markdown) |
Every tool requires a reason string (why the agent is calling it). That value is written into the audit trail as ground-truth rationale.
Korral per-store API keys
Korral IT rotates a per-store API key weekly in GCP Secret Manager. Store-scoped StoreLink calls send:
X-Korral-Store-Key: <key>
Secret naming: projects/{KORRAL_GCP_PROJECT}/secrets/{KORRAL_SECRET_PREFIX}{store_id}/versions/latest Example for store 47: korral-store-47.
Auth behavior:
- Before any store-scoped request, the key is loaded (cached in memory).
- If the secret does not exist, the tool fails immediately with a clear message for the agent (no HTTP call):
No Korral API key configured for store … Ask Korral IT to provision the weekly-rotated key. - If StoreLink returns 401 (key rotated in flight), the cache is invalidated, the latest secret is fetched, and the request is retried once.
Store-scoped tools/endpoints: get_store, inventory/POS inside get_on_hand_vs_pos, create_replenishment, get_replenishment. list_stores / SKU-supplier lookups do not require a store key.
Use Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS or workload identity) so the MCP process can read Secret Manager.
Audit logging
All audit events are standardized JSON (one object per line / NDJSON), written to stderr and appended to STORELINK_AUDIT_LOG.
Common envelope:
{
"schema_version": "1.0",
"event_type": "tool_call_started",
"timestamp": "2026-08-01T17:00:00.123Z",
"session_id": "…",
"interaction_id": "…",
"thread_id": "…",
"service": "storelink-mcp",
"level": "info",
"payload": {
"tool": "get_on_hand_vs_pos",
"arguments": { "store_id": "47", "sku": "8847291", "reason": "…" },
"reason": "Check store 47 butter sell-through before replenish decision"
}
}
event_type |
When |
|---|---|
session_started / session_ended |
Process lifetime |
tool_call_started |
Tool invoked |
tool_call_succeeded |
Tool returned (includes duration_ms, result_summary) |
tool_call_failed |
Tool error (includes error, and traceback for unexpected failures) |
interaction_id is unique per tool invocation and shared across that call’s started/succeeded/failed events.
Category buyer activity feed
Alongside the engineer audit log, every agent action is also written in plain language for category buyers:
- Why the agent acted (from the required
reasonargument) - What happened (stock checked, order placed, etc.)
- Outcome (e.g. replenishment recommended when delta ≥ 6)
Files:
logs/buyer-activity.md— human-readable timelinelogs/buyer-activity.ndjson— same entries as structured JSON
MCP resource (not a tool — does not clutter the buyer agent loop):
buyer://activity— current-session feed in markdown
Example markdown entry:
### 2026-08-01T17:22:49Z — Checked on-hand vs recent sales for Madeta butter 250g at store 47
**Status:** Needs attention
**Why the agent did this:** Check butter delta at store 47 before replenish decision
**What happened:** In the last 24 hours, about 12 units sold; 4 units are still available on the shelf (gap/delta: 8).
**Outcome:** Sales are outpacing stock (delta 8 ≥ 6). A replenishment order is recommended.
Mock mode includes Czech/EU stores (plus scenario stores 47 and 102), metric SKUs including 8847291, inventory, POS history (CZK/EUR), and Central European suppliers.
Run
uv run storelink-mcp
# or
uv run python -m storelink_mcp.server
Cursor / Claude Desktop config
Add to your MCP settings (e.g. Cursor mcp.json):
{
"mcpServers": {
"storelink": {
"command": "/Users/aleccritser/.local/bin/storelink-mcp-cursor",
"env": {
"STORELINK_MODE": "mock"
}
}
}
}
Important: the project path contains a space (Duvo Task). Cursor splits the command field on spaces, so pointing command at .venv/bin/storelink-mcp under that folder fails with spawn /Users/aleccritser/Duvo ENOENT. Use the no-space launcher at ~/.local/bin/storelink-mcp-cursor (created by the project setup), or uv with the directory passed as a separate arg:
{
"mcpServers": {
"storelink": {
"command": "/Users/aleccritser/.local/bin/uv",
"args": ["run", "--directory", "/Users/aleccritser/Duvo Task", "storelink-mcp"],
"env": {
"STORELINK_MODE": "mock"
}
}
}
}
For live HTTP against a StoreLink deployment:
{
"mcpServers": {
"storelink": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/Duvo Task", "storelink-mcp"],
"env": {
"STORELINK_MODE": "http",
"STORELINK_BASE_URL": "https://storelink.com",
"STORELINK_API_KEY": "your-api-key"
}
}
}
}
Example agent flow
get_on_hand_vs_pos("47", "8847291", reason="…")→ readdeltaandproduct.case_pack- If
delta >= 6→create_replenishment(..., reason="delta>=6 …") - Optionally check store
102(delta should stay below 6 — no order) get_replenishment(..., reason="…")→ poll status