Skip to main content
Glama
README.md
# Signal Triage — a context-aware MCP App

An [MCP Apps (SEP-1865)](https://modelcontextprotocol.io/extensions/apps/overview) experiment that grew from plumbing demo into a small demo engine: signals from **multiple mock sources** land on one interactive triage board, mapped against a **customer case** (who the signals matter *to*) and rendered in a **brand** (what the customer's world looks like). Both the case and the brand are plain markdown files, swappable at runtime.

## Quick start — see it in two minutes

Requires Node 20+. No API keys, no accounts.

```bash
npm install
npm run build
npm run serve:http     # terminal 1 — MCP server on http://localhost:3001/mcp
npm run shim           # terminal 2 — minimal host on http://localhost:4180
```

Open **http://localhost:4180/host-shim.html**. The shim is a ~100-line reference host: it fetches the UI via `resources/read`, completes the MCP Apps handshake, and proxies every tool call to the real server — the log under the widget shows each message the app sends. Triage some signals (Keep/Drop/Undo, filter by source), then switch to the **Briefing** tab and press *Draft briefing in chat* to watch the v2 hand-back (`ui/update-model-context` + `ui/message`) appear in the log.

Triage decisions persist in `data/decisions.json` (gitignored) — delete it for a fresh board.

## What you'll see

**v1 — triage.** `get_signals` aggregates all sources and maps each signal to the case's focus areas by keyword (whole-word, plural-tolerant). The board shows focus-area chips with kept/matched counts, source filter tabs, per-row relevance chips ("No direct match" = drop candidate), and Keep/Drop/Undo. Every click travels back as a `record_decision` tool call (app-only, invisible to the model).

**v2 — briefing.** The Briefing tab groups kept signals per focus area and flags areas with no kept coverage as **gaps**. "Draft briefing in chat" pushes the full briefing into model context and sends a short trigger message so the host's assistant drafts the customer-ready briefing. UI-less hosts get the same synthesis from the `get_briefing` tool.

Both tools keep plain-text fallbacks, so the server stays useful on hosts without MCP Apps support.

## The five inputs

| Input | File | Role |
|---|---|---|
| Context | [CUSTOMER_CASE.md](CUSTOMER_CASE.md) | An [OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) concept document (`type: Customer Case`); its `focus_areas` frontmatter (id, title, keywords) is the contract every signal is triaged against. |
| Style | [DESIGN.md](DESIGN.md) | A [design.md-spec](https://github.com/VoltAgent/awesome-design-md) token file. [design.ts](design.ts) compiles its frontmatter (colors/typography/rounded) into CSS variables injected into the UI resource at serve time. |
| Data | [data/sources/](data/sources/) | Three mock JSON feeds (external news, market intel, internal ops/HR). Mock by design — no API keys. |
| Knowledge | [.claude/skills/signal-triage/](.claude/skills/signal-triage/SKILL.md) | An agent skill whose reference files form an OKF bundle: the case contract, one doc per source, and a triage playbook. |
| Stories | [USER_STORIES.md](USER_STORIES.md) | Stories 1–2 produced **v1** (git tag `v1`); story 3 produced **v2** (tag `v2`). |

## Swapping the inputs (no rebuild, no restart)

Both context and style are **runtime** inputs — the server re-reads `CUSTOMER_CASE.md` on every tool call, and compiles `DESIGN.md` into the UI resource every time it's served:

```bash
# Switch customer: every signal re-maps to the new focus areas
cp cases/norrsken-freight.md CUSTOMER_CASE.md

# Switch brand: the widget re-renders in the new design tokens
cp designs/nordfrakt.design.md DESIGN.md
```

Reload the shim (or let the host re-read tool + resource) and the same 14 signals appear re-mapped and re-branded. Ready-made examples live in [cases/](cases/) and [designs/](designs/). Unknown or missing design tokens fall back to built-in defaults, and host-provided style variables always win over both.

Where the files come from is the point of the pipeline:

- **CUSTOMER_CASE.md** is the planned bridge target for the *organisational-stress-test* project — one `focus_areas` entry per stressed area. Contract details: [customer case contract](.claude/skills/signal-triage/references/context/customer-case-contract.md).
- **DESIGN.md** is what [design-md-generator](https://github.com/SwePalm/design-md-generator) produces from a live website URL — point it at a customer's site and the demo renders in their brand.

Two verified portability experiments back this up: swapping only the style left every behavior identical, and swapping only the case (Norrsken Freight, a 3PL → Kvist & Söner, an industrial wholesaler) inverted relevance — the warehouse-robotics signal that was noise for Norrsken became Kvist's automation signal, while Norrsken's top talent signal dropped to "No direct match". Relevance is a property of the case, not the signal.

## Using it from a real host

For stdio hosts (Claude Desktop / Claude Code style config):

```json
{
  "mcpServers": {
    "signal-triage": {
      "command": "node",
      "args": ["/absolute/path/to/dist/main.js", "--stdio"]
    }
  }
}
```

For the SDK's reference host: clone [`ext-apps`](https://github.com/modelcontextprotocol/ext-apps), `cd examples/basic-host`, `npm install` (add `tsx`, `cross-env`, `@types/cors` as dev deps if installing standalone), `NODE_ENV=development npm run build`, then `npx tsx serve.ts` and open `http://localhost:8080` while this server runs on :3001. No `bun` required.

`npm run dev` runs the Vite watcher plus the HTTP server together for iteration (UI changes need a rebuild — the served resource is the compiled `dist/mcp-app.html`).

## Stack

Official SDK + patterns: [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps), vanilla TS/HTML UI bundled to a single file (Vite + `vite-plugin-singlefile`), Node + Express for Streamable HTTP, stdio also supported. Frontmatter parsed with `yaml`.

## Versions

- `v1` — user stories 1–2: multi-source board + case relevance.
- `v2` — user story 3: briefing view, gaps, hand-back to chat.
- after `v2`: portability experiments, then runtime ingestion of both swappable inputs.

The pre-build design sketch this repo started from is preserved on the [`initial-draft`](../../tree/initial-draft) branch.

## Learnings

The reflective deliverable of the original experiment is [LEARNINGS.md](LEARNINGS.md) — including a v2 addendum on what building "one level up" from the plumbing taught. The original scope-setting PRD is preserved as [MCP_App_Learning_PRD.md](MCP_App_Learning_PRD.md).