Skip to main content
Glama
claygeo
by claygeo

distru-mcp

CI

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 smoke

npm 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 install

To 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

distru_list_products

GET /public/v1/products

distru_get_product

GET /public/v1/products/{id}

distru_get_inventory

GET /public/v1/inventory

distru_list_orders

GET /public/v1/orders

distru_get_order

GET /public/v1/orders/{id}

distru_list_invoices

GET /public/v1/invoices

distru_list_companies

GET /public/v1/companies

distru_match_po_to_catalog

(local analysis, no endpoint)

Write tool, registered only when DISTRU_ALLOW_WRITES=true:

Tool

Endpoint

distru_draft_sales_order

POST /public/v1/orders


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:

  1. 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.

  2. brand|name|size duplicate rows. Same product entered twice under unrelated SKU conventions, usually because two people onboarded the same vendor. No reissue relationship to key off.

  3. Category slug collision. Pre-Rolls and Prerolls exist as two distinct category records normalising to the same slug. Any filter on one silently drops the other.

  4. 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_line

Two 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

no_match

yes

Nothing in the catalog resembles the line.

ambiguous_match

yes

Two or more products scored within the tie band. Nothing on the line separates them.

line_contradicts_sku

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.

unknown_sku

yes

The line states a SKU the catalog has never heard of. Any candidate shown was found by text similarity, not by that identifier.

sku_collision

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.

reissued_sku_pair

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.

duplicate_identity

yes

Duplicate rows share brand, name, and size under unrelated SKU conventions. Nothing in the data says which is canonical.

category_slug_collision

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.

msrp_conflict

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.

size_unspecified

yes

The line names no size and the catalog matches its text at more than one.

pack_unspecified

yes

The line names no pack count and the tied candidates split between singles and multipacks.

size_unverified

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.

price_mismatch

yes

The line prices the item differently than the catalog does. A price override is a commercial decision, not a matching decision.

insufficient_inventory

yes

The resolved product cannot cover the requested quantity — and a duplicate row for the same product may hold the rest.

inactive_product

yes

The product resolved cleanly and is no longer active.

unparseable_line

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, and test/live-client.test.ts drives it against a stub fetch to 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 run

The 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 tsx

CI 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. Follow next_page rather than counting rows. The client refuses to follow a next_page pointing 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, ,T is on or before, A,B is 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 /inventory requires groupings, and it must include PRODUCT. Each grouping adds its id field to the rows; omitted groupings mean the field is absent, not null. Grouping by BATCH_NUMBER drops product-tracked products and forces reserved to "0".

  • Writes are upserts. Omit id to create, include it to update. On update, omitting a field leaves it unchanged and sending null clears it — but collections are replace-in-full, so a partial items array 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 tools
distru_get_inventoryGet inventory levelsA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
pageNo1-indexed page number. Page size is not guaranteed stable; follow next_page instead.
groupingsYesAttributes to roll up by. Must include PRODUCT. Order sets the sort order.
product_idsNo
location_idsNo
product_skusNo

TDQS

A4.6/5.0
Behavior5/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 orderA
Read-only

Fetch one sales order by id (GET /public/v1/orders/{id}), including its line items, charges, and any linked invoices.

ParametersJSON Schema
NameRequiredDescriptionDefault
idYesThe order UUID.

TDQS

A4.3/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 productA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
idYesThe product UUID.

TDQS

A4.7/5.0
Behavior5/5

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

Beyond the readOnlyHint and openWorldHint annotations, the description discloses 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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 companiesA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
idsNo
nameNoCase-insensitive substring match.
pageNo1-indexed page number. Page size is not guaranteed stable; follow next_page instead.
namesNoExact match on any name in this list.
deletedNoWhether to include soft-deleted records. Defaults to "no".
categoryNo
updated_datetimeNoComma-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

A4.2/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 invoicesA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
idsNo
pageNo1-indexed page number. Page size is not guaranteed stable; follow next_page instead.
statusesNo
is_voidedNo
order_idsNo
company_idsNo
invoice_numberNoSubstring match on invoice number.
invoice_datetimeNoComma-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

A3.9/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness3/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines3/5

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 ordersA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
idsNo
pageNo1-indexed page number. Page size is not guaranteed stable; follow next_page instead.
statusesNo
company_idsNoBuyer company relationship ids.
product_idsNoRestrict to orders containing any of these products.
order_numberNoSubstring match on order number.
order_numbersNoExact match on any order number.
order_datetimeNoComma-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_datetimeNo

TDQS

A3.8/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 productsA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
idsNoMatch any product id in this list.
skuNoCase-insensitive SUBSTRING match on SKU, not an exact match. Use `skus` for exact matching on one or more full SKUs.
nameNoCase-insensitive substring match on product name.
pageNo1-indexed page number. Page size is not guaranteed stable; follow next_page instead.
skusNoExact case-insensitive match on any SKU in this list.
deletedNoWhether to include soft-deleted records. Defaults to "no".
brand_idsNo
is_activeNo
category_idsNo
updated_datetimeNoComma-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_activeNoRestrict to products currently holding stock.

TDQS

A4.4/5.0
Behavior5/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose5/5

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

The description opens with a specific verb 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.

Usage Guidelines3/5

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 catalogA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
linesYesThe PO lines to resolve.
flag_price_mismatchNoStop a line when its price differs from the catalog price. Default true.
include_catalog_warningsNoInclude structural problems found in the catalog itself. Default true.
flag_insufficient_inventoryNoStop a line when on-hand quantity cannot cover it. Default true.

TDQS

A4.7/5.0
Behavior5/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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

Schema coverage is 100%, so the baseline is 3. The description adds 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.

Purpose5/5

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.

Usage Guidelines4/5

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.

  1. 8 tool updatesv0.1.0
    • First observeddistru_get_inventory
    • First observeddistru_get_order
    • First observeddistru_get_product
    • First observeddistru_list_companies
    • First observeddistru_list_invoices
    • First observeddistru_list_orders
    • First observeddistru_list_products
    • First observeddistru_match_po_to_catalog

TDQS

A4.3/5.0
Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness4/5

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

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

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

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables AI agents to interact with Odoo ERP as the authenticated user, with tools for discovery, planning, and mutations bounded by user permissions.
    34
    67
    2
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables 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
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables 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

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