store-ops-mcp
Enables store operations including inventory and sales queries and automated replenishment ordering through natural language.
README
store-ops-mcp
A TypeScript Model Context Protocol server built on the
official SDK (@modelcontextprotocol/sdk). It exposes two store-operations tools backed by
in-memory mock data (no database required):
| Tool | Purpose |
|---|---|
get_store_inventory_and_sales |
Consolidated read — inventory levels and sales velocity for a store in one call, plus days-of-supply, low-stock flags, and reorder suggestions. |
create_replenishment_order |
Places a mock restock order for one or more SKUs, groups lines into purchase orders by supplier, and returns a costed confirmation. |
evaluate_replenishment |
Check-and-replenish workflow — for one SKU across N stores, compares on-hand vs. last 24h POS, computes the shortfall gap, and auto-raises an order at every store whose gap exceeds a threshold (default 6). |
Design choices & tradeoffs
Each choice optimizes for an agent doing a buyer's job well, not for a general-purpose API. The cost of each is stated plainly.
1. One combined tool instead of mirroring StoreLink's separate endpoints. Inventory and sales come back together, already compared.
- You get: the agent asks one question and gets an answer it can act on — fewer steps, less to misread, lower chance of a wrong subtraction.
- You give up: generality. Someone who wanted only raw inventory gets a bit more than they asked for.
2. The server does the reorder math, not the agent. The "reorder when the gap exceeds 6" rule lives in code.
- You get: the same correct, explainable decision every time — the model can't fumble the arithmetic.
- You give up: flexibility — the threshold is a sensible default in the server, not chosen per call (though it can be overridden).
3. Tools return a short confirmation, not the raw system response. An order returns an id, status, and totals.
- You get: the agent sees just enough to confirm success and report back.
- You give up: the full underlying response, which a power user might occasionally want.
4. A deliberately small toolset — and no destructive tools. No raw database access, no delete, no "edit anything" tool.
- You get: a surface that's safe to hand an autonomous agent and easy to reason about.
- You give up: the ability to do arbitrary operations through this server (by design).
5. Mock data instead of a live StoreLink connection.
- You get: anyone can clone and run it in seconds — no credentials, no network.
- You give up: real integration, which wasn't what this exercise was testing.
6. Keys read fresh on every request; missing keys fail safely.
- You get: weekly key rotation "just works" with no restart, and an unknown store gets a clear, safe refusal instead of a crash.
- You give up: a negligible re-read on each call.
7. Two plain-text log files — one for the buyer, one for engineers.
- You get: each reader gets a log written in their language, with zero extra infrastructure.
- You give up: a searchable dashboard out of the box (the structured log is ready to feed one later).
Setup
npm install
npm run build # compiles src/ -> dist/
Run
npm start # node dist/index.js (speaks MCP over stdio)
Docker / deployment
A production multi-stage Dockerfile builds a minimal, non-root image:
docker build -t store-ops-mcp:1.0.0 .
docker run -i --rm \
-e STORE_KEY_47=sk_live_xxx \
-v store-ops-logs:/var/log/store-ops \
store-ops-mcp:1.0.0
-i is required — the server speaks MCP over stdio. See DEPLOYMENT.md for running
entirely inside Korral's private cloud with full data residency (air-gap posture, secrets,
log volumes, Kubernetes manifest).
The server communicates over stdio, the standard transport for local MCP servers. It prints a banner to stderr (stdout is reserved for the JSON-RPC protocol stream).
Smoke test
A tiny MCP client is included that spawns the server, lists the tools, and calls both:
node scripts/smoke-test.mjs
Use it from an MCP client
Add it to a client's MCP config (e.g. Claude Desktop's claude_desktop_config.json):
{
"mcpServers": {
"store-ops": {
"command": "node",
"args": ["E:\\temp\\duvo\\dist\\index.js"]
}
}
}
Tool reference
get_store_inventory_and_sales
| Param | Type | Required | Description |
|---|---|---|---|
storeId |
string | yes | STORE-001 or STORE-002 |
category |
string | no | Filter, e.g. Dairy, Beverages, Household |
lowStockOnly |
boolean | no | Return only items at/below their reorder point |
Returns store totals plus a per-SKU breakdown with inventory, sales
(incl. revenue30d, daysOfSupply), and replenishment (suggested qty + supplier/lead time).
create_replenishment_order
| Param | Type | Required | Description |
|---|---|---|---|
storeId |
string | yes | Target store |
lines |
array | yes | [{ "sku": "SKU-1001", "quantity": 60 }, ...] |
notes |
string | no | Free-text note on the order |
The order is validated all-or-nothing (unknown SKUs reject the whole order), grouped into one
purchase order per supplier, costed at wholesale unitCost, and the affected products'
onOrder quantities are updated so subsequent inventory reads reflect the pending order.
evaluate_replenishment
| Param | Type | Required | Description |
|---|---|---|---|
sku |
string | yes | Product to evaluate, e.g. 8847291 |
storeIds |
string[] | yes | Stores to check, e.g. ["47", "102"] |
gapThreshold |
number | no | Order only when (last24h sales - on-hand) > threshold. Default 6 |
dryRun |
boolean | no | Evaluate/recommend without placing orders |
Decision rule: gap = unitsSoldLast24h - onHand. When gap > gapThreshold the store
is breached and an order for max(reorderQuantity, gap) units is raised; otherwise no action.
This is the logic behind the worked example:
SKU 8847291 (Madeta butter 250g) is running empty at stores 47 and 102. Check on-hand vs. last 24h of POS for both, and raise a replenishment order for any store where the gap exceeds 6 units.
node scripts/task-scenario.mjs
Result: Store 47 (on-hand 4, sold 18 → gap 14 > 6) → order RO-47-0001 for 48 units;
Store 102 (on-hand 5, sold 9 → gap 4 ≤ 6) → no action.
Credentials
Every store is gated by a per-store API key read from the environment. The variable name is
STORE_KEY_<STOREID> — the storeId upper-cased with non-alphanumerics collapsed to _:
| Store | Env var |
|---|---|
47 |
STORE_KEY_47 |
102 |
STORE_KEY_102 |
STORE-001 |
STORE_KEY_STORE_001 |
Behaviour (implemented in src/index.ts → validateStoreCredential):
- Fail safe — a missing or blank key never throws or crashes the server. Single-store tools
return an
isErrorresult (Access denied. Missing credential …); the multi-storeevaluate_replenishmentmarks just that storecredential_invalidand continues with the rest. The raw key is never logged or returned — only a short SHA-256 fingerprint is used internally. - Mid-flight changes — the variable is re-read on every call (never cached at startup), so
rotating or removing a key takes effect on the next request with no restart. A changed key is
detected via fingerprint and logged as
credential_rotated(audit + debug).
# example
export STORE_KEY_47=sk_live_xxx
export STORE_KEY_102=sk_live_yyy
node dist/index.js
Demonstrate the full lifecycle (missing → present → rotated → removed → blank):
node scripts/credential-test.mjs
Dual logging
Every tool call writes to two append-only logs (see src/logger.ts). The
location defaults to the process working directory; override with STORE_OPS_LOG_DIR.
buyer_audit.log— plain, simple English. One readable line per business event for a buyer/ops reader:[2026-06-30T19:09:55.382Z] Store 47 (Praha Vinohrady): Madeta butter 250g running low — 4 on hand vs 18 sold in last 24h (gap 14 over 6). Raised order RO-47-0001 for 48 unit(s). [2026-06-30T19:09:55.414Z] Store 102 (Brno Kralovo Pole): Madeta butter 250g stock OK — 5 on hand vs 9 sold in last 24h (gap 4 within threshold 6). No order needed.fde_debug.log— structured JSONL (one JSON object per line) with full technical detail for a Forward Deployed Engineer:{"ts":"2026-06-30T19:09:55.365Z","event":"replenishment_evaluation","storeId":"47","sku":"8847291","onHand":4,"unitsSoldLast24h":18,"gap":14,"gapThreshold":6,"breached":true,"action":"order_placed","orderId":"RO-47-0001","quantity":48}
stdout is never used for logging — it carries the MCP JSON-RPC stream.
Example traces are committed under samples/ (the live *.log files are
gitignored — they're generated artifacts and hold business data in production).
Mock data
Defined in src/data.ts: two stores, several SKUs each, three suppliers with
lead times. Edit that file to change the catalog.
Project layout
src/index.ts MCP server + tool definitions
src/data.ts mock stores / products / suppliers
scripts/smoke-test.mjs end-to-end client test
dist/ compiled output (after npm run build)
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。