korral-storelink-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@korral-storelink-mcpWill we run out of SKU 12345 before closing at store 678?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
korral-storelink-mcp
An MCP server that lets a Duvo agent do a Korral category buyer's detective work — checking on-hand vs. POS, judging
whether a store will be empty by afternoon, raising replenishment orders — against Korral's homegrown StoreLink
API. Built on mcp-onprem-starter: stdio transport, a validated
config, a closed error taxonomy, and a write gate.
StoreLink is not available to us yet, so the upstream is stubbed (src/mock/server.ts). The tool surface below —
not the mock — is the actual deliverable: it's what an agent sees on every turn and what a human reviews in an audit
log, and it is designed to work unchanged once a real UPSTREAM_BASE_URL is set.
Tool surface
5 tools, covering 8 StoreLink endpoints. The governing constraint is context: every tool definition costs tokens
on every turn, so each tool below earns its place against a specific buyer question — see
docs/decisions/ for the full reasoning and what was rejected.
The core design choice: rather than mirroring each REST endpoint 1:1, related endpoints are folded into
workflow-shaped tools that return an answer plus its evidence, instead of raw payloads the agent would have to
assemble and do arithmetic over itself. That arithmetic matters — a naive "on-hand ÷ recent sales rate" projection
run in-context produces a different stockout time on every call. Here it's computed once, deterministically, in
tested code (src/lib/depletion.ts), and returned as a reproducible number the agent relays
rather than derives.
Tool | Type | Buyer question it answers |
| read | Which stores can I see, what time is it there, when do they close? |
| read | Will this SKU run out today, and can a delivery beat it? |
| read | That projection looks wrong — what actually sold? |
| read | Did the order I raised go through / ship? |
| write | Raise it — dry-run preview by default |
Naming convention
korral_ + verb_noun, snake_case.
korral_, notstorelink_. StoreLink is the implementation Korral happens to run today — the thing most likely to be replaced. Korral is the durable business domain, and the one a buyer reading an audit log recognizes.Verb first, and every read starts
list_/check_/get_while the sole write startsraise_— a reviewer scanning a log spots the one tool that mutates state from its first word alone.raise_because it's the buyers' own word for the action, notcreate_.No abbreviations except
sku, which is genuine domain vocabulary.
Return shape
Every tool returns the same envelope (src/tools/shared.ts):
// success
{ "ok": true, "data": { /* tool-specific */ } }
// failure
{ "ok": false, "error": { "code": "STORE_NOT_AUTHORIZED", "message": "...", "remediation": "..." } }Two shape conventions apply throughout, deliberately:
Lists are bounded and self-describing. Anything list-shaped (
korral_list_recent_sales) returns{ items, truncated, count_returned, count_total_available }rather than an unbounded array — the agent knows to narrow its query instead of assuming it saw everything.Projections are never a bare number.
korral_check_stock_riskreturns a stockout window (earliest/latest), aconfidenceenum computed by explicit rules in code,data_quality_flags, and a plain-Englishcaveatstring — not a single confident timestamp. See ADR-0004.
1. korral_list_authorized_stores
In: none.
Out: per store —
store_id,name,timezone,local_time_now,opening_hours_today,closes_in_minutes; pluscount.Annotations:
readOnlyHint: true·destructiveHint: false·idempotentHint: true·openWorldHint: true"Empty by afternoon" is meaningless without local time and closing time. This is also how the agent discovers its scope — the response is exactly the deployment's keyring (see below), so it's honest whether that's one store or several.
2. korral_check_stock_risk — the headline tool
In:
sku(required),store_id(see scoping rules below),lookback_days(default 14, max 28).Out: identity (
product_name,category,supplier_name) · state (on_hand_units,on_hand_as_of) · velocity (units_per_hour_tradingas[low, high],basis,observation_days) · projection (projected_stockout_window,projected_stockout_confidence,will_stockout_before_close_today: true|false|unknown,units_short_by_close) · replenishment (supplier_lead_time_days,earliest_replenishment_arrival,arrival_beats_stockout) ·evidence(per-day units sold, ≤28 rows) ·data_quality_flags·caveat.Annotations:
readOnlyHint: true·destructiveHint: false·idempotentHint: true·openWorldHint: trueCollapses what would otherwise be 4 separate calls (inventory, POS, SKU, supplier) into one deterministic answer. The
evidenceblock is what lets a buyer check the projection without a second tool call — the whole reason this is one workflow tool instead of four thin ones.
3. korral_list_recent_sales — the escape hatch
In:
sku(required),store_id,since(default 24h ago, max 7 days back),limit(default 100, max 500).Out:
items: { timestamp, units, transaction_id }[],count_returned,count_total_available,window_start,window_end,truncated.Annotations:
readOnlyHint: true·destructiveHint: false·idempotentHint: true·openWorldHint: trueThe one question
check_stock_riskstructurally can't answer: transaction shape. Sixty units as one catering order vs. sixty separate baskets calls for opposite decisions, and any aggregate destroys that distinction. Also the audit path when a buyer disputes a number.
4. korral_get_order_status
In:
order_id(required),store_id.Out:
status,submitted_at,expected_arrival,sku,product_name,quantity_units.Annotations:
readOnlyHint: true·destructiveHint: false·idempotentHint: true·openWorldHint: trueCovers the cross-session follow-up ("did yesterday's order ship?") — also the state check that the write tool's own
WRITE_OUTCOME_UNKNOWNremediation tells a caller to perform after an ambiguous write.
5. korral_raise_replenishment_order — the only write
In:
sku(required),quantity_units(required),reason(required — the buyer's justification, lands in the audit log),store_id,confirm(default false),idempotency_key(optional, auto-derived).Annotations:
readOnlyHint: false·destructiveHint: false·idempotentHint: false·openWorldHint: truedestructiveHint: falseis deliberate, not an oversight: a replenishment order is additive — it creates a record, it doesn't overwrite or delete one. The hint means "may irreversibly destroy data," not "has real-world consequences."idempotentHint: falseis the honest default: called twice, it creates two orders. Our idempotency key is an in-process cache that doesn't survive a restart, so claiming otherwise would be a lie the annotation tells the agent. Revisit only if StoreLink itself starts deduplicating.
Dry-run by default (
confirm: false). The preview is built so a human can judge "should this order exist, at this size?" with no further tool call: the exact request payload, the resolvedproduct_name(nobody can vet a bare SKU code), the justification snapshot (on-hand, velocity, stockout window, lead time, anddays_of_cover_after_delivery— the single best sanity check, since 45 days of cover on a chilled product exposes an order-of-magnitude mistake that the raw quantity alone hides), a duplicate-open-order warning, and a quantity-sanity flag.confirm: trueis the agent asserting intent — not proof a human approved it. Unless Duvo's host application puts a real approval step in front of the confirmed call, this handshake is decorative. Said here plainly rather than left implicit. Two independent backstops hold regardless of what the agent asserts:ALLOW_WRITESmust be set at the deployment level (defaults tofalse), andMAX_ORDER_UNITSis a hard server-side ceiling noconfirmcan bypass.
Deliberately not exposed
Cut | Why |
| Metadata, not an answer — a lead time only matters against a projected stockout. Folded into |
| Cut for safety, not economy. A bare on-hand number is the most misleading fact in this domain — "300 units" reads as reassuring while omitting the sell rate that makes it meaningful. On-hand is always returned with velocity, never alone. |
| Would advertise stores this deployment holds no key for, inviting a doomed cross-store loop. Replaced by |
A generic | Defeats the write gate, the closed error union, and response bounding in one move, and hands an LLM arbitrary URL construction against an API whose URLs already misrepresent the real permission scope (see below). |
| Not in StoreLink's documented surface — we don't invent capability. Would be a genuinely destructive write needing its own gate design if added later. |
Store scoping: store_id and the keyring
StoreLink's paths are shaped /v1/stores/{store_id}/... as if any of ~180 stores were reachable, but each
X-Korral-Store-Key is scoped to exactly one. The URL shape misrepresents the real permission surface — this
server does not repeat that misrepresentation to the agent.
The server holds a keyring (store_id → key, see STORELINK_KEYRING below). store_id is a real tool
parameter, but it is validated against the keyring before any HTTP call leaves the process:
Keyring has one store →
store_idis optional; omitting it resolves to that store.Keyring has several →
store_idis required. There's no safe implicit default among multiple authorized stores — that's exactly where a silent-wrong-store bug would live.An unrecognized
store_id→STORE_NOT_AUTHORIZED, always a local rejection. A store ID this deployment holds no key for is never forwarded upstream, so a lax StoreLink endpoint can't be exploited to leak another store's data.
Full reasoning, including the rejected alternatives (forwarding store_id unchecked; one deployment per store) and
what to do if Korral later issues chain-wide keys, is in ADR-0003.
Both of Korral IT's two hard failure cases are tested end to end, not just asserted: a key rotating on
StoreLink's side while this server is still running the old one (AUTH_KEY_INVALID, never silently retried), and
the agent asking for a real store this deployment simply isn't authorized for (STORE_NOT_AUTHORIZED, rejected
locally before any request reaches StoreLink). See tests/key-rotation.test.ts and ADR-0009.
Related MCP server: korral-mcp
Seeing it work: a real scenario
docs/scenarios/butter-restock.md is the full request/response transcript
for a real buyer scenario, run against the actual server and the bundled StoreLink stub — not hand-written:
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.
It shows korral_check_stock_risk and korral_list_recent_sales called for both stores, the gap computed from
their real responses, and korral_raise_replenishment_order called (dry-run preview, then executed) only for the
store whose gap actually breaches the threshold — the other store's real numbers don't cross it, and no order is
raised for it. Regenerate with npm run scenario after any tool or mock change; see ADR-0007 for why this is a
generated artifact rather than prose.
Quick start
npm install
npm run devBoots the server over stdio against the bundled StoreLink stub — no configuration needed. Point an MCP client at it and call any of the 5 tools above.
To verify the whole deployable chain — build, container boot, a real protocol round trip, and a rollback drill:
just verifyTesting
npm run test # run the full suite once
npm run test:watch # re-run on file changes, for local development
npm run ci # typecheck + test + build — what CI runs, and what must pass before every commit69 tests across 8 files, all real: nothing here mocks the MCP protocol or fakes a tool's response. Every test
either calls pure functions directly or spawns the actual server (src/index.ts) as a real stdio subprocess and
drives it with a real @modelcontextprotocol/sdk Client — the same shape of connection a Duvo agent runtime
uses. Server-spawning tests each set whatever env they need internally (ALLOW_WRITES, a specific
STORELINK_KEYRING, LOG_LEVEL) — there's nothing to export by hand before running npm run test. The bundled
mock upstream binds an OS-assigned ephemeral port per instance (startMockUpstream(port = 0)), so test files that
each start their own server never collide even when vitest runs them concurrently.
File | What it proves |
|
|
|
|
| The stockout projection algorithm (ADR-0004) in isolation — pure functions, no server: normal projections, refusing to guess on thin data, every |
| The generic |
| The full stdio round trip — exact tool list and annotations, |
|
|
| The Step 3 guarantees (ADR-0008) against real captured stderr, not just tool responses — a shared |
| The Step 4 failure stories (ADR-0009) — a key rotating on StoreLink's side mid-session fails as |
Running a single file or a single test:
npx vitest run tests/depletion.test.ts # one file
npx vitest run tests/depletion.test.ts -t "refuses" # one test/describe block by name matchTwo things to know before extending the suite:
Server-spawning tests (
server.test.ts,write-gate.test.ts,observability.test.ts,key-rotation.test.ts) launchtsx src/index.tsas a real child process rather than importingbuildServerdirectly, specifically so they exercise the actualmain()boot path — config loading, the mock-upstream fallback, keyring construction — not just the tool-registration logic. If you're adding a test that only needs to check a tool handler's behavior without touching process boundaries, prefer unit-testing the underlying pure function (asdepletion.test.tsandkeyring.test.tsdo) over spawning another subprocess — it's faster and the failure is easier to localize.key-rotation.test.tsis the one file that runs the mock upstream in-process (viastartMockUpstream()imported directly) rather than letting a spawned server start its own — that's what lets it mutate the mock's accepted key mid-test via__rotateMockStoreKey()while a separately-spawned real server subprocess is still connected, simulating a rotation racing a live session. See the file's header comment and ADR-0009 for why.
End-to-end, beyond the test suite: just verify (cold container build, non-root boot, save/load rollback
drill) and just smoke (dependency budget, npm pack/install/connect against the packaged binary) — see
DEPLOYMENT.md — and npm run scenario, which regenerates
docs/scenarios/butter-restock.md from a real run, covered above.
Configuration
See .env.example for the full reference. StoreLink-specific settings:
Variable | Purpose |
| JSON object mapping |
| Hard ceiling on a single replenishment order, enforced independently of |
| Default POS lookback window for |
Shipping this into Korral's environment
DEPLOYMENT.md covers the runnable artifact (a multi-stage Dockerfile, verified end to end
by just verify — cold build, non-root boot, save/load rollback drill) and the concrete GCP placement: this server
and the Duvo agent runtime it's spawned from run on the same GCE VM or GKE node, inside Korral's tenancy, pulling
from a private Artifact Registry — no architecture change from the stdio model described above, since the runtime
already has exactly one egress destination (StoreLink) and nothing else. See "Korral GCP deployment" in
DEPLOYMENT.md for the placement diagram and the digest-pinned redeploy procedure for frequent post-launch updates,
and ADR-0010 for why this didn't require a different deployment shape.
Observability
docs/observability.md is written for the two people who actually read these logs: an
FDE debugging a failure at 11pm with log access and nothing else, and a category buyer reading the audit log the
next morning to see what the agent did on their behalf. Every tool call gets a call_id correlating it with every
upstream request it triggers; tool.err logs the real error code and message, not just timing; and the audit
stream covers every call — read or write — not only executed writes. See ADR-0008 for why.
Design decisions
Every non-obvious choice above — why workflow-shaped tools over a thin per-endpoint mirror, why the keyring instead
of trusting store_id at face value, how the stockout projection expresses uncertainty, why confirm isn't treated
as human approval — is recorded as an ADR in docs/decisions/, each with what was rejected and a
"revisit when" trigger. Two are explicitly falsifiable during the pilot: if korral_list_recent_sales gets called
after nearly every risk check, check_stock_risk's evidence is under-serving and should be enriched rather than
answered with more tools; if buyers never chase order status, korral_get_order_status should be cut.
What's included (template layer)
Path | What |
| Entry point; |
| Zod-validated config, including the StoreLink keyring. |
| Store-scope resolution and validation — see "Store scoping" above. |
| The stockout-projection algorithm behind |
| Closed error-code taxonomy, extended with |
| Generic REST client, plus the |
| The 5 tools above, one module each. |
| The StoreLink stub — stores, SKUs, suppliers, and POS histories shaped to exercise each data-quality flag. |
| ADR log — the "why" behind everything in this README. |
| Distribution paths, keyring provisioning, and the weekly key-rotation runbook. |
License
MIT.
Available Tools
5 toolskorral_check_stock_riskCheck stock riskARead-onlyIdempotent
Assess whether a SKU at a store will run out before closing today, and whether a replenishment order would arrive in time. Combines current on-hand, recent sales velocity, and supplier lead time into one deterministic projection with an explicit confidence level and any data-quality caveats — the evidence used is included so the projection can be checked without another call. Use korral_list_recent_sales if you need the individual transactions behind the velocity figure (e.g. to check whether a single bulk sale skewed it).
| Name | Required | Description | Default |
|---|---|---|---|
| sku | Yes | SKU code to check. | |
| store_id | No | Which store to check. Optional only if this deployment is authorized for exactly one store, in which case it defaults to that store. Required otherwise — call korral_list_authorized_stores to see the options. | |
| lookback_days | No | Days of POS history to use. Hard cap 28. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, openWorld, idempotent, and non-destructive. The description adds valuable behavioral context: it states the tool 'combines current on-hand, recent sales velocity, and supplier lead time into one deterministic projection with an explicit confidence level' and notes that 'the evidence used is included so the projection can be checked without another call.' 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.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences. The first sentence front-loads the purpose, the second adds transparency and an explicit alternative. No filler or redundancy — every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Despite no output schema, the description tells the agent what to expect: a deterministic projection, confidence level, data-quality caveats, and included evidence. It also orients the agent relative to sibling tools and explains the need for store authorization. The combination of annotations and description is sufficient for safe invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, including details for store_id (optional only if one store is authorized) and lookback_days (default, max, exclusive minimum). The tool description itself does not add parameter meaning beyond the schema; it only mentions 'SKU at a store' in passing. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a very specific purpose: 'Assess whether a SKU at a store will run out before closing today, and whether a replenishment order would arrive in time.' This clearly identifies the verb (assess) and resource (stock risk for a SKU/store), and distinguishes it from siblings like korral_list_recent_sales by explaining what evidence it includes.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly names korral_list_recent_sales as an alternative when the user needs individual transactions, and the store_id parameter in the schema points to korral_list_authorized_stores. This provides clear when-to-use and when-not-to-use guidance, exceeding the minimum.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
korral_get_order_statusGet replenishment order statusARead-onlyIdempotent
Look up the status of a replenishment order previously raised with korral_raise_replenishment_order, by its order_id. Use this to follow up on an order from an earlier session, or to check upstream state after a write call returned WRITE_OUTCOME_UNKNOWN before deciding whether to resubmit.
| Name | Required | Description | Default |
|---|---|---|---|
| order_id | Yes | Order ID returned by korral_raise_replenishment_order. | |
| store_id | No | Which store the order belongs to. Optional if this deployment is authorized for exactly one store; required otherwise. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the tool's safety profile is clear. The description adds valuable behavioral context, explaining that it can be used to check upstream state after a write call returned WRITE_OUTCOME_UNKNOWN, which helps the agent decide whether to resubmit. This goes beyond the annotations by framing the tool's role in a workflow, though it does not describe return format or other edge cases.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences long, front-loaded with the core purpose in the first sentence and usage guidance in the second. Every clause earns its place with relevant information, and there is no redundancy or filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple lookup tool with rich annotations and complete schema coverage, the description is highly complete. It explains the tool's role in the order lifecycle, when to use it, and how it relates to the write operation. The absence of an output schema is not a gap because annotations and usage guidance provide sufficient context for the agent.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema covers 100% of parameters with descriptions: order_id is 'Order ID returned by korral_raise_replenishment_order' and store_id specifies 'Which store the order belongs to... Optional if this deployment is authorized for exactly one store; required otherwise.' The description does not add additional parameter semantics beyond what the schema already provides, so it meets the baseline for high schema coverage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: 'Look up the status of a replenishment order previously raised with korral_raise_replenishment_order, by its order_id.' It uses a specific verb ('look up'), identifies the resource (replenishment order status), and mentions the key parameter (order_id). It also distinguishes from siblings by focusing on order status lookup versus listing stores, checking stock risk, listing sales, or raising orders.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit usage context: 'Use this to follow up on an order from an earlier session, or to check upstream state after a write call returned WRITE_OUTCOME_UNKNOWN before deciding whether to resubmit.' This tells the agent when to use it, though it does not explicitly state when not to use it or name alternatives. Since the sibling tools serve different purposes, the context is sufficient.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
korral_list_authorized_storesList authorized storesARead-onlyIdempotent
List the Korral stores this deployment is authorized to query, each with local time and closing time. Call this first to learn which store_id(s) are available — every other tool rejects a store_id not in this list. If exactly one store is authorized, store_id is optional on every other tool and defaults to it.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, openWorldHint=true, idempotentHint=true, and destructiveHint=false. The description adds context beyond these: it specifies the output includes local time and closing time, and warns that sibling tools will reject store_ids not returned here, implying this list is authoritative for valid IDs. No contradictions. Minor gap: it doesn't specify the exact response structure, but for a no-param read-only list this is acceptable.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, no wasted words. The purpose, usage guidance, and edge-case default are all front-loaded and clearly structured. Each sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter, read-only list tool with strong annotations and no output schema, the description is complete: it states what is listed, what fields are included, the tool's role as a prerequisite, and the single-store default behavior. It covers both the output and the logical relationship with sibling tools, leaving no critical gaps.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema has zero parameters, so the baseline is 4. The description adds value by explaining that the tool's output contains store_id values which are used as parameters for other tools, and clarifies the optionality/default behavior. This bridges the output to subsequent parameter usage, which is helpful even though this tool itself has no parameters.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the action ('List') and resource ('Korral stores this deployment is authorized to query'), with specific detail about returned data (local time, closing time). It distinguishes this tool from siblings as the discovery mechanism for valid store_id values, which none of the listed sibling tools provide.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly instructs to call this tool first, explains that every other tool rejects invalid store_id values, and describes the default behavior when exactly one store is authorized. This gives unambiguous when-to-use guidance relative to all sibling tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
korral_list_recent_salesList recent salesARead-onlyIdempotent
List individual POS transactions for a SKU at a store, most recent first. Use this to see whether a sales velocity figure from korral_check_stock_risk was driven by many small baskets or a single large one (e.g. a catering order), or as the audit trail when a projection needs to be double-checked against what actually sold.
| Name | Required | Description | Default |
|---|---|---|---|
| sku | Yes | SKU code to look up. | |
| limit | No | Max transactions to return. Hard cap 500. | |
| since | No | ISO 8601 timestamp; only transactions at or after this time are returned. Defaults to 24 hours ago. Capped at 7 days back. | |
| store_id | No | Which store. Optional if this deployment is authorized for exactly one store; required otherwise. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish safe read-only/idempotent behavior. The description adds concrete behavioral details: returns individual transaction lines (not aggregates), sorted most recent first, and serves as an audit trail. This goes beyond the schema's parameter descriptions. Minor gap: no mention of return fields or pagination, but schema covers limit.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the primary action, and the second sentence provides valuable selection context without redundancy. Every word earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a medium-complexity read-only tool with well-documented schema and clear annotations, the description provides purpose, granularity, ordering, and use-case guidance. The only missing piece is what fields appear in each transaction, but no output schema exists and the description's clarity compensates. It's sufficiently complete 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.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline applies. The description reinforces that sku and store_id define the target, and context implies limit/since control the window, but it doesn't add new parameter-level meaning beyond the schema. It does tie 'sales velocity' to the sku parameter, which adds a hint, but not substantial.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('List') with a clear resource ('individual POS transactions') and scope ('for a SKU at a store, most recent first'). It also differentiates from sibling tools by referencing korral_check_stock_risk and framing this as the transactional detail behind that aggregate view.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states when to use: to break down sales velocity into basket sizes or as an audit trail against projections. Names the related sibling tool (korral_check_stock_risk) and gives a concrete decision scenario, providing clear guidance for tool selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
korral_raise_replenishment_orderRaise a replenishment orderA
Raise a replenishment order for a SKU at a store. Defaults to a dry run: the response is a preview containing the exact order plus the on-hand, sales velocity, and stockout projection that justify it, a duplicate-order warning, and a quantity-sanity check — enough for a human to approve or reject without another tool call. Set confirm=true to execute, which also requires the deployment to have ALLOW_WRITES enabled. confirm=true reflects this agent's own judgment, not a recorded human approval — treat it accordingly.
| Name | Required | Description | Default |
|---|---|---|---|
| sku | Yes | SKU code to order. | |
| reason | Yes | Why this order is being raised — required so the audit log carries intent, not just the payload. | |
| confirm | No | Set true to execute. Default false returns a dry-run preview only. | |
| store_id | No | Which store. Optional if this deployment is authorized for exactly one store; required otherwise. | |
| quantity_units | Yes | Units to order. | |
| idempotency_key | No | Optional caller-supplied key to avoid double-submission on retry. Auto-generated if omitted. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations offer only high-level hints (readOnly=false, idempotent=false). The description adds substantial behavioral detail: dry-run default with preview contents, confirm gating execution, ALLOW_WRITES requirement, and the critical caveat that confirm=true reflects agent judgment not human approval. 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.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the core action, then tightly packed with essential workflow and safety information. Every clause earns its place; no repetition or fluff.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description covers the return value preview elements (order, on-hand, sales velocity, stockout projection, warnings, sanity check), execution prerequisites, and a safety caveat. For a tool with this complexity, the description is fully self-sufficient.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3. The description enriches parameter understanding by explaining confirm's dual dry-run/execute behavior, store_id optionality based on authorization, and idempotency_key's purpose—none of which are in the schema descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb+resource: 'Raise a replenishment order for a SKU at a store.' It clearly distinguishes this write tool from sibling read-only tools like list_authorized_stores and check_stock_risk.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides clear context: dry run by default for human approval, confirm=true to execute, and ALLOW_WRITES requirement. It doesn't explicitly state when not to use it or name alternatives, but the write vs read contrast is implicit and the workflow is well described.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
5 tool updates
v0.1.0- First observed
korral_check_stock_risk - First observed
korral_get_order_status - First observed
korral_list_authorized_stores - First observed
korral_list_recent_sales - First observed
korral_raise_replenishment_order
TDQS
Scored across 5 tools
Each tool has a clear, unique purpose: store discovery, risk assessment, sales audit, order creation, and order status lookup. The descriptions explicitly cross-reference when to use related tools (e.g., check_stock_risk vs list_recent_sales), eliminating ambiguity.
All tool names follow a uniform korral_<verb>_<object> pattern using snake_case. Verbs are consistent (list, check, get, raise) and objects are descriptive, making the toolset easily predictable.
With 5 tools, the set is well-scoped for a replenishment workflow. Each tool covers an essential step (discovery, analysis, detail, write, follow-up) without redundancy or bloat.
The core lifecycle of assessing stock risk and raising/checking replenishment orders is covered, including a dry-run preview and audit trail. Minor gaps exist, such as no list of all orders and no cancel/update order operation, but these may be out of scope for the intended purpose.
Maintenance
Related MCP Connectors
MCP Server for an Agent Task Marketplace
MCP server for Product Management
MCP Server for agents to onboard, pay, and provision services autonomously with InFlow
Remote MCP server for product discovery catalog and retrieving product details.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceMCP server that provides pre-checkout basket tools and a local API/viewer, enabling agents to research products and manage a shopping cart through natural language.1MIT
- FlicenseNot gradedqualityBmaintenanceMCP 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.-
- FlicenseAqualityBmaintenanceMCP server for managing store replenishment, including listing stores, raising orders, checking order status, and viewing SKU reports with observability and key rotation handling.4-
- FlicenseAqualityCmaintenanceMCP server exposing the StoreLink grocery stocking API as tools for category buyer workflows, including inventory checks and replenishment orders.5-