Skip to main content
Glama
README.md
# Zava Relocation MCP UI Demo

<a href="demo/zava-relocation-demo.mp4"><img src="demo/zava-relocation-demo-preview.png" alt="Watch the 2-minute Zava Relocation demo" width="600"></a>

**Watch the demo:** [2-minute narrated walkthrough](demo/zava-relocation-demo.mp4)

Zava Relocation Inc. helps employees move for a new job. This project is a reference demo for building an interactive **MCP App** with [MCP-UI](https://github.com/MCP-UI-Org/mcp-ui), a local Qwen2.5 7B model, and a synchronized conversational intake form.

The user can chat with Ava, upload an offer letter, or edit the profile directly. Extracted information is applied to the form immediately and highlighted so the user can review what changed.

For a customer-facing teaching walkthrough, see the dedicated [MCP UI + LLM form-fill guide](C:/Users/stflower/code/projects/mcp-ui/docs/mcp-ui-llm-form-fill-guide.md).

## What the demo demonstrates

- Chat-driven form completion with live field updates
- Optional browser voice mode: speak to Ava and hear Qwen responses aloud
- PDF and DOCX offer-letter extraction in the browser
- Local interpretation with Qwen2.5 7B through Foundry Local
- Five profile sections: contact, employment, move, moving logistics, and preferences
- Grounded moving-option and reimbursement guidance from the fictional Contoso policy PDF
- Progress tracking, highlighted AI updates, reset, and completion state
- Responsive desktop and mobile layouts
- MCP Apps resource/tool linkage through `@mcp-ui/server` and `@modelcontextprotocol/ext-apps`
- A production build that inlines the UI into one HTML resource

> **Demo boundary:** This is a local prototype. It does not persist relocation cases, authenticate users, or submit data to a production HR system. The sample PDFs in [`sample-documents/`](C:/Users/stflower/code/projects/mcp-ui/sample-documents) contain fictional data.

## Architecture

```text
MCP Apps host
      |
      | Streamable HTTP: POST /mcp
      v
Node + Express MCP server
      |-- start_relocation_intake tool
      |-- ui://zava-relocation/intake resource
      |-- POST /api/chat
      v
Foundry Local (same machine)
      |
      v
Qwen2.5 7B

Browser UI
  |-- PDF.js / Mammoth extract document text locally
  |-- regex extractor gives immediate form updates
  |-- /api/chat sends text and current form to local Qwen
```

There are two ways to use the UI:

1. **Standalone mode:** Vite serves the React application at `http://localhost:5173`.
2. **MCP App mode:** An MCP Apps-compatible host connects to `http://localhost:3001/mcp`, discovers `start_relocation_intake`, and renders the linked `ui://zava-relocation/intake` resource.

## How MCP-UI is leveraged

This project uses the MCP Apps pattern recommended by MCP-UI:

1. [`server/index.ts`](C:/Users/stflower/code/projects/mcp-ui/server/index.ts) creates an `McpServer` and a `StreamableHTTPServerTransport`.
2. The production `dist/index.html` is loaded into a UI resource using `createUIResource`.
3. `registerAppResource` publishes that resource at `ui://zava-relocation/intake`.
4. `registerAppTool` exposes `start_relocation_intake` and links it to the UI with:

   ```ts
   _meta: {
     ui: { resourceUri: relocationUI.resource.uri },
   }
   ```

5. The embedded UI signals readiness with `ui-lifecycle-iframe-ready` and can send host messages with `window.parent.postMessage`.

The important distinction is that the MCP server does not render the form itself. It registers the tool and UI resource; the MCP Apps host decides where and how to display that resource.

## Foundry Local and Qwen2.5 7B

The assistant uses **Qwen2.5 7B through Foundry Local**. Foundry Local runs on the same machine as this Node server and exposes an OpenAI-compatible local chat-completions endpoint. No cloud model fallback is configured.

### Prerequisites

Foundry Local support depends on the host machine. On Windows, Microsoft documents Windows 11 24H2 or later, .NET 9 or later, and a DirectX 12-capable GPU for the Windows ML runtime.

Install the Foundry Local CLI:

```powershell
winget install Microsoft.FoundryLocal
```

Close and reopen PowerShell, then verify the CLI:

```powershell
foundry --version
```

List the model aliases available in the local catalog:

```powershell
foundry model list
```

Start or download the Qwen model using the alias shown by the catalog. The expected demo alias is:

```powershell
foundry model run qwen2.5-7b
```

Keep Foundry Local running while using the demo. The current Foundry Local service endpoint for this project is:

```text
http://127.0.0.1:61563/v1/chat/completions
```

If the installed catalog uses a different alias or port, configure the Node server before starting it:

```powershell
$env:FOUNDRY_LOCAL_ENDPOINT = "http://127.0.0.1:<actual-port>/v1/chat/completions"
$env:FOUNDRY_LOCAL_MODEL = "qwen2.5-7b-instruct-cuda-gpu"
```

Foundry Local assigns the service port dynamically. Use `foundry service status` or `foundry service list` to inspect the active service, and use `GET http://127.0.0.1:<port>/openai/models` to list available model IDs. The exact model ID can vary by hardware; on this machine the available Qwen GPU model is `qwen2.5-7b-instruct-cuda-gpu`.

### What the model receives

[`server/foundryLocal.ts`](C:/Users/stflower/code/projects/mcp-ui/server/foundryLocal.ts) sends Qwen:

- The latest user message or a document-review instruction
- The current form state
- Up to 8,000 characters of extracted document text

The system prompt asks Qwen to return JSON in this shape:

```json
{
  "reply": "I found your new employer and start date.",
  "fields": {
    "employer": "Northstar Analytics",
    "role": "Senior Product Manager",
    "startDate": "2026-10-07"
  }
}
```

Only allowlisted form keys are accepted by the server. The model cannot add arbitrary fields to the client state.

### Voice mode

Qwen2.5 7B remains a text-only model. Voice mode uses browser capabilities around the existing text pipeline:

```text
microphone
  -> browser SpeechRecognition
  -> transcript
  -> POST /api/chat
  -> Foundry Local + Qwen
  -> text reply and form fields
  -> browser SpeechSynthesis
  -> spoken Ava response
```

Click the microphone button in the composer to speak. When recognition ends, the transcript is submitted through the same chat flow used by typed messages. The **Ava voice on/off** control enables or disables spoken replies, **Voice** lets you choose an installed browser voice, and **Stop Ava** interrupts the current response. The app prefers Microsoft/Edge natural English voices when available, such as Ava, Jenny, Aria, or Sonia. Chrome and Edge provide the best support; microphone permission is required and voice input needs localhost or HTTPS. Voice quality depends on the voices installed and exposed by the browser.

Voice input uses a guided one-field-at-a-time flow. The app identifies the next incomplete required field, asks Qwen to focus on that field, advances the active form section after the answer, and speaks one short next question. This keeps each voice turn easy to remember. Typed chat remains free-form.

## Document parsing flow

The browser handles the original file; the file itself is not uploaded to a cloud service:

1. [`src/App.tsx`](C:/Users/stflower/code/projects/mcp-ui/src/App.tsx) validates the extension and 10 MB limit.
2. [`src/documentParser.ts`](C:/Users/stflower/code/projects/mcp-ui/src/documentParser.ts) uses PDF.js for PDFs and Mammoth for DOCX files.
3. The extracted text is sent to local Qwen through `POST /api/chat` for document interpretation.
4. Qwen returns structured fields. The UI uses a consistent document-review message telling the user to review the form and manually complete any missing information; it does not enumerate missing fields.
5. The model fields are applied and highlighted in the form.

### Grounded logistics walkthrough

Upload [`contoso-moving-offers-and-reimbursement-guide.pdf`](C:/Users/stflower/code/projects/mcp-ui/sample-documents/contoso-moving-offers-and-reimbursement-guide.pdf), then ask Ava questions such as “Which option is best for a 250-mile move?” or “Can I use a rented truck for 150 miles?” The browser keeps the extracted policy text as grounding for later chat turns, and the model is instructed to answer policy questions only from that text. The Moving logistics section captures the selected method, approximate distance, reimbursement path, and notes.

The browser-side PDF.js and Mammoth libraries are text extraction utilities only; they do not decide which values belong in the relocation form. Foundry Local/Qwen is the source of truth for PDF/DOCX field extraction. If the model is unavailable, the UI reports the error instead of silently filling document fields with a non-LLM parser.

## Code breadcrumbs

| Area | File | Purpose |
| --- | --- | --- |
| Main UI | [`src/App.tsx`](C:/Users/stflower/code/projects/mcp-ui/src/App.tsx) | Chat, form sections, uploads, reset, progress, MCP host messages |
| Styling | [`src/styles.css`](C:/Users/stflower/code/projects/mcp-ui/src/styles.css) | Zava layout, responsive behavior, light/dark theme variables |
| Form types | [`src/types.ts`](C:/Users/stflower/code/projects/mcp-ui/src/types.ts) | `IntakeForm`, `FormField`, `Message`, and blank initial state |
| PDF/DOCX parsing | [`src/documentParser.ts`](C:/Users/stflower/code/projects/mcp-ui/src/documentParser.ts) | Browser-side PDF.js and Mammoth extraction |
| Immediate extraction | [`src/extraction.ts`](C:/Users/stflower/code/projects/mcp-ui/src/extraction.ts) | Labeled values, dates, phone, email, and relocation phrase matching |
| Local LLM client | [`server/foundryLocal.ts`](C:/Users/stflower/code/projects/mcp-ui/server/foundryLocal.ts) | OpenAI-compatible request, JSON validation, field allowlist |
| MCP server | [`server/index.ts`](C:/Users/stflower/code/projects/mcp-ui/server/index.ts) | Express routes, MCP transport, tool/resource registration |
| Dev proxy | [`vite.config.ts`](C:/Users/stflower/code/projects/mcp-ui/vite.config.ts) | Proxies browser `/api` calls to port 3001 |
| Sample files | [`sample-documents/`](C:/Users/stflower/code/projects/mcp-ui/sample-documents) | Fictional offer letters for upload testing |
| Single-file build | [`vite.config.ts`](C:/Users/stflower/code/projects/mcp-ui/vite.config.ts) | `vite-plugin-singlefile` inlines JavaScript and CSS |

## Install and run

Install Node dependencies:

```powershell
npm install
```

### Standalone development mode

Start Vite and the MCP server together:

```powershell
npm run dev
```

Open:

```text
http://localhost:5173
```

The Vite `/api` proxy forwards local model requests to port 3001.

### MCP Apps mode

Build the UI first. The MCP server embeds the resulting `dist/index.html`:

```powershell
npm run build
npm start
```

Configure the MCP Apps-compatible host with:

```text
http://localhost:3001/mcp
```

Then call:

```text
start_relocation_intake
```

The server also exposes a basic health check:

```text
http://localhost:3001/health
```

## Demo workflow

1. Start Foundry Local and make the Qwen model available.
2. Run `npm run dev`.
3. Click a quick prompt or type a relocation message.
4. Watch the matching fields populate and highlight.
5. Upload one of the PDFs in [`sample-documents/`](C:/Users/stflower/code/projects/mcp-ui/sample-documents).
6. Review the extracted and model-enriched fields.
7. Use **Reset demo** to return to a blank state.

Useful chat prompts:

- `I'm moving from Seattle to Austin for a role at Contoso.`
- `My family has 3 people.`
- `Employer: Fabrikam`
- `Position: Senior Product Manager`
- `I need temporary housing.`

## Troubleshooting

### `Could not connect to Foundry Local`

Check that Foundry Local is running, that the model has been downloaded/loaded, and that the endpoint matches `FOUNDRY_LOCAL_ENDPOINT`.

### `Model not found`

Run `foundry model list` and set `FOUNDRY_LOCAL_MODEL` to an alias in the installed catalog.

### The MCP server says `Missing dist/index.html`

Run:

```powershell
npm run build
```

before `npm start`.

### The form does not find fields in a document

The PDF must contain selectable text. Scanned/image-only PDFs need OCR before PDF.js can extract useful text. Labeled values such as `Employee name:`, `Email address:`, `New employer:`, `Job title:`, `Start date:`, `Moving from:`, and `Moving to:` are easiest for the deterministic extractor to recognize.

## Scripts

| Command | Purpose |
| --- | --- |
| `npm run dev` | Start Vite and the MCP server in watch mode |
| `npm run dev:ui` | Start only Vite |
| `npm run dev:mcp` | Start only the MCP server in watch mode |
| `npm run build` | Type-check and create the single-file production UI |
| `npm start` | Start the MCP server against `dist/index.html` |
| `npm run preview` | Preview the Vite production build |

To regenerate the fictional Contoso policy PDF, install the script dependency and run the generator:

```powershell
python -m pip install -r scripts/requirements.txt
python scripts/generate_contoso_policy_pdf.py
```