Korral StoreLink MCP Server
README.md
# Korral StoreLink MCP Server
A standalone [Model Context Protocol](https://modelcontextprotocol.io) server, written in
TypeScript with the official [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk),
that exposes the **StoreLink** backend of the Korral grocery chain to an AI agent.
It ships **two collapsed, semantic tools** designed for a stock-out / replenishment workflow:
| Tool | Purpose |
| --- | --- |
| `get_store_inventory_and_sales(store_id, sku)` | One call returns on-hand inventory **and** trailing-24h POS sales, plus a pre-computed `demand_gap` and `stockout_risk`. |
| `create_replenishment_order(store_id, sku, quantity)` | Fires a mock `POST` to raise a replenishment (restock) order and returns the confirmation. |
The transport is **Stdio** — the server reads JSON-RPC from stdin and writes to stdout, which is
how MCP hosts (Claude Desktop, IDEs, agent runtimes) launch local servers.
## Quick start
```bash
npm install
npm test # vitest — unit tests for the tool logic
npm run build # esbuild — bundles a single production file to dist/index.js
npm start # runs dist/index.js over stdio
```
During development you can run the TypeScript source directly without building:
```bash
npm run dev # tsx src/index.ts
```
### Wiring it into an MCP host
```json
{
"mcpServers": {
"korral-storelink": {
"command": "node",
"args": ["E:/temp/duvo-mcp-assignment/dist/index.js"]
}
}
}
```
## Mock data
The mock backend (`src/mockData.ts`) tracks one SKU — **8847291, Madeta butter 250g** — across two
stores, deliberately seeded to show contrasting demand gaps:
| Store | On-hand | POS (last 24h) | Demand gap | Risk |
| ----- | ------- | -------------- | ---------- | -------- |
| 47 | 2 | 10 | **8** | high |
| 102 | 12 | 14 | **2** | moderate |
`demand_gap = units_sold_last_24h - on_hand`. Store 47 sold far more than it has on the shelf — a
clear restock candidate; Store 102 is keeping pace.
## Architectural choices & tradeoffs
### 1. Collapsed, semantic tools over a 1:1 API mirror
In the real StoreLink API, inventory and POS sales live behind **two separate endpoints**. A naïve
MCP server would expose them as two tools, forcing the agent to make two round-trips, hold both
payloads in context, and do the subtraction itself.
Instead, `get_store_inventory_and_sales` **collapses both endpoints into one payload** and
**pre-computes `demand_gap` and `stockout_risk`**. The benefits for an agent:
- **Fewer tokens / less context churn** — one tool result instead of two, with only the fields that
matter for the decision.
- **Fewer round-trips** — one tool call to assess a store/SKU instead of two-then-reason.
- **Less room for error** — the server, not the model, does the arithmetic and risk bucketing.
**Tradeoff:** the tool is opinionated and less general-purpose than raw endpoints. A different
consumer that wanted *only* inventory now over-fetches POS data, and the `stockout_risk` thresholds
(`gap >= 5` → high, `>= 1` → moderate) are baked into the server rather than chosen by the caller.
For an agent-first tool surface this is the right trade — we optimize for the agent's context window
and decision quality, not for maximal API flexibility.
### 2. Action tool returns a confirmation, not a raw HTTP echo
`create_replenishment_order` models a side-effecting `POST`. It returns a small, structured
confirmation (`order_id`, `status: "submitted"`, echoed inputs) rather than a verbose HTTP response,
so the agent gets just enough to confirm success and report back.
### 3. Schema validation with Zod
Tool inputs are validated with Zod schemas registered through the SDK. Each field carries a
`.describe()` string that the SDK surfaces in the JSON Schema sent to the model — so the agent learns
what `store_id`, `sku`, and `quantity` mean. `quantity` is constrained to a **positive integer** both
in the schema and defensively in the handler.
### 4. Pure functions + thin server wiring (testability)
The tool logic (`getStoreInventoryAndSales`, `createReplenishmentOrder`) is implemented as **pure,
exported functions**; `buildServer()` only wires them to the MCP transport. This keeps the
Vitest suite fast and transport-free — it tests behavior directly, with no stdio mocking.
### 5. esbuild → single bundled file
`npm run build` (see `build.mjs`) bundles everything — including the SDK and Zod — into one
self-contained, minified `dist/index.js` with a `#!/usr/bin/env node` shebang. An MCP host can run it
with a single `node dist/index.js` command, no `node_modules` required at the deploy target. Node
built-ins are left external since the runtime provides them.
**Tradeoff:** bundling dependencies makes the output larger (~330 KB) and pins them at build time, but
removes any runtime install step — the right call for a server meant to be dropped into a host config.
## Project layout
```
.
├── build.mjs # esbuild bundle script -> dist/index.js
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── src
│ ├── index.ts # MCP server: schemas, tools, stdio wiring
│ ├── index.test.ts # Vitest unit tests
│ └── mockData.ts # Mock StoreLink inventory + POS data
└── dist
└── index.js # Bundled production server (generated)
```
TDQS
A3.9/5.0
Scored across 2 tools
Disambiguation5/5
The two tools have clearly distinct purposes: one retrieves inventory and sales data, the other creates a replenishment order. There is no overlap or ambiguity.
Naming Consistency5/5
Both tools follow a consistent verb_noun pattern in snake_case: get_store_inventory_and_sales and create_replenishment_order.
Tool Count3/5
Only 2 tools is minimal, but acceptable for a narrowly scoped replenishment server. However, typical integrations might benefit from a few more tools.
Completeness2/5
The server covers the core check-and-create cycle but lacks essential operations like listing, updating, or canceling replenishment orders, which agents may need.
Maintenance
ActivityInactive
ResponsivenessNo issues