distru-mcp
Click on "Install 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., "@distru-mcpList open sales orders and show inventory availability for their line items."
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.
distru-mcp
An MCP server over Distru's public REST API, built to demonstrate what production guardrails on an agent-facing ERP integration actually look like.
It runs with zero credentials. Out of the box every tool serves realistic
fixture data generated from Distru's documented response schemas, so you can
clone it and see the whole thing work in about a minute. Set
DISTRU_API_TOKEN and the same tools talk to the live API instead.
This is an independent project. It is not affiliated with, endorsed by, or supported by Distru. It is built entirely against their publicly published API documentation at https://apidocs.distru.dev and the OpenAPI 3.0 document that page links to. All brands, products, companies, and people in the fixture data are invented.
60-second quickstart
git clone <this repo> && cd distru-mcp
npm install
npm run smokenpm run smoke runs the whole thing end to end in fixture mode: lists the
tools, calls read tools, feeds a deliberately messy purchase order through the
matcher, then walks the write tool through all four of its outcomes — blocked
by the review gate, dry-run preview, confirmation refused because the order
changed, and finally a confirmed simulated write — and prints the audit log it
produced. No token, no network, no config.
To run it as an actual MCP server over stdio:
npm run build
node dist/index.js # or: npx . after npm installTo register it with an MCP client:
{
"mcpServers": {
"distru": {
"command": "node",
"args": ["/absolute/path/to/distru-mcp/dist/index.js"]
}
}
}That configuration gives you fixture mode with writes disabled. Add
"env": { "DISTRU_API_TOKEN": "...", "DISTRU_ALLOW_WRITES": "true" } when you
mean it. See .env.example for every variable.
Related MCP server: enterprise-agent-lab
Tools
Read tools, always available:
Tool | Endpoint |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| (local analysis, no endpoint) |
Write tool, registered only when DISTRU_ALLOW_WRITES=true:
Tool | Endpoint |
|
|
The guardrails, and why each one exists
These are the point of the project. The tools are the easy part.
1. Permission scoping by registration, not by refusal
The write tool is not registered unless DISTRU_ALLOW_WRITES is exactly the
string "true". Not truthy, not "1", not "yes". On a read-only deployment
it does not appear in tools/list at all.
The distinction between "absent" and "refused" matters more than it looks. A refused capability is still a capability the model can see, plan around, attempt, and then report on — "I tried to place the order but was denied" is a sentence that should never be possible on a read-only deployment. An absent capability produces no intent to act at all.
The exact-string check is deliberate. A permission this consequential should not be switchable by a stray value in a shell profile.
2. Dry run by default, bound by a confirmation token
distru_draft_sales_order returns a preview and writes nothing unless you pass
confirm: true. The preview includes the exact JSON body that would be POSTed,
and a confirmation_token — a hash of that body.
confirm: true requires the token, and the token is recomputed from the
confirming call's own arguments. Preview a one-line order, show it to a human,
then confirm a two-line order, and the write is refused with
confirmation_mismatch.
That check is the difference between a confirmation gate and a
confirmation-shaped speed bump. The tool's own description tells the caller to
show the preview to a user and then call again — but for most of this project's
life those were two unrelated calls with nothing tying them together, so
"confirmed" only ever meant "some call had confirm: true on it". It matters
most for the caller this server is actually built for: a model reading
documents it does not control, between the two calls.
order_datetime is stamped once, at execution, and deliberately excluded from
the token. It used to be defaulted to "now" independently on each call, which
meant the previewed body already differed from the sent body whenever the
caller omitted it. The test that was supposed to catch that pinned the value,
which is exactly how it hid.
The gate I care about most: the token is still not sufficient. If any PO
line landed in needs_review, the write is blocked regardless. You either fix
the input, or you pass allow_partial: true, which orders the clean lines and
hands the flagged ones back.
Confirmation means "yes, do the thing you showed me". It does not mean "and also decide the parts you just told me you could not decide". Collapsing those two into one flag is how you end up with a system that technically asked permission and still did the wrong thing.
3. Structured audit log
Every tool invocation appends one JSONL line: timestamp, tool, mode, whether it
was a write, outcome (ok / blocked / error), duration, a hash of the
arguments, and the argument key names.
{"ts":"2026-08-31T13:39:44.827Z","tool":"distru_draft_sales_order","mode":"fixture","write":true,"outcome":"ok","duration_ms":1,"args_hash":"e2a566dc634d515d","args_keys":["allow_partial","company_id","confirm","lines","order_datetime"],"meta":{"confirm":true,"action":"executed","simulated":true,"order_number":"SO-SIM-9001","order_total":"2892.00"}}Two design decisions in there:
No free-form argument value is written. A PO line carries customer names, quantities, and prices. An audit log is not the place to make a second copy of them. You get a SHA-256 hash and the key names — enough to prove two calls were identical, to correlate a line with a request you still hold, and to see the shape of what was sent.
Getting that right took more than not writing args. Every human-readable
error message in a system like this embeds the input that caused it: zod's enum
message quotes the value it rejected, a 404 message quotes the requested id,
and an API error message quotes the whole request path — which is every filter
value you sent. So errors are recorded as a classification plus schema-level
detail ("error_kind":"validation", "error":"page: too_small";
"error_kind":"api_error", "error":"HTTP 404 at id"). The prose still goes
back to the caller. It is only kept out of the durable log.
The meta object is the deliberate carve-out for facts worth recording — the
id of an order that was created, a count of lines flagged. Two things enforce
the promise there rather than relying on tool authors: caller-supplied
identifiers go through a UUID validator (get_product({id: "CUSTOMER-SSN-..."})
records "(non-uuid)"), and every meta value passes a sanitiser that keeps
numbers, booleans, and short tokens from a constrained charset and redacts
anything else.
Logging failures never break a tool call. A full disk or a read-only mount degrades to one warning on stderr, emitted at most once, and the tool returns normally. An audit log that can take down the server is a liability, not a control.
That claim was false for longer than I would like. The try/catch covered the
sink but not the failure reporter inside it — so a throwing onFailure (EPIPE
writing to a closed stderr, the realistic case for a stdio server) rejected the
queue promise that record() returns and the dispatcher awaits. It failed that
call, and because the chain never recovered, every call after it for the life
of the process. The reporter is now wrapped, the queue cannot reject, and
record() is guarded end to end. Because a swallowed error is exactly the sort
of thing that silently stops working, there are explicit tests for all of it.
4. Mode is stamped on every payload
Every tool result carries "mode": "fixture" or "mode": "live", and fixture
results carry an explicit notice. Not just at startup, where it scrolls away.
An assistant that cannot tell fixture data from production data will cheerfully tell someone their warehouse holds 480 prerolls that do not exist. Stamping every payload makes that structurally harder.
Mode is also derived rather than declared: there is no DISTRU_MODE variable
to get wrong. Either a token is present or it is not. You cannot accidentally
point a demo at production, and you cannot accidentally run production against
fixtures.
5. Money never touches a float
Distru returns every price, quantity, and total as a decimal string,
specifically so clients do not lose precision to JSON floats. Honouring that
only means something if the client also refuses to use floats, so all
arithmetic goes through src/decimal.ts, which is BigInt
fixed-point.
The one place a decimal becomes a number is the request boundary, because
Distru's OrderItemRequest documents quantity and price_base as JSON
numbers even though the response returns them as strings. That conversion
throws rather than silently rounding if a value cannot survive the round trip.
The messy-PO demo
The fixture catalog is deliberately dirty. Four traps, each one I have watched a real wholesale catalog produce:
Reissued SKU ids. The same physical product exists twice because the SKU was reissued after a packaging change. Both rows are active; only one holds inventory.
brand|name|sizeduplicate rows. Same product entered twice under unrelated SKU conventions, usually because two people onboarded the same vendor. No reissue relationship to key off.Category slug collision.
Pre-RollsandPrerollsexist as two distinct category records normalising to the same slug. Any filter on one silently drops the other.Conflicting MSRP. Two rows for one item disagree on suggested retail.
distru_match_po_to_catalog takes free-form PO lines and returns two buckets:
matched, and needs_review with the reason and every candidate considered.
The design rule is that when the catalog is ambiguous, the tool returns the ambiguity. It does not pick. A matcher that always returns its best guess demos better and is worse in production, because the failure is silent.
Running examples/messy-po.json — 13 lines against
a 15-product catalog:
catalog size : 15
matched : 3
needs review : 10
matched subtotal : 2892.00
MATCHED
line 0 MAV-EMR-FL-35G Ember Row Flower 3.5g qty 48 = 1152.00
line 9 FGP-BRT-5MG Bramble Tonic 5mg qty 60 = 540.00
line 12 CCC-LGF-PR-1G Long Field Preroll 1g qty 240 = 1200.00 [note: category_slug_collision]
NEEDS REVIEW
line 1 Nightjar Botanicals Blue Harbor Preroll 1g - 144 units
-> ambiguous_match
2 catalog products scored within 0.08 of the best match. Nothing on this line separates them.
-> reissued_sku_pair
Two catalog entries differ only by a reissued ID. They are the same physical
product under a superseded and a current SKU, and the inventory usually sits
on only one of them.
line 2 Sable Ridge Farms Alpine Mist Cartridge 0.5g - 60 units
-> ambiguous_match
-> duplicate_identity
Duplicate catalog rows share the same brand, name, and size under unrelated
SKU conventions. Nothing in the data identifies which row is canonical.
line 3 Foxglove Provisions Meadow Gold Gummies 100mg - 90 units
-> ambiguous_match
-> reissued_sku_pair
-> msrp_conflict
The same product identity carries 2 different MSRPs (24.00 vs 30.00). Choosing
a row here decides the customer's retail price, which is not this tool's call
to make.
line 4 Cobalt Creek Sunset Lane Preroll 5-Pack
-> category_slug_collision
The line names category "prerolls", but the catalog holds 2 distinct category
records that normalise to it ("Pre-Rolls", "Prerolls"). Any filter on one of
them silently drops the other.
line 5 Marrow & Vine Ember Row Flower - 24 units
-> size_unspecified
The line does not state a size, and 2 catalog products match its text at 3.5g, 7g.
-> ambiguous_match
line 6 sku NJB-BLH-PR-1G
-> reissued_sku_pair
-> insufficient_inventory
The line asks for 24 but only 0 is available on this product. Note that a
duplicate catalog row for the same product may hold the rest.
line 7 Harvest Moon Shatter 1g - 12 units
-> no_match
line 8 Nightjar Tidewater Live Resin 1g - 30 units @ $24.00
-> price_mismatch
The line prices this at 24.00 but the catalog lists 28.00. A price override on
an order is a commercial decision, not a matching decision.
line 10 Nightjar Old Mill Preroll 1g - 20 units
-> inactive_product
line 11 assorted preroll singles, quantity TBD
-> unparseable_lineTwo lines deserve a second look.
Line 6 gives an exact SKU. It resolves unambiguously, with total confidence, to the superseded row holding zero stock. An exact identifier match is not a reason to stop checking, so the trap detectors run on resolved lines too. This is the case that separates a real matcher from a demo.
Line 12 matched, and carries a note rather than a blocking reason. Its category has a slug twin, so downstream category reporting on this order will split — but the match itself is sound and the order should go through. A guardrail that cannot tell "this is wrong" from "this is worth knowing" gets ignored within a week.
The full annotated run, with complete JSON evidence, is in
examples/WALKTHROUGH.md.
Every way a line can be flagged
The full vocabulary, since the flags are the product. "Blocks" means the line
lands in needs_review and the write gate refuses it; a note rides along on a
match that is otherwise sound.
Code | Blocks | Meaning |
| yes | Nothing in the catalog resembles the line. |
| yes | Two or more products scored within the tie band. Nothing on the line separates them. |
| yes | The line's SKU resolves, but the rest of the line disagrees with the product it resolves to — size, brand, name, category, or pack. A SKU scraped out of free text gets extra suspicion, because a quote or PO number looks exactly like one. |
| yes | The line states a SKU the catalog has never heard of. Any candidate shown was found by text similarity, not by that identifier. |
| yes | Two catalog rows carry the same SKU once separators and case are normalised. A lookup cannot choose between them, and first-wins would hide the other row's inventory. |
| yes | Two entries differ only by a reissued ID — the same physical product under a superseded and a current SKU, with the stock usually on one of them. |
| yes | Duplicate rows share brand, name, and size under unrelated SKU conventions. Nothing in the data says which is canonical. |
| both | The named category normalises to two distinct catalog records. Blocks when the category was the line's only anchor; rides as a note when the match stands without it. |
| yes | The same product identity carries different MSRPs. Choosing a row decides the customer's retail price, which is not this tool's call to make. |
| yes | The line names no size and the catalog matches its text at more than one. |
| yes | The line names no pack count and the tied candidates split between singles and multipacks. |
| note | The line states a size the catalog row does not record, so the claim could not be checked. The match rests on the rest of the line. |
| yes | The line prices the item differently than the catalog does. A price override is a commercial decision, not a matching decision. |
| yes | The resolved product cannot cover the requested quantity — and a duplicate row for the same product may hold the rest. |
| yes | The product resolved cleanly and is no longer active. |
| yes | The line carries no usable signal ("assorted preroll singles, quantity TBD"). |
The demo PO exercises eleven of these. The other five — the contradiction,
collision, and pack detectors — came out of an adversarial review pass run
against the matcher itself, and the suite in test/ pins all
sixteen.
What this does not do
Being specific, because a vague scope section is worthless:
Eight endpoints out of 113. Products (list and get), inventory, orders (list, get, and upsert), invoices, and companies. No assemblies, batches, packages, purchases, transfers, returns, payments, price tiers, custom fields, tasks, strains, or any of the eighteen report endpoints.
One write flow. Drafting a sales order. No invoice creation, no payments, no inventory adjustments, no deletes. The write surface is small on purpose.
Never tested against the live API. I do not have a Distru account. No request in this repository has ever reached
app.distru.com. The live client is written from the published OpenAPI document and the reference docs, andtest/live-client.test.tsdrives it against a stubfetchto assert the exact URLs, headers, bodies, error handling, and pagination behaviour it produces — but a stub agreeing with my reading of the docs is not the same as a server agreeing with it. That is the single biggest caveat here and I would not claim otherwise.No compliance integration. Metrc and BioTrack fields exist in the types because they exist in the API. Nothing in this server touches a compliance transfer, and it should not without a great deal more care.
No PDF endpoints. They carry their own rate limit (20/minute, 1,000/day) that would need real backoff handling.
No webhook receiver. The API supports signed webhooks; consuming them is a different program.
No retry or rate-limit backoff. A failed request fails. For a read-mostly tool server driven by a human-paced conversation that is the right trade, but it is a trade.
The matcher is heuristic. Token similarity with hard filters on brand, size, category, and pack count. A line that supplies a SKU resolves through the SKU, but the same assertions are then re-checked against the row it resolved to, so a SKU cannot silently override the rest of the line. It is tuned to stop early rather than to maximise match rate, and the tie band is deliberately wide. It has no embeddings, no learning, and no memory of past decisions.
Fixture mode is not a Distru simulator. It implements the filters the tools expose and refuses anything else rather than silently ignoring a filter and returning too many rows. It is not a general mock of the API.
Layout
src/
index.ts stdio entry point
server.ts MCP wiring (thin: no logic lives here)
dispatch.ts registry, permission gate, validation, audit wrapper
config.ts environment resolution
client.ts DistruClient interface + live and fixture implementations
http.ts fetch wrapper, query serialization
audit.ts JSONL audit logger
decimal.ts BigInt fixed-point arithmetic
matcher.ts PO line matching and trap detection
normalize.ts text, size, SKU, and category-slug normalization
types.ts domain types mirrored from the OpenAPI document
tools/
types.ts tool definition shape
read.ts the eight read tools
write.ts the one write tool
fixtures/
seed.ts deterministic ids and timestamps
catalog.ts the deliberately dirty catalog
transactions.ts orders, invoices, inventory derived from the catalog
store.ts filtering and pagination
test/ 221 tests, incl. a regression suite for past defects
examples/ messy PO plus an annotated walkthrough
scripts/smoke.ts the end-to-end fixture-mode runThe tool handlers know nothing about MCP. The dispatcher is what the tests and the smoke script drive, which is the same path the server drives — so the tests cannot pass while the real path is broken, and the audit log cannot silently stop working.
Development
npm run typecheck # tsc --noEmit, strict, with noUncheckedIndexedAccess
npm test # vitest, 221 tests
npm run smoke # end-to-end fixture run
npm run build # emit to dist/
npm run dev # run from source via tsxCI runs typecheck, tests, build, and the smoke run on Node 20, plus a clean-room grep.
Dependencies are @modelcontextprotocol/sdk and zod. Dev dependencies are
typescript, vitest, tsx, and @types/node. That is the whole list.
API notes worth knowing
Things the docs say that are easy to get wrong, and that this client handles:
Array filters repeat a bracketed key:
?skus[]=A&skus[]=B. Not comma-joined, not a repeated bare key.Pagination is
page[number]=N, 1-indexed, and page size is explicitly not guaranteed stable. Follownext_pagerather than counting rows. The client refuses to follow anext_pagepointing at a different origin, since it attaches a bearer token to every request.Datetime filters encode direction in the comma position:
T,is on or after,,Tis on or before,A,Bis between, all inclusive.Money and quantities are strings. A few fields are still JSON numbers and the docs say they will become strings, so parse defensively.
GET /inventoryrequiresgroupings, and it must includePRODUCT. Each grouping adds its id field to the rows; omitted groupings mean the field is absent, not null. Grouping byBATCH_NUMBERdrops product-tracked products and forcesreservedto"0".Writes are upserts. Omit
idto create, include it to update. On update, omitting a field leaves it unchanged and sendingnullclears it — but collections are replace-in-full, so a partialitemsarray deletes the lines you left out.Enums may grow. Handle unknown tokens rather than assuming the documented list is closed.
License
MIT. See LICENSE.
Available Tools
8 toolsdistru_get_inventoryGet inventory levelsARead-only
Get on-hand inventory rolled up by a chosen set of attributes (GET /public/v1/inventory). groupings is REQUIRED and must include PRODUCT. Each grouping adds its id field to every row: PRODUCT adds product_id, LOCATION adds location_id, BATCH_NUMBER adds batch_number; omitted groupings are absent from the rows entirely. Groups with zero quantity are omitted, and deactivated products are excluded. Grouping by BATCH_NUMBER drops product-tracked products and always reports reserved as "0". This data is eventually consistent: a write can take about a second to show up here.
| Name | Required | Description | Default |
|---|---|---|---|
| page | No | 1-indexed page number. Page size is not guaranteed stable; follow next_page instead. | |
| groupings | Yes | Attributes to roll up by. Must include PRODUCT. Order sets the sort order. | |
| product_ids | No | ||
| location_ids | No | ||
| product_skus | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Adds substantial behavior beyond the readOnlyHint/openWorldHint annotations: eventual consistency ('a write can take about a second to show up here'), omission of zero-quantity groups, exclusion of deactivated products, and the BATCH_NUMBER special cases (drops product-tracked products, always reports reserved as "0"). The readOnlyHint is consistent with the 'Get' verb, and no contradiction exists.
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?
Five dense sentences, front-loaded with purpose and the required-parameter constraint before the caveats. The grouping-to-id-field mapping, row omission rules, and consistency note are all load-bearing; there is no filler and no redundant restatement of the title or schema.
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 grouping-to-id-field mapping is essential and is provided, partially compensating for the missing return-value documentation. Remaining gaps are minor: the response envelope/pagination metadata and how the filter arrays compose with roll-ups are not described, and filter semantics rest on parameter names. For the tool's complexity, this is near-complete.
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 only 40% (page and groupings documented; product_ids, location_ids, and product_skus are bare arrays). The description compensates richly for the required groupings parameter by mapping each grouping to the id field it adds and explaining row-omission behavior, but the three filter arrays get no description-level semantics beyond their self-evident names. Net positive compensation for the critical parameter.
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?
States a specific action on a specific resource — 'Get on-hand inventory rolled up by a chosen set of attributes' — and pins it to the explicit endpoint (GET /public/v1/inventory). The roll-up-by-groupings mechanic is unique among the siblings, which are all products, orders, invoices, and companies, so an agent can distinguish this tool without opening the schema.
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?
Makes the valid usage envelope explicit: groupings is REQUIRED and must include PRODUCT, with per-grouping effects spelled out. It never names alternatives or when-not-to-use conditions, but the sibling tools are domain-disjoint resources, so selection is unambiguous. The operational constraints are concrete enough to prevent invalid calls.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
distru_get_orderGet a sales orderARead-only
Fetch one sales order by id (GET /public/v1/orders/{id}), including its line items, charges, and any linked invoices.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | The order UUID. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true. The description adds meaningful behavioral context by specifying that the result includes line items, charges, and linked invoices, and that it is a read-only GET operation. No contradictions 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?
A single, tight sentence that front-loads the verb and resource, specifies the endpoint, and enumerates the included data. No filler or redundant phrasing.
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 single-parameter read-only tool, the description gives all necessary context: what it fetches, how it identifies the order (id), and what is included in the result. Annotations cover the safety profile, and no output schema is needed.
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 schema fully documents the only parameter 'id' as 'The order UUID' with 100% coverage. The description merely says 'by id' and includes the {id} placeholder, which adds no new meaning beyond the schema. 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 specific verb ('Fetch'), a specific resource ('one sales order by id'), and names the exact endpoint. It also lists the included components (line items, charges, linked invoices), clearly distinguishing it from sibling list tools like distru_list_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 clearly implies the tool is for retrieving a single order when its ID is known, and naming the id parameter reinforces that. It does not explicitly say when to use list_orders instead, but the singular 'one' and the endpoint pattern provide enough context.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
distru_get_productGet a productARead-only
Fetch one product by id (GET /public/v1/products/{id}). Returns 404 if the id does not exist on this account. Ids are opaque UUID strings - never construct or parse one.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | The product UUID. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnlyHint and openWorldHint annotations, the description discloses important behavior: a 404 is returned when the id does not exist on this account, and ids are opaque UUIDs that must never be constructed or parsed. This gives the agent practical guidance for handling failures and id values.
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 three short sentences, front-loaded with the primary action and endpoint. Every sentence adds distinct value: what it does, failure behavior, and id-handling guidance. No wasted words.
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 single-parameter read operation, the description covers the action, endpoint, error case, and id semantics. The annotations cover safety, and no output schema is needed because the operation is straightforwardly 'fetch one product'.
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 already documents the id parameter well, and schema coverage is 100%. The description adds valuable extra meaning by warning that ids are opaque and must never be constructed or parsed, which goes beyond the schema's 'The product UUID.'
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 specific verb and resource: 'Fetch one product by id' with the exact endpoint 'GET /public/v1/products/{id}'. It clearly distinguishes this from sibling tools like distru_list_products by focusing on single-record retrieval by id.
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 clearly implies when to use this tool: when you need one product by id. It does not explicitly name alternatives or say 'use list_products to browse', but the single-id scope and endpoint make the usage context unambiguous.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
distru_list_companiesList companiesARead-only
List company relationships - customers, vendors, and brands all live here (GET /public/v1/companies). A product's brand is a company record, so this is also how you resolve a brand name to a brand_id for product filtering.
| Name | Required | Description | Default |
|---|---|---|---|
| ids | No | ||
| name | No | Case-insensitive substring match. | |
| page | No | 1-indexed page number. Page size is not guaranteed stable; follow next_page instead. | |
| names | No | Exact match on any name in this list. | |
| deleted | No | Whether to include soft-deleted records. Defaults to "no". | |
| category | No | ||
| updated_datetime | No | Comma-delimited datetime range. "T," = on or after T; ",T" = on or before T; "A,B" = between A and B inclusive. Format: YYYY-MM-DDTHH:MM:SS.MSZ. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already provide readOnlyHint and openWorldHint, so the description adds value by revealing that the endpoint is GET /public/v1/companies and that company records serve multiple roles (customers, vendors, brands). It also signals that the tool can be used as an intermediate lookup step, which is behaviorally relevant. No contradictions 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?
Two tight sentences with no filler. The verb and resource are front-loaded, and the brand-resolution use case is tucked into a second sentence. Every clause contributes.
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 read-only list operation with no output schema, the description covers the key call context: the resource scope and a high-value use case. It doesn't describe return shape or pagination, but the page parameter schema already covers next_page behavior. Overall it's sufficient for an agent to 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 71%, so the schema already documents most parameters with descriptions. The description adds a useful hint that the 'name' parameter can be used to resolve brand names to brand_id, but it doesn't clarify the two undocumented parameters (ids, category). Overall, the description doesn't significantly compensate for the coverage gap, landing at baseline.
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?
States a specific verb ('List') and resource ('company relationships'), then clarifies scope by noting customers, vendors, and brands all live here. The added note about resolving brand names to brand_id for product filtering makes it distinct from product/order/invoice list tools.
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 explains when to use this tool: when working with company relationships, especially when needing to resolve a brand name to a brand_id for product filtering. It doesn't explicitly name alternative tools for other resources, but the sibling context and the brand resolution use case make the usage boundary reasonably clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
distru_list_invoicesList invoicesARead-only
List invoices (GET /public/v1/invoices). Status is one of NOT_PAID, PARTIALLY_PAID, FULLY_PAID, OVER_PAID. paid_amount and remaining_amount are decimal strings. A voided invoice keeps its row and carries a voided_datetime.
| Name | Required | Description | Default |
|---|---|---|---|
| ids | No | ||
| page | No | 1-indexed page number. Page size is not guaranteed stable; follow next_page instead. | |
| statuses | No | ||
| is_voided | No | ||
| order_ids | No | ||
| company_ids | No | ||
| invoice_number | No | Substring match on invoice number. | |
| invoice_datetime | No | Comma-delimited datetime range. "T," = on or after T; ",T" = on or before T; "A,B" = between A and B inclusive. Format: YYYY-MM-DDTHH:MM:SS.MSZ. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare the operation read-only and open-world, so the description does not need to restate safety. It adds meaningful behavior: statuses are limited to four values, monetary fields are decimal strings, and voided invoices remain in results with a voided_datetime. These details go beyond annotations and schema.
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 focused sentences with no filler. The endpoint is front-loaded, and the follow-up details about statuses, decimal strings, and voided behavior each add meaningful information.
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 an 8-parameter list tool with no output schema, the description covers key response semantics but leaves some parameter behavior undocumented. It does not clarify how is_voided relates to voided_datetime, nor does it describe pagination beyond what the schema's page parameter already states. Overall it is serviceable but not fully complete.
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 only 38%, so the description needed to compensate. It does define status values and reveals output-field semantics that clarify statuses and is_voided, but it does not explain ids, order_ids, company_ids, or the is_voided filter directly. The names are self-explanatory, but the description only partially fills the schema gap.
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 identifies the tool as 'List invoices' with the exact endpoint, then adds distinguishing semantics: status vocabulary, decimal-string formatting, and voided-invoice behavior. This makes it easy to tell apart from sibling list/get tools.
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 resource is clear, so the intended use is implied: list invoices rather than orders, products, or companies. However, there is no explicit guidance about when to prefer this over related tools or what filtering use cases it best supports.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
distru_list_ordersList sales ordersARead-only
List sales orders (GET /public/v1/orders). Status is one of PENDING, PROCESSING, READY_TO_SHIP, DELIVERING, DELIVERED, COMPLETED, CANCELED. Totals are decimal strings. New status values may be added over time, so handle unknown tokens rather than assuming this list is closed.
| Name | Required | Description | Default |
|---|---|---|---|
| ids | No | ||
| page | No | 1-indexed page number. Page size is not guaranteed stable; follow next_page instead. | |
| statuses | No | ||
| company_ids | No | Buyer company relationship ids. | |
| product_ids | No | Restrict to orders containing any of these products. | |
| order_number | No | Substring match on order number. | |
| order_numbers | No | Exact match on any order number. | |
| order_datetime | No | Comma-delimited datetime range. "T," = on or after T; ",T" = on or before T; "A,B" = between A and B inclusive. Format: YYYY-MM-DDTHH:MM:SS.MSZ. | |
| updated_datetime | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already provide readOnlyHint and openWorldHint, and the description adds concrete behavioral detail beyond them: totals are decimal strings and status values are not a closed set. This materially helps an agent avoid parsing mistakes and future breakage. 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 compact and front-loaded with the core operation, then adds only high-value caveats: status tokens, decimal-string totals, and open-world status handling. Every sentence earns its place and there is no redundant boilerplate.
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 list endpoint with nine optional filters and no output schema, the description covers the most important non-obvious details: allowed statuses, decimal-string totals, and open-world status handling. It doesn't describe the full response shape or default ordering, but the schema's page and datetime documentation plus annotations cover most invocation needs.
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 67%, and the schema already documents most parameters well, including page semantics, datetime ranges, and filter meanings. The description adds no parameter-specific detail beyond the status enum, which is already in the schema, so it contributes little beyond the structured input description.
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 specific verb and resource: 'List sales orders' with the endpoint GET /public/v1/orders. It is clearly a collection-level read operation, though it does not explicitly differentiate itself from the sibling distru_get_order beyond the natural list-vs-single distinction.
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?
Usage context is implied by the 'List' verb and the sibling tool names, but the description never states when to choose this over distru_get_order or other sibling tools. It gives helpful operational context about statuses and open-world values but no explicit when/when-not guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
distru_list_productsList productsARead-only
List products from the Distru catalog (GET /public/v1/products). Filters combine with AND; values within a single list filter combine with OR. Prices and quantities are returned as decimal STRINGS, not numbers, to preserve exact precision - do not parse them as floats. Returns { data, next_page }; next_page is an absolute URL or null.
| Name | Required | Description | Default |
|---|---|---|---|
| ids | No | Match any product id in this list. | |
| sku | No | Case-insensitive SUBSTRING match on SKU, not an exact match. Use `skus` for exact matching on one or more full SKUs. | |
| name | No | Case-insensitive substring match on product name. | |
| page | No | 1-indexed page number. Page size is not guaranteed stable; follow next_page instead. | |
| skus | No | Exact case-insensitive match on any SKU in this list. | |
| deleted | No | Whether to include soft-deleted records. Defaults to "no". | |
| brand_ids | No | ||
| is_active | No | ||
| category_ids | No | ||
| updated_datetime | No | Comma-delimited datetime range. "T," = on or after T; ",T" = on or before T; "A,B" = between A and B inclusive. Format: YYYY-MM-DDTHH:MM:SS.MSZ. | |
| has_quantity_active | No | Restrict to products currently holding stock. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnlyHint annotation, the description discloses key behavioral traits: filters combine with AND, list values combine with OR, prices/quantities are decimal strings (with an explicit warning not to parse as floats), and pagination via next_page as an absolute URL or null. These are valuable runtime details not present in annotations or schema.
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 tightly packed sentences: endpoint/scoping, filter semantics, and return format. No filler, front-loaded with the primary action, and each sentence contributes distinct operational knowledge.
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 list tool with 11 optional parameters and no output schema, the description covers the essential runtime context: return envelope, pagination mechanism, and precision caveat. It does not enumerate fields within the returned data items, but the tool name and purpose make those inferable. Sibling context and high schema coverage fill most remaining gaps, though an explicit return-item schema would make it fully complete.
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 schema already describes most parameters at 73% coverage, so the baseline is 3. The description adds meaningful cross-cutting semantics that apply to all parameters: the AND/OR filter combination model and the decimal-string precision warning for price/quantity fields. This goes beyond what the schema provides, though individual parameter meanings are left to the schema.
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 and resource: 'List products from the Distru catalog'. It also names the exact endpoint (GET /public/v1/products), making the operation unambiguous. This clearly distinguishes it from sibling tools like distru_get_product (single product lookup) and distru_get_inventory.
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 gives clear context for filter combination behavior and pagination, implying this is the tool for querying multiple products with filters. However, it does not explicitly state when to prefer this over siblings (e.g., 'use distru_get_product for a single product') or provide any when-not/exclusion guidance. Usage context is implied but not made explicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
distru_match_po_to_catalogMatch PO lines to the catalogARead-only
Match free-form purchase-order lines against the product catalog and report what could NOT be resolved. Read-only; writes nothing. Returns two buckets: matched (one product, unambiguously) and needs_review (why it stopped, plus every candidate considered). It deliberately does not guess when the catalog is ambiguous - duplicate rows for one product, reissued SKUs, colliding category slugs, and conflicting MSRPs all stop the line and are returned with evidence. Treat needs_review as the point of the tool, not as an error: those lines need a human, and presenting a confident answer for them would be wrong.
| Name | Required | Description | Default |
|---|---|---|---|
| lines | Yes | The PO lines to resolve. | |
| flag_price_mismatch | No | Stop a line when its price differs from the catalog price. Default true. | |
| include_catalog_warnings | No | Include structural problems found in the catalog itself. Default true. | |
| flag_insufficient_inventory | No | Stop a line when on-hand quantity cannot cover it. Default true. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, so 'Read-only; writes nothing' only reinforces them. The description goes well beyond annotations by disclosing the two-bucket return shape, the deliberate no-guess policy on ambiguity, the concrete stop conditions (duplicate rows, reissued SKUs, colliding category slugs, conflicting MSRPs), and — most importantly — that needs_review is the tool's point, not a failure signal. That last point prevents an agent from misreading results and retrying.
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?
Five sentences, roughly 100 words, with the core purpose front-loaded. Each sentence earns its place: purpose, safety, output shape, no-guess policy with concrete triggers, and interpretation guidance. There is no repetition of schema content and no 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?
Even without an output schema, the description compensates by specifying the two return buckets and their contents. All four parameters are fully documented in the input schema, annotations cover the safety profile, and the description supplies the interpretation rules an agent needs to act correctly on results. Nothing required to invoke the tool correctly is missing.
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 is 3. The description adds value on top by supplying the domain model behind the flags: conflicting MSRPs motivate flag_price_mismatch, duplicate rows and colliding slugs motivate include_catalog_warnings, and the 'why it stopped' bucket connects to all three toggles. It also frames lines as 'free-form,' which contextualizes the raw field example in the schema.
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?
States a specific verb and resource pair — 'match free-form purchase-order lines against the product catalog' — and immediately clarifies its distinguishing scope: it reports what could NOT be resolved. This clearly separates it from siblings like distru_list_products, distru_get_product, and distru_get_inventory, which list or fetch records rather than resolve them.
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 establishes clear context: use this when you hold free-form PO lines that need resolution against the catalog. It does not, however, explicitly name when-not-to-use cases or point at alternatives, so it falls just short of the explicit routing standard.
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. Dates show when Glama detected each change.
8 tool updates
v0.1.0- First observed
distru_get_inventory - First observed
distru_get_order - First observed
distru_get_product - First observed
distru_list_companies - First observed
distru_list_invoices - First observed
distru_list_orders - First observed
distru_list_products - First observed
distru_match_po_to_catalog
TDQS
Each tool maps to a distinct Distru resource or action: list/get pairs are clearly separated by verb, inventory is a specialized rollup, and match_po_to_catalog handles a unique matching workflow. There is no pair of tools that would satisfy the same user intent.
All tools use the same distru_ prefix and snake_case naming, with list_/get_ following a consistent collection-vs-detail pattern. The longer match_po_to_catalog name still follows the action_resource style and is readable.
8 tools is a well-scoped size for a Distru data-access server. Each of the major read surfaces—products, inventory, orders, invoices, companies, and PO matching—has exactly the tools it needs, with no redundant helpers.
For a read/query-oriented server, the surface covers the core workflows well: product and order detail, inventory rollups, invoices, companies, and PO-to-catalog exception matching are all available. It lacks get-by-id for invoices/companies and offers no mutating operations, so lifecycle management tasks would dead-end, but that appears to be outside this set's read-only design.
Maintenance
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
Verified, pay-per-use API tools for AI agents through one authenticated connection.
Odoo ERP for AI agents: hosted OAuth endpoint, gated writes, one endpoint for every instance.
Agent-native security, trust, reliability, data and procurement tools for AI workflows.
Discover, inspect and run 63,000+ agent tools from one balance. Pay per call, no subscriptions.
1
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables AI agents to interact with Odoo ERP as the authenticated user, with tools for discovery, planning, and mutations bounded by user permissions.34672MIT
- FlicenseNot gradedqualityCmaintenanceEnables controlled AI-agent access to enterprise-shaped tools with a deny-by-default gated write path, human approval, dry-run execution, and append-only audit logging.1-
- FlicenseNot gradedqualityCmaintenanceEnables AI agents to query and manage ERP procurement data, including suppliers, parts, inventory, and purchase orders, with human approval for write operations.-
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to manage Indian enterprise systems by providing tools for accounting (TallyPrime), invoicing (Zoho Books), and GST compliance (validation, e-invoice generation, and GSTR-2B reconciliation), with dry-run-first writes and an audit trail.Apache 2.0
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/claygeo/distru-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server