Skip to main content
Glama
tpiros

Sales Explorer

by tpiros
README.md
# Sales Explorer — a minimal MCP App

A tiny, dependency-light demo of an **MCP App**: an interactive UI that an MCP
server ships into an AI host (Claude, etc.), rendered in a sandboxed iframe right
in the conversation.

The whole point is one moment: **you click a bar and the chart drills down with
no model round-trip** — and the assistant still knows what you're looking at.

## The three beats this demonstrates

1. **UI in the chat, not text.** Ask "show me sales by region" → a bar chart
   renders inline instead of a table of numbers.
2. **Bidirectional, model-free interaction.** Click a region → the iframe calls
   the `get_sales` tool *itself* and re-renders. No new prompt, no model turn.
3. **The model stays in the loop.** After drilling in, the app tells the host
   what you're viewing, so you can then say "email this to my team" and it knows
   what "this" is.

## How it works (the two files that matter)

- `server.ts` — registers a **tool** (`get_sales`) whose `_meta.ui.resourceUri`
  points at a **UI resource** (`ui://sales-explorer/mcp-app.html`). That one line
  of metadata is what turns a normal tool into an MCP App.
- `src/mcp-app.ts` — the iframe app. It renders the chart from the result the
  host pushes in (`app.ontoolresult`), and on a bar click calls
  `app.callServerTool({ name: "get_sales", arguments: { region } })`.

The UI is bundled by Vite into a **single self-contained HTML file**
(`dist/mcp-app.html`) so the server can return it as one resource with no
external asset requests.

## Run it

Requires Node ≥ 22.18 (run TypeScript directly — no `tsx`/`ts-node`).

```bash
npm install
npm run dev        # builds the UI on change + serves MCP over HTTP on :3001
```

- HTTP endpoint: `http://localhost:3001/mcp` (Streamable HTTP, stateless)
- stdio: `npm run start:stdio`

Then point an MCP-Apps-capable host at it (Claude Desktop, MCPJam, VS Code
Copilot, …) and ask: *"show me sales by region."*

## Verify without a host (curl)

```bash
npm start &   # build UI + serve

# list tools — note the _meta.ui.resourceUri on get_sales
curl -s -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -X POST http://localhost:3001/mcp \
     --data '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# the drill-down the iframe performs on a click
curl -s -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -X POST http://localhost:3001/mcp \
     --data '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_sales","arguments":{"region":"EMEA"}}}'
```

## Project layout

```
server.ts          # MCP server: get_sales tool + ui:// resource
main.ts            # transport (Streamable HTTP / stdio)
mcp-app.html       # UI shell (bundled into dist/mcp-app.html)
src/mcp-app.ts     # the iframe app: render chart, click -> callServerTool
src/styles.css     # chart styling (uses host theme variables when provided)
vite.config.ts     # single-file bundle config
```

Built on `@modelcontextprotocol/ext-apps` + `@modelcontextprotocol/sdk`.