Skip to main content
Glama
yashgoel1331

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)