Skip to main content
Glama
kartparash-cmd

Korral StoreLink MCP

Korral StoreLink MCP

An MCP server that gives an AI grocery replenishment agent scoped access to StoreLink, Korral's internal stock-tracking API.

The intended user of this server is not a person — it is a replenishment agent operated by a category buyer. The buyer asks things like "how's the Madeta butter doing at Brno?"; the agent resolves the product, reads the stock position across stores, and — only with the buyer's explicit approval — submits a replenishment order.

The server exposes exactly four tools: three reads and one append-only write. StoreLink itself is stubbed in memory behind a StoreLinkClient interface, so a real HTTP client can be dropped in later without touching src/server.ts. It speaks stdio for local and desktop clients and streamable HTTP for the container that runs inside Korral's GCP — see DEPLOYMENT.md.

Install

npm install
cp keys.example.json keys.json    # per-store API keys

Requires Node.js 18 or newer.

Without keys.json, the server still starts and still answers — every store simply reports No credentials for store <id>. Ask Korral IT for access. A missing or malformed key file is never a crash.

Related MCP server: Korral StoreLink MCP Server

Build

npm run build      # tsc -> dist/

Run

Two transports, two entrypoints, one set of tools (src/server.ts exports a createServer() factory used by both).

npm run dev         # tsx src/stdio.ts   - stdio, for desktop MCP clients
npm start           # node dist/stdio.js

npm run dev:http    # tsx src/http.ts    - streamable HTTP, what runs in the container
npm run start:http  # node dist/http.js

stdio is the local/desktop path. It writes nothing to stdout except protocol frames; all logging goes to stderr or to log/. Seeing only korral-storelink MCP server running on stdio on stderr is correct - it is waiting for a client.

Streamable HTTP is the deployed path: stateless, POST /mcp plus GET /healthz, listening on PORT (default 8080). See DEPLOYMENT.md.

Smoke test either one:

# stdio
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' | npm run dev --silent

# http
npm run dev:http &
curl -X POST localhost:8080/mcp -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'

Wiring it into an MCP client

Add the server to your client's MCP config (Claude Desktop's claude_desktop_config.json, .mcp.json for Claude Code, or the equivalent):

{
  "mcpServers": {
    "korral-storelink": {
      "command": "node",
      "args": ["/absolute/path/to/korral-storelink-mcp/dist/stdio.js"]
    }
  }
}

To run from source without building, point it at tsx instead:

{
  "mcpServers": {
    "korral-storelink": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/korral-storelink-mcp/src/stdio.ts"]
    }
  }
}

Tools

The expected call order is lookup_skuget_stock_positioncreate_replenishment_orderget_replenishment_status.

Tool

Kind

Inputs

Returns

lookup_sku

read

query

matches: [{ sku, name, category, unit }] — case-insensitive substring on name, exact/prefix on code. No matches is an empty list plus a hint, not an error.

get_stock_position

read

sku, store_ids[]

One entry per store: { store_id, on_hand, units_sold_last_24h, gap, as_of }. gap is computed by the server as units_sold_last_24h - on_hand and always returned alongside the raw numbers. Unknown store → per-store error entry; SKU not ranged at a store → per-store error entry (not a typo, not retryable); unknown SKU → whole call errors.

create_replenishment_order

write

store_id, sku, quantity, reason

{ order_id, status, confirmation }. order_id is RPL-<store>-<seq>, status starts at submitted, confirmation is a paste-into-Slack sentence. reason is required and validated.

get_replenishment_status

read

store_id, order_id

The order record with its current status. store_id is a scope check — a mismatch returns not-found, never another store's order.

create_replenishment_order is the only tool with real-world effect: a submitted order feeds Korral's distribution run, so stock gets picked and a truck gets loaded. The tool description tells the agent to confirm store, SKU and quantity with the human first, never to infer a quantity, and to cite live stock numbers in reason.

gap semantics

gap = units_sold_last_24h - on_hand.

  • gap > 0 — the store sold more in 24h than it currently has on the shelf. This is the replenishment signal.

  • gap = 0 — knife edge, not "covered": the shelf holds exactly one more day at yesterday's rate, so the store runs empty in roughly 24 hours. A watch item, never reported as fine.

  • gap < 0 — stock exceeds the last 24h of sales by |gap|. No shortfall indicated.

It is a 24-hour arithmetic signal, not a forecast: no safety stock, no shelf capacity, no promotion or seasonality adjustment, no lead time. It is evidence for a human decision, not an order quantity.

reason validation

reason is rejected when it is empty or whitespace only, contains no letters (a bare number), is shorter than 12 characters, is fewer than 3 words, or matches a placeholder list (n/a, -, test, asap, tbd, …). It is stored on the order as the audit trail a human reads weeks later.

Seeded demo data

The in-memory stub seeds 9 SKUs across 5 stores.

Stores

ID

Name

47

Korral Brno-Kralovo Pole

63

Korral Brno-Lesna

102

Korral Praha-Vinohrady

111

Korral Ostrava-Poruba

128

Korral Olomouc-Nova Ulice

SKUs

SKU

Name

Category

Unit

8847291

Madeta butter 250g

Dairy

each

8847315

Semi-skimmed milk 1L

Dairy

each

8847402

Cheddar block 400g

Dairy

each

6620118

Sourdough loaf 800g

Bakery

each

6620174

Wholemeal rolls x6

Bakery

each

4410093

Free-range eggs x6

Produce

each

4410220

Bananas loose

Produce

kg

4410388

Vine tomatoes 500g

Produce

each

2205617

Ground coffee 500g

Ambient

each

The hero case — SKU 8847291, Madeta butter 250g

Store

on_hand

units_sold_last_24h

gap

Reading

47

3

11

+8

Selling far faster than stock covers — replenish

63

14

12

−2

Covered

102

9

5

−4

Comfortably covered

111

6

9

+3

Mild gap, worth watching

128

7

7

0

Knife edge — one more day at yesterday's rate, empties in ~24h

The demo flow: ask about "butter" → lookup_sku resolves 8847291get_stock_position across ["47","102","111"] shows the +8 at store 47 → the buyer approves 24 units → create_replenishment_order returns RPL-47-001get_replenishment_status reads it back.

Orders live in process memory only: an order created in a session is readable back in that same session and nowhere else.

Per-store authentication

StoreLink issues one API key per store, valid only for that store. Keys live in keys.json at the project root, mapping store id to key:

{ "47": "slk_47_...", "102": "slk_102_...", "111": "slk_111_..." }

src/keys.ts re-reads that file on every key lookup — there is no cache. An operator rotating a key externally is picked up by the very next tool call, with nothing to invalidate and no restart. In production the same StoreKeyProvider interface is backed by GCP Secret Manager reading versions/latest, which behaves identically (sketch at the bottom of src/keys.ts).

What the buyer sees:

Situation

Result

Key valid

Normal response

Key rejected (401), reload picks up a rotated key

Call succeeds — the buyer never knows it wobbled

Key rejected (401), reload does not help

Authentication failed for store <id> after key reload. If it still fails, retry shortly; if it keeps failing, contact Korral IT.

No key configured for the store

No credentials for store <id>. Ask Korral IT for access.

The retry runs once, and only on a 401. Everything else — unknown SKU, not ranged, transport failure — passes straight through, because create_replenishment_order is not idempotent and a silent second attempt would load a second truck.

Auth failures degrade per store: asking for stock across ["47","102","128"] returns real numbers for 47 and 102 alongside a credentials error for 128. Only when every requested store fails does the call come back as an error, so the agent can never read a wall of auth failures as "no gaps found".

keys.example.json ships a deliberately stale key for store 63 and omits store 128, so both failure paths can be demonstrated without editing code. The keys StoreLink accepts live separately in SEED_ACCEPTED_KEYS in src/storelink.ts — that separation is what makes a mismatch expressible at all. Both sets are fabricated fixtures for an in-memory stub; they authenticate nothing real. keys.json itself is gitignored.

Key material never leaves src/keys.ts. Not in an error message, not in a log line, not in a stack. Errors name the store and nothing else. JSON parse failures deliberately discard the parser's message, because syntax errors can quote surrounding text and in that file the surrounding text is credentials.

Observability

Two outputs under log/ (gitignored), for two different readers:

log/trace.jsonl — one JSON line per tool call, for FDEs. Every call, read or write, success or failure:

{"ts":"...","request_id":"req_9b0ab68b...","tool":"create_replenishment_order","store_id":"47",
 "status":"ok","error":null,"duration_ms":1,"args":{"store_id":"47","sku":"8847291","quantity":24,"reason":"..."}}

store_id is always string|null rather than sometimes an array, so the column has one type on every line; multi-store calls keep their full store_ids inside args.

log/audit.log — one line per write action, for the category buyer. Always carries the reason:

2026-08-13T12:49:20.918Z CREATED RPL-47-001 store=47 sku=8847291 qty=24 req=req_9b0ab68b... reason="Gap of 8 units at store 47 over 24h: on_hand 3 vs 11 sold. Buyer approved 24."
2026-08-13T12:49:20.919Z REJECTED - store=47 sku=8847291 qty=24 req=req_d419e212... detail="...placeholder text..." reason="n/a"

Three outcomes, and the distinction is load-bearing: CREATED (order reached StoreLink), REJECTED (refused before submission — nothing was written, guaranteed), and UNCERTAIN (the call failed in a way that cannot tell "never landed" from "landed but the response was lost"). Recording an UNCERTAIN as REJECTED would make the trail lie in the expensive direction.

The two files join on request_id: hand an FDE one audit line and they can grep straight to the full argument trace.

Redaction applies to both. Argument keys matching api_key|token|secret|password|auth|bearer|credential|cookie|session_id|signature|private_key are replaced wholesale — none of the four tools take such an argument today, so this is a tripwire for the day one is added. Credential-shaped values are stripped regardless of the key they arrive under (Bearer …, JWTs, sk-live-…, ghp_…, AKIA…, Slack tokens), which matters most for reason, the only free-text field in the server:

reason="Buyer approved 5; ops pasted token [REDACTED] by mistake"

Logging never writes to stdout (that belongs to the JSON-RPC transport) and never throws — a full disk degrades to a one-time stderr warning rather than failing a replenishment order. Set KORRAL_LOG_DIR to relocate the directory; paths resolve from the module, not process.cwd(), because MCP clients spawn the server with an arbitrary working directory.

Not supported

Deliberate boundaries, enforced in the types and stated in every tool description:

  • No supplier or vendor data. No supplier names, lead times, order minimums, supplier costs, or purchase orders to vendors. A replenishment order here moves Korral's own stock; it does not raise anything with a supplier.

  • No POS or transaction-level data. The only sales figure anywhere is the aggregate units_sold_last_24h. No baskets, receipts, hourly breakdowns, or per-transaction detail.

  • No pricing, cost or margin fields on any type.

  • No cancel, delete or amend. Orders are append-only. There is no tool to stop, change, reschedule or expedite an order once submitted — the buyer must contact Korral ops directly.

  • No history or trend. Only the current snapshot and the trailing 24 hours. No week-over-week comparison, no forecast.

  • No browse or list. Orders can only be read back by exact order_id scoped to their store. There is no search or list-orders capability.

  • No delivery ETA. The server does not know when the truck arrives.

Design decisions

Task-shaped tools, not a REST mirror

The four tools match the four things a category buyer actually does — name a product, check its cover, order more, confirm it landed — rather than mirroring StoreLink's endpoints, because a model handed GET /inventory plus GET /stores has to invent the workflow on every call and will eventually invent it wrong.

The seam costs one indirection and buys the ability to develop, test and demo the whole server with no network and no credentials, then swap in the real HTTP client without touching a line of server.ts.

What is deliberately not exposed, and why

Supplier data, POS transaction detail, pricing and margin are absent from the types, not just the tools, because a replenishment agent needs none of them to decide whether store 47 runs out of butter — and every field that exists is a field the model can leak, misread, or promise the buyer.

Why the server computes gap and still returns the raw numbers

The arithmetic lives on the server so it is identical on every call and cannot drift with the model's mood, and on_hand and units_sold_last_24h come back alongside it so the buyer can audit the number rather than trust it.

The definition of gap — needs Korral's sign-off

gap = units_sold_last_24h - on_hand is a deliberately crude proxy for "will this run out", chosen because it is explainable to a buyer in one sentence; it ignores delivery in flight, day-of-week seasonality and shelf capacity, and the definition should be confirmed with Korral before day 1 because every downstream order quantity inherits it.

Why there is exactly one write tool

One write path means one place where validation, confirmation, auditing and authentication have to be right, and a buyer reading the tool list can see the entire blast radius of the agent in a single line.

Why reason is required on that write

reason is the audit trail a human reads weeks later when the pallets are already on the shelf, and requiring it in the same call that commits the order forces the model to articulate its justification before acting rather than reconstruct one afterwards.

Why there is no cancel or delete tool

Append-only was chosen over fuller CRUD because an agent that can undo its own writes will eventually undo the wrong one, and "you cannot cancel this here, contact ops" is a boundary a model can state honestly to a buyer.

Two logs because there are two readers

trace.jsonl answers an FDE's question — what exactly did the agent send, and what came back — while audit.log answers the buyer's — who ordered what, and why; one file serving both would be unreadable to one of them and unparseable to the other.

Why rejected and uncertain writes are audited too

An audit trail that records only successes quietly implies nothing else was attempted, so refusals are logged as REJECTED and the "we cannot tell whether it landed" case is logged as UNCERTAIN rather than being flattened into either outcome.

Key rotation: re-read, retry once, then escalate

Keys are re-read on every lookup so Korral IT's weekly rotation lands without a redeploy; a rejected key is reloaded and retried exactly once so a rotation mid-call recovers invisibly, and a second rejection stops trying and tells the buyer to contact Korral IT — because at that point it is an access problem, not something retrying will fix.

Why a 401 is retried once, and only a 401

One retry recovers a stale key, and confining it to 401 keeps the non-idempotent write tool from ever being silently repeated for a failure that might have already loaded a truck.

LLM conversation is data, and it must not leave the tenancy

Stock positions and replenishment reasons cross the model boundary on every call, so where the LLM runs is the first thing to settle with Korral's security team — every boundary this server enforces is void if the conversation itself is processed, retained or trained on outside their perimeter.

storelink.ts gains an HTTP implementation of the same interface and keys.ts swaps FileKeyProvider for the Secret Manager one; server.ts, logger.ts and auth.ts do not move, and the first thing to break will be the assumption that orders survive in memory across restarts and replicas.

Available Tools

4 tools
create_replenishment_orderCreate replenishment orderA

Submit a replenishment order for one SKU to one Korral store in StoreLink. This is the ONLY write tool on this server, and it has real-world effect: a submitted order feeds Korral's internal distribution run — stock gets picked and a truck gets loaded against it. Treat every call as physically consequential.

CONFIRM BEFORE YOU CALL Get explicit human approval from the category buyer for this exact combination of store, SKU and quantity before calling. Never infer a quantity the buyer did not state or approve, never round a number up "to be safe", and never batch several stores by calling this repeatedly without confirming each one. If the buyer's instruction is ambiguous about quantity or store, ask — do not call and then report what you did.

NO IDEMPOTENCY — A RETRY LOADS A SECOND TRUCK Every call issues a NEW order_id. There is no de-duplication key, no idempotency token, and no way to ask this server whether an equivalent order already exists — there is no list or search, and get_replenishment_status only works on an order_id you already hold. Calling twice with identical store, SKU, quantity and reason creates two orders and two physical picks. If a call fails, times out, or you cannot tell whether it landed, do NOT call again: tell the buyer exactly what arguments you sent, say you cannot confirm the outcome, and let them check with Korral ops. Retry ONLY when the tool returned an explicit error stating nothing was submitted.

THE reason FIELD IS NOT A FORMALITY reason is required and is stored on the order as the audit trail a human will read weeks later. It must be a specific, human-meaningful sentence citing the actual stock evidence you just read from get_stock_position — the store, the numbers and the gap. Good: "Gap of 8 units at store 47 over 24h: on_hand 3 vs 11 sold, buyer approved 24 units." Rejected: whitespace only, a bare number, or placeholder text such as "n/a", "-", "test", "asap". If you have not called get_stock_position for this store and SKU in this conversation, you do not have the evidence to write a valid reason — go and get it.

reason is also written to the buyer's audit log, verbatim, whether the order is accepted or refused. Do not paste credentials, tokens or anything secret into it.

WHAT IT RETURNS

  • order_id Readable, stable identifier, format RPL--, e.g. "RPL-47-001". Keep it — it is the only handle for reading the order back, and you must give it to the buyer.

  • status Lifecycle state, always "submitted" at creation.

  • confirmation One-line human-readable sentence the buyer can paste straight into Slack. Surface it verbatim.

ACCESS ERRORS — nothing was written Every error that opens "Rejected before submission — NO order was created" is a guarantee: no stock moved, and retrying after fixing the cause is safe. That includes the two credential failures, "No credentials for store " and "Authentication failed for store after key reload". Both are access problems, not stock problems: do NOT reroute the order to a store you can reach, do not lower the quantity, and do not tell the buyer the order was placed. Report the message verbatim and let them take it to Korral IT.

WHEN TO USE IT Step 3 of the standard workflow: lookup_sku -> get_stock_position -> create_replenishment_order -> get_replenishment_status. Use it only after a positive gap (or an explicit buyer instruction) has been established with live numbers and the buyer has approved the quantity. Then hand back the order_id and confirmation line.

WHEN NOT TO USE IT — AND WHAT IT CANNOT UNDO Orders here are APPEND-ONLY. This server has no cancel, no delete, no amend, no quantity change and no reschedule — none of the other three tools can do it either, and there is no hidden path to it. If the buyer wants an order stopped or changed after submission, say plainly that this server cannot do it and they must contact Korral ops directly; do not create a compensating or negative order to "fix" it. It also raises nothing with suppliers: it does not create a purchase order to a vendor, does not know lead times, order minimums, costs, price or margin, and will not tell you when the truck arrives. It also refuses a SKU that is not ranged at the target store — that store carries no inventory record for it. That is a merchandising fact, not a retryable error: report it, and do not substitute a different store or a similar SKU to get the call through. Never call it speculatively, to test the tool, or to "see what happens".

ParametersJSON Schema
NameRequiredDescriptionDefault
skuYesExact Korral SKU code to replenish, e.g. "8847291", as resolved by lookup_sku. One order covers one SKU only.
reasonYesRequired human-meaningful justification citing the stock evidence, e.g. "Gap of 8 units at store 47 over 24h: on_hand 3 vs 11 sold in 24h". Whitespace-only, bare numbers, and placeholders like "n/a", "-" or "test" are rejected.
quantityYesWhole number to send, 1-10000, counted in the SKU's `unit` from lookup_sku: pieces when unit is "each", KILOGRAMS when unit is "kg". Fractional quantities are rejected outright, including for kg SKUs. If the buyer approves a fractional weight (e.g. "12.5 kg of bananas"), do not round it yourself in either direction — tell them this system accepts whole kilograms only and ask which whole number they want. Must be a number the buyer explicitly stated or agreed to: never inferred, never rounded to a case or pallet size (this server has no case-size data, so any such rounding is invention), never copied from `gap` without approval.
store_idYesKorral store ID the stock is being sent to, e.g. "47". Must be a store you have just checked with get_stock_position; one order covers one store only.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes far beyond the annotations. It discloses that every call issues a new order_id with no idempotency token, that retries load a second truck, that orders are append-only with no cancel/delete/amend, and that access errors guarantee nothing was written. It also explains the real-world physical consequence of a submitted order. This is exceptional behavioral disclosure, far exceeding the minimal hint from annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long and detailed, but it is very well-structured with uppercase section headers, bullet points, and a clear flow from warning to returns to usage rules. Some redundancy exists (e.g., 'never infer a quantity' appears multiple times, no-idempotency is emphasized in both a section and later limitations), which prevents a 5. However, given the high-stakes nature of the tool, the length is largely justified.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description is remarkably complete. It covers return values (order_id format, status, confirmation line), error semantics (rejected-before-submission guarantee, credential failures), edge cases (SKU not ranged), and integration with sibling tools. It even advises on how to handle ambiguous buyer instructions and failed calls. No output schema exists, so this detailed textual description fully compensates.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Although schema coverage is 100%, the description adds critical safety context. For quantity, it clarifies that the number must be explicitly approved by the buyer, never inferred or rounded to case/pallet size. For reason, it explains the audit-trail function and requires evidence from get_stock_position. For store_id, it adds the constraint that it must be a store just checked and warns against rerouting on auth errors. These enrich the schema meaning substantially.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a precise verb and resource: 'Submit a replenishment order for one SKU to one Korral store in StoreLink.' It explicitly labels itself as the 'ONLY write tool on this server,' instantly distinguishing it from its read-only siblings. The scope (one SKU, one store) is also stated up front.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description contains dedicated 'WHEN TO USE IT' and 'WHEN NOT TO USE IT' sections. It positions the tool as step 3 in a named workflow (lookup_sku -> get_stock_position -> create_replenishment_order -> get_replenishment_status), specifies that it should only be used after a positive gap and buyer approval, and explains what the tool cannot do (cancel, amend, create purchase orders). This is explicit, actionable guidance with alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_replenishment_statusGet replenishment statusA
Read-only

Read back a single replenishment order that was created through this server, scoped to its store. Read-only.

WHAT IT RETURNS The order record: order_id, store_id, sku, quantity, reason, current status, and its creation timestamp. status reflects where the order stands in StoreLink's internal flow; it begins at "submitted" on creation and advances from there. Report the status and the timestamp together — a status you read five minutes ago may have moved.

WHEN TO USE IT Step 4 and the final step of the standard workflow: lookup_sku -> get_stock_position -> create_replenishment_order -> get_replenishment_status. Use it when the buyer asks "did that order go through?" or "where is RPL-47-001?", and to confirm a write landed after create_replenishment_order returned. You need the order_id from the creation call — there is no browse, search or list-orders capability here, so an order_id you were never given cannot be recovered from this server.

SCOPE CHECK Both store_id and order_id are required and must match the same order. A valid order_id paired with the wrong store_id returns a not-found style error — it will never leak another store's order. If you get not-found, re-check the order_id and the store it was created for before telling the buyer the order is missing.

ACCESS ERRORS ARE NOT "NOT FOUND" "No credentials for store " and "Authentication failed for store after key reload" mean this server could not ask the question — NOT that the order is missing. The order may well exist and be on its way. Never report an access error to the buyer as a missing or failed order; give them the message so they can raise it with Korral IT.

WHEN NOT TO USE IT Reading an order does not change it, and nothing here can: there is no cancel, delete, amend or expedite on this server, and status is not writable through any tool. If the buyer wants the order stopped or altered, tell them this server cannot do it and they must contact Korral ops. It also shows no supplier or vendor detail, no lead time or delivery ETA, no cost, price or margin, and no POS data. For current shelf stock rather than order state, call get_stock_position instead.

ParametersJSON Schema
NameRequiredDescriptionDefault
order_idYesOrder identifier returned by create_replenishment_order, format RPL-<store>-<seq>, e.g. "RPL-47-001". Orders cannot be searched or listed — without this exact ID the order is unreachable.
store_idYesKorral store ID the order was created for, e.g. "47". Acts as a scope check: it must match the order, or the lookup returns not-found rather than another store's order.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare readOnlyHint and openWorldHint; the description adds substantial behavioral context: status lifecycle, the requirement to pair order_id from creation, the inseparable store scope, not-found semantics vs. access errors, and the impossibility of mutation. This goes far beyond the annotations and contains no contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long but tightly organized with clear section headers (WHAT IT RETURNS, WHEN TO USE IT, SCOPE CHECK, ACCESS ERRORS, WHEN NOT TO USE IT). Every sentence carries operational significance—return fields, status-timestamp pairing, error disambiguation, and alternative tools. Nothing is redundant.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is complex due to order-state semantics, store scoping, and error-prone access failures. With no output schema, the description fully covers return content, usage context, scope assertions, error interpretation, and non-mutability. It is self-sufficient for an agent to select and invoke correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and both parameters already carry detailed descriptions. The tool description adds meaning by explaining the dependency on a prior create_replenishment_order call, the lack of search/recovery, and the strict pairing requirement. This is a modest increment over the schema, so 4 rather than 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb ('Read back') and resource ('single replenishment order') scoped to its store. It clearly distinguishes itself from siblings by stating there is no browse/search/list capability and explicitly contrasts with get_stock_position for current shelf stock.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit workflow positioning ('Step 4 and the final step of the standard workflow'), concrete buyer-intent triggers ('did that order go through?'), and a dedicated 'WHEN NOT TO USE IT' section naming alternatives (e.g., get_stock_position) and exclusions (no cancel/delete/amend).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_stock_positionGet stock positionA
Read-only

Read the current StoreLink inventory snapshot for one SKU across one or more Korral stores, and compute the replenishment gap. Read-only.

WHAT IT RETURNS positions: one entry per requested store, each containing

  • store_id The store this row is for, e.g. "47".

  • on_hand Units physically in stock at the snapshot time.

  • units_sold_last_24h Aggregate units sold in the trailing 24 hours. Aggregate only — there are no baskets, receipts or timestamps behind it.

  • gap COMPUTED BY THIS SERVER as units_sold_last_24h - on_hand.

  • as_of ISO-8601 timestamp of the inventory snapshot the numbers came from. Stock moves continuously; quote this when you report numbers to the buyer.

All three raw numbers are always returned alongside gap so the buyer can check the arithmetic. Never re-derive gap differently, and never present gap without the on_hand and units_sold_last_24h it came from.

HOW TO READ gap gap > 0 The store sold more in the last 24h than it currently holds. This is the replenishment signal. Magnitude = exactly how many more units would have been needed to serve the last 24 hours out of today's shelf (on_hand 3, sold 11 -> gap +8 at store 47). gap = 0 KNIFE EDGE, not "covered": the shelf holds exactly one more day at yesterday's rate, so the store runs empty in roughly 24 hours (on_hand 7, sold 7 -> gap 0). Surface it to the buyer as a watch item; never report it as fine. gap < 0 Stock exceeds the last 24h of sales by |gap| (on_hand 9, sold 5 -> gap -4). No shortfall indicated. gap is 24-hour arithmetic, not a forecast and not an order quantity. It carries no safety stock, no shelf-capacity limit, no promotion, seasonality or day-of-week adjustment, and no supplier lead time. Never pass gap into create_replenishment_order's quantity as a default — quote it to the buyer as evidence and let them state the number.

WHEN TO USE IT Step 2 of the standard workflow: lookup_sku -> get_stock_position -> create_replenishment_order -> get_replenishment_status. Use it to answer "are we short on X?", to compare a SKU across a store list, and ALWAYS immediately before proposing an order, so the reason string on that order cites live numbers. Pass every store the buyer cares about in one call rather than looping.

ERRORS — five distinct failures needing different responses:

  • Unknown store_id, or a store this server has no credentials for -> a per-store entry carrying an "error" field; other stores still return normally. Check every entry before summarising; never total or average across entries that carry an error.

  • SKU valid but NOT RANGED at that store -> also a per-store "error" entry ("not ranged"). This is not a typo and not transient: that store carries no inventory record for the product, so it cannot be replenished there and the failure is not fixable by retrying or correcting the code. Report it to the buyer and do not call create_replenishment_order for that store.

  • "No credentials for store " -> that store has no API key configured here. It says NOTHING about the stock, which may be fine or may be empty. Never present it as a stock answer, never substitute a different store, and never guess. Pass the message to the buyer so they can raise access with Korral IT.

  • "Authentication failed for store after key reload" -> the key was already reloaded once and still rejected. Retrying immediately will not help; a short wait might, and if it persists it is a Korral IT matter. Again, this is not a stock answer.

  • Unknown sku -> the WHOLE call fails; the code is wrong. Call lookup_sku and retry with a sku copied verbatim from its matches. If EVERY requested store errors, the whole call is returned as an error too — that is a credentials or connectivity problem, so do not report it to the buyer as "no gaps found".

WHEN NOT TO USE IT It does not write anything and cannot reserve, hold or move stock. It has no supplier information (no vendor, lead time, order minimum or cost), no pricing, cost or margin, and no POS or transaction-level detail beyond the single aggregate units_sold_last_24h — no basket data, no hourly breakdown, no history beyond the current snapshot and trailing 24h. Do not offer the buyer a trend, a week-over-week comparison, or a delivery ETA from this tool.

ParametersJSON Schema
NameRequiredDescriptionDefault
skuYesExact Korral SKU code to check, e.g. "8847291". Must be a real code — resolve product names through lookup_sku first; a wrong code fails the entire call.
store_idsYesOne or more Korral store IDs to check, e.g. ["47", "102", "111"]. Pass all stores of interest in a single call; unknown IDs come back as per-store error entries rather than failing the others.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Despite readOnlyHint=true and openWorldHint=true annotations, the description goes far beyond by detailing the exact computation of gap, the knife-edge gap=0 interpretation, the five distinct error modes with appropriate responses, and explicit warnings like never passing gap into create_replenishment_order's quantity. It also discloses that units_sold_last_24h is an aggregate with no underlying baskets or timestamps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Although long, the description is impeccably structured with clear headings (WHAT IT RETURNS, HOW TO READ gap, WHEN TO USE IT, ERRORS, WHEN NOT TO USE IT). Every sentence earns its place, covering complex edge cases without redundancy, and the core purpose is front-loaded in the first line.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema and complex per-store error semantics, the description fully compensates by explaining return fields, gap interpretation, error types, and limitations. It also places the tool in the broader workflow, making it complete for an agent to use correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds marginal value by reinforcing the workflow context ('Pass every store the buyer cares about in one call rather than looping') and clarifying the per-store error behavior for store_ids, though most parameter details are already in the schema. The description's examples (e.g., store_id '47') and the call to resolve SKUs via lookup_sku add a little extra semantic color.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a precise verb+resource: 'Read the current StoreLink inventory snapshot for one SKU across one or more Korral stores, and compute the replenishment gap.' It clearly distinguishes itself from siblings by stating its role as the read-only gap calculator between lookup_sku and create_replenishment_order, and the workflow section reinforces this.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

A dedicated 'WHEN TO USE IT' section explicitly places the tool as Step 2 of the standard workflow, names the exact conditions for use (e.g., always before proposing an order, pass all stores at once), and 'WHEN NOT TO USE IT' lists exclusions like no supplier info, no trend analysis, and no ORDER quantity derivation. This is explicit when-to-use and when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

lookup_skuLook up SKUA
Read-only

Resolve a product name fragment or SKU code to Korral StoreLink SKU records. Read-only.

WHAT IT RETURNS matches: an array of { sku, name, category, unit }.

  • sku Korral's 7-digit internal SKU code, e.g. "8847291". This is the only identifier the other tools in this server accept.

  • name Buyer-facing product name, e.g. "Madeta butter 250g".

  • category Merchandising category, e.g. "Dairy", "Bakery", "Produce".

  • unit The unit stock and sales are counted in: "each" or "kg". All quantities elsewhere in this server (on_hand, units_sold_last_24h, gap, order quantity) are expressed in this unit.

Matching is case-insensitive substring on name, plus exact or prefix match on sku. A query with no matches returns an EMPTY matches array and a hint — it is not an error. Re-query with a shorter or more distinctive fragment ("butter", not "Madeta butter 250g salted").

WHEN TO USE IT This is step 1 of the standard workflow and normally the FIRST call you make: lookup_sku -> get_stock_position -> create_replenishment_order -> get_replenishment_status Use it whenever the buyer names a product in words rather than digits ("how's the Madeta butter doing?", "check the sourdough"), whenever you are unsure a SKU code is valid, and whenever get_stock_position returns an unknown-SKU error. Multiple matches mean the query is ambiguous — show the buyer the candidates with their categories and units and ask which one, rather than picking for them.

WHEN NOT TO USE IT It returns no stock levels, no sales figures and no order history — call get_stock_position for that. It does not say which stores range a SKU — only get_stock_position reveals that, per store. It exposes nothing about suppliers (no vendor names, lead times, order minimums or costs), nothing about price, cost or margin, and no transaction-level POS data. Do not tell the buyer this server can retrieve any of those; it cannot, and no other tool here can either.

ParametersJSON Schema
NameRequiredDescriptionDefault
queryYesA SKU code (exact or leading digits, e.g. "8847291" or "884") or a case-insensitive fragment of the product name (e.g. "butter", "sourdough"). Shorter fragments match more broadly; an empty result means try fewer words, not that the product is gone.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint and openWorldHint annotations, the description discloses matching behavior (case-insensitive substring, prefix/exact SKU match), empty-match behavior (returns empty array and hint, not an error), and re-query guidance. It also explicitly lists what is not included (stock, sales, order history, supplier info). No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but efficiently structured with distinct sections (WHAT IT RETURNS, WHEN TO USE IT, WHEN NOT TO USE IT). Every sentence provides actionable information, with front-loaded purpose and clear formatting. No fluff or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description carries the full burden of explaining return values. It details the matches array fields (sku, name, category, unit) with examples and explains the unit system. It also covers edge cases (no matches, ambiguous matches) and clearly states limitations relative to sibling tools. Complete for the tool's role.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already provides a detailed description of the query parameter, so baseline is 3. The description adds matching rules ('case-insensitive substring on name, plus exact or prefix match on sku') and clarifies the meaning of an empty result ('it is not an error'). This goes beyond the schema's parameter text, though schema coverage is already high (100%).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb+resource: 'Resolve a product name fragment or SKU code to Korral StoreLink SKU records.' It clearly distinguishes itself from siblings by positioning it as 'step 1 of the standard workflow' and contrasting with get_stock_position which handles stock levels.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The 'WHEN TO USE IT' and 'WHEN NOT TO USE IT' sections give explicit guidance: use as the first call, when buyers mention product names, or when get_stock_position returns an unknown-SKU error. It also explicitly names get_stock_position as the alternative for stock data, and explains what it cannot retrieve. This is exemplary usage guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

TDQS

A4.9/5.0
Disambiguation5/5

Each tool serves a distinct step in the replenishment workflow: lookup resolves names to SKUs, get_stock_position reads inventory and computes gaps, create_replenishment_order submits a write, and get_replenishment_status reads back an order. There is no functional overlap, and the workflow descriptions reinforce clear boundaries.

Naming Consistency5/5

All four tools follow a consistent verb_noun snake_case pattern: lookup_sku, get_stock_position, create_replenishment_order, get_replenishment_status. The verbs (lookup/get/create/get) accurately reflect the action, and the nouns are specific resources, making the naming predictable and readable.

Tool Count5/5

Four tools is well-scoped for a focused inventory replenishment server. Each tool covers a necessary step in the workflow without redundancy or bloat, fitting comfortably within the ideal 3-15 tool range.

Completeness4/5

The core workflow is fully covered: SKU resolution, stock reading, order creation, and order status retrieval. However, there are notable gaps around order lifecycle management—no cancel, amend, or list orders—which the descriptions explicitly acknowledge but still limit the server's ability to handle post-submission needs without external intervention.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • -
    license
    Not graded
    quality
    Not graded
    maintenance
    Enables AI agents to interact with Skulabs inventory management system through comprehensive tools for managing products, orders, customers, and analytics. Supports voice agents like Retell AI and desktop applications like Claude for natural language inventory operations.
  • F
    license
    A
    quality
    C
    maintenance
    Enables AI agents to interact with Korral's StoreLink system to check store inventory and sales data, and create replenishment orders to manage stock-outs.
    2
  • F
    license
    Not graded
    quality
    B
    maintenance
    MCP server that lets a Duvo agent talk to Korral's StoreLink API, enabling category buyers to offload daily stock checking, replenishment ordering, and order tracking tasks.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kartparash-cmd/korral-storelink-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server