Warehouse Triage MCP
by yashgoel1331
README.md
# Warehouse Triage MCP
A remotely hosted TypeScript MCP server that helps warehouse operations investigate delayed inventory stages and create safe, deduplicated human-review escalations.
- Hosted MCP endpoint: `https://warehouse-triage-mcp-production.up.railway.app/mcp`
- Health check: [warehouse-triage-mcp-production.up.railway.app/health](https://warehouse-triage-mcp-production.up.railway.app/health)
- Source: [github.com/yashgoel1331/warehouse-triage-mcp](https://github.com/yashgoel1331/warehouse-triage-mcp)
All records are synthetic. The hosted demo requires no local setup.
Safety patterns follow the same spirit as [fulfillment-triage-mcp](https://github.com/vdhkcheems/fulfillment-triage-mcp): read-only investigation, signed continuation tokens, and an additive escalation write path that re-locks and rechecks evidence before committing.
## Try the hosted workflow
Connect any Streamable HTTP-compatible MCP client to:
```text
https://warehouse-triage-mcp-production.up.railway.app/mcp
```
For Claude, add this URL under **Settings → Connectors → Add custom connector**, enable **Warehouse Triage** in a new chat, and try:
> Investigate PO-847291 in the demo workspace. Explain the current stage, timing, threshold, source-reported signals, and supported next action. Do not create an escalation.
Then explicitly approve the only business mutation:
> Create the human-review escalation using the investigation you just completed. Do not change receiving, putaway, or inventory state yourself.
Finally verify persistence and a refusal:
> Re-investigate PO-847291 in the demo workspace.
> Investigate CASE-WITHIN-THRESHOLD in the demo workspace and try to create an escalation.
Expected results:
- Receiving is overdue after strictly exceeding its eight-hour threshold.
- The operator must explicitly request escalation.
- Creation writes an immutable escalation, audit event, and idempotency result only.
- Re-investigation returns `ALREADY_ESCALATED`.
- The within-threshold request is rejected with `FULFILLMENT_NOT_OVERDUE`.
## Product boundary
The selected user is a warehouse operations specialist investigating inventory that appears stuck in:
| Stage | Escalation threshold |
| --- | --- |
| Receiving | More than 8 hours after ASN arrival |
| Putaway | More than 4 additional hours after receiving completion |
| Discrepancy resolution | More than 12 hours after the earliest unresolved discrepancy flag |
The MCP gathers synthetic purchase-order, receiving, putaway, discrepancy, and existing-escalation evidence. It derives the active stage, calculates elapsed time, distinguishes source observations from proven causes, and recommends a supported next action.
It can create one human-review escalation. It cannot confirm receiving, complete putaway, resolve discrepancies, adjust quantities, cancel a purchase order, or mutate source inventory state.
## MCP surface
| Capability | Type | Purpose |
| --- | --- | --- |
| `investigate_inventory_issue` | Read-only tool | Evaluate one purchase-order or ASN reference in a workspace and return evidence, timing, decision, signals, and a signed continuation token |
| `create_inventory_escalation` | Additive tool | Revalidate the signed investigation and create or safely refuse a deduplicated escalation |
| `warehouse-triage://demo/scenarios` | Resource | Describe the synthetic scenarios available in the demo workspace |
Tool inputs:
- Investigate: `workspace_slug`, `reference`, optional `as_of` (RFC3339)
- Escalate: `continuation_token`, `episode_key`, `idempotency_key`, optional `operator_note`
Some AI clients do not expose MCP resources or `structuredContent` to the model. For compatibility, the investigation’s plain-text content includes the essential decision, detailed source-attributed signals, and exact signed continuation token.
## Why MCP is central
The model does not receive a generic database or HTTP wrapper. The MCP defines the product workflow and safety boundary:
```mermaid
flowchart LR
O["Operations specialist"] --> C["MCP-compatible AI client"]
C --> I["investigate_inventory_issue"]
I --> E["PostgreSQL evidence"]
I --> D["Derived delay decision"]
D -->|"Explicit operator approval"| X["create_inventory_escalation"]
X --> R["Lock and revalidate evidence"]
R --> A["Escalation + audit + idempotency"]
```
The model cannot supply a stage, threshold, elapsed duration, evidence version, or evidence snapshot. Those values are calculated or recovered from trusted server state.
## Safety and correctness
- Source purchase-order, receiving, putaway, and discrepancy records are read-only to MCP tools.
- Threshold equality is not overdue; the stage must strictly exceed its limit.
- Missing or contradictory evidence produces a refusal instead of a guess.
- Source messages are labeled as untrusted observations, not instructions or proven root causes.
- A short-lived HMAC-signed investigation token preserves the exact evidence across clients that open a new MCP session for each tool call.
- Escalation creation locks and reloads evidence, recalculates eligibility, and rejects stale versions.
- Idempotency prevents repeated identical commands; a separate stable episode key prevents duplicates submitted with different keys.
- Escalation, audit event, and idempotency result commit atomically.
- Synthetic workspaces are isolated by `workspace_id`.
- Request body size limits are enforced on `/mcp`.
## Demo scenarios
Seeded under workspace slug `demo` (`npm run db:seed`):
| Reference | Expected investigation |
| --- | --- |
| `PO-847291` | Receiving overdue; escalation supported |
| `CASE-PUTAWAY-OVERDUE` | Putaway overdue; escalation supported |
| `CASE-DISCREPANCY-OVERDUE` | Discrepancy resolution overdue; escalation supported |
| `CASE-WITHIN-THRESHOLD` | Not overdue; escalation refused |
| `CASE-ALREADY-ESCALATED` | Existing escalation returned |
| `CASE-MISSING-EVIDENCE` | Insufficient evidence; no guess |
| `CASE-CONTRADICTORY-RECEIVING` | Contradictory receiving evidence; refused |
## Architecture
- Node.js 22 and strict TypeScript
- Official MCP TypeScript SDK with Streamable HTTP at `/mcp`
- PostgreSQL on Supabase via `pg` (hand-written SQL; no ORM / PostgREST)
- Executable SQL migrations with checksum tracking
- Zod input schemas
- Domain policy separated from repositories, service orchestration, MCP handlers, and HTTP transport
- Railway-hosted Node service; Supabase-hosted PostgreSQL
The schema models demo workspaces, purchase orders, receiving/putaway/discrepancy evidence, escalations, idempotency, and audit events. See [docs/database-schema.md](docs/database-schema.md) for relationships, statuses, indexes, and repository methods.
## Local setup
Prerequisites:
- Node.js 22+
- A PostgreSQL database (Supabase session pooler recommended; IPv4)
Copy `.env.example` to `.env` and set at least:
```bash
DATABASE_URL='postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:5432/postgres?sslmode=require'
INVESTIGATION_TOKEN_SECRET='local-development-secret-at-least-32-characters'
```
URL-encode special characters in the password. Prefer the **session** pooler for `SELECT … FOR UPDATE` safety.
Install, migrate, seed, and run:
```bash
npm ci
npm run db:migrate
npm run db:seed
npm run dev
```
Verify:
```bash
curl http://127.0.0.1:3000/health
```
Use MCP Inspector:
```bash
npx @modelcontextprotocol/inspector
```
Select **Streamable HTTP** and connect to `http://127.0.0.1:3000/mcp`.
## Verification
Run typecheck, unit tests, and build:
```bash
npm run check
```
Run the DB-backed integration suite (uses `TEST_DATABASE_URL` or `DATABASE_URL`):
```bash
npm run test:integration
```
Or both:
```bash
npm run test:all
```
The current suite contains **44 tests** covering:
- Exact SLA boundaries and invalid/contradictory evidence
- Investigation decisions and refusal codes
- Read-only investigation behavior
- Atomic escalation, audit, and idempotency writes
- Stale evidence, duplicate keys, concurrent episode races, and transaction rollback
- Signed-token tampering and expiry
- MCP discovery, schemas, text compatibility, resources, and annotations
- HTTP method, body-size, health, and MCP initialize behavior
The hosted endpoint was also exercised with Streamable HTTP initialize and manually through Claude as an independent AI consumer.
## Decisions, assumptions, and exclusions
Key decisions:
- Prefer one coherent warehouse-inventory workflow over broad OMS/WMS coverage.
- Escalate to a human instead of automating operational recovery.
- Keep the mutation additive and make source-system state unreachable from MCP writes.
- Use realistic PostgreSQL-backed synthetic systems rather than hard-coded tool responses.
- Preserve workflow continuity with a signed capability instead of assuming every MCP client retains a transport session.
- Host Postgres on Supabase and the Node process on Railway.
Assumptions:
- Stage clocks run continuously; calendars and paused SLA windows are out of scope.
- Only the current active stage is eligible.
- Source timestamps and status fields are the available evidence.
- Human review is represented by PostgreSQL escalation/audit rows, not an external ticketing system.
Explicit exclusions:
- Real warehouse, purchase-order, or customer data
- Confirming receiving, completing putaway, resolving discrepancies, quantity adjustments, cancellations, or inventory mutations
- Escalation assignment, acknowledgement, closure, or downstream queue processing
- Authentication, production tenant identity, and a frontend
- Background monitoring or automatic escalation
Known production limitations:
- Authentication is intentionally omitted because the public demo uses isolated synthetic data.
- MCP transport is stateless JSON mode; signed workflow tokens still reference database-backed evidence across fresh sessions.
- `/health` is a process liveness check, not a database-readiness probe.
- Rotating the investigation-token secret invalidates outstanding short-lived tokens.
## Environment and scripts
| Variable | Purpose |
| --- | --- |
| `DATABASE_URL` | Postgres connection string (required) |
| `TEST_DATABASE_URL` | Optional override for integration tests |
| `INVESTIGATION_TOKEN_SECRET` | HMAC secret (≥32 chars) |
| `INVESTIGATION_TOKEN_TTL_MS` | Token TTL (default `900000`) |
| `HOST` / `PORT` | Bind address (default `0.0.0.0:3000`) |
| `MCP_MAX_BODY_BYTES` | Request body limit (default `262144`) |
| `NODE_ENV` | `development` \| `test` \| `production` |
| Script | Action |
| --- | --- |
| `npm run dev` | Watch mode server |
| `npm start` | Migrate then run compiled server |
| `npm run build` / `typecheck` | Compile / typecheck |
| `npm test` | Unit tests |
| `npm run test:integration` | Integration tests against Postgres |
| `npm run test:all` | Unit + integration |
| `npm run db:migrate` | Apply pending SQL migrations |
| `npm run db:seed` | Replace the `demo` workspace with catalog cases |
| `npm run check` | typecheck + unit + build |
## Supporting documents
- [Proposed product and technical plan](docs/proposed-plan.md)
- [Domain contract and safety rules](docs/domain-contract.md)
- [PostgreSQL schema rationale](docs/database-schema.md)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues