From a3f53899af9efcc275ac8ad97215f6af8b63d65d Mon Sep 17 00:00:00 2001
From: SudNitro23
Date: Tue, 16 Jun 2026 10:36:51 +0100
Subject: [PATCH] PWC-8: Inline Nitro PDF viewer in Claude
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Render PDFs inside the Claude conversation using Nitro's nitro-pdf-reader
web component, folded into one self-contained MCP-App HTML — no browser
popup, no network, no second login.
- view_pdf tool mounts the viewer as an MCP-App resource; PDF bytes stream
over the MCP transport via get_pdf_for_viewer (the sandbox CSP blocks all
outbound network).
- scripts/inline-viewer.mjs bundles the multi-file public-reader build into
one HTML: app chunks + worker inlined, Pdfium WASM fed via emscripten
wasmBinary, Kendo CSS + IBM Plex fonts inlined as data: URLs.
- scripts/viewer-bridge.js connects via @modelcontextprotocol/ext-apps,
loads the file, reroutes Download through the host, and adds a widescreen
toggle.
- save_pdf_edits is a stub pending a component save event.
- Remove the legacy popup/companion editor and its express/open deps.
Co-Authored-By: Claude Opus 4.8
---
.gitignore | 4 +
CLAUDE.md | 12 +
docs/claude-mcp-app-build-guide.md | 121 +
node-version/.prettierignore | 4 +
node-version/build.mjs | 18 +
node-version/eslint.config.ts | 2 +-
node-version/package-lock.json | 33 +-
node-version/package.json | 3 +-
node-version/scripts/inline-viewer.mjs | 279 +
node-version/scripts/viewer-bridge.js | 240 +
node-version/src/assets/mcp-app.html | 119148 ++++++++++++++++++++++
node-version/src/tools/index.ts | 2 +
node-version/src/tools/viewer.ts | 174 +
13 files changed, 120036 insertions(+), 4 deletions(-)
create mode 100644 docs/claude-mcp-app-build-guide.md
create mode 100644 node-version/.prettierignore
create mode 100644 node-version/scripts/inline-viewer.mjs
create mode 100644 node-version/scripts/viewer-bridge.js
create mode 100644 node-version/src/assets/mcp-app.html
create mode 100644 node-version/src/tools/viewer.ts
diff --git a/.gitignore b/.gitignore
index 0e7722f..4a58a83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -98,3 +98,7 @@ tmp*.py
node-version/dist/
node-version/node_modules/
node-version/coverage/
+
+
+# Local test documents
+samples/
diff --git a/CLAUDE.md b/CLAUDE.md
index 7bacaca..50a507b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -13,8 +13,20 @@ MCP server connecting Claude Desktop to Nitro's Document Intelligence Platform A
- `src/handlers/` — `PlatformHandler` (operations) and `FilesHandler` (local I/O)
- `src/tools/` — MCP tool implementations
- `src/auth/` — PKCE auth flow and token management
+- `src/assets/mcp-app.html` — prebuilt single-file Nitro PDF viewer (generated by `scripts/inline-viewer.mjs`, copied to `dist/assets/` by `build.mjs`)
+- `scripts/` — viewer build pipeline (`inline-viewer.mjs`, `viewer-bridge.js`)
- `tests/` — Vitest test suite
+## Inline PDF Viewer
+
+`view_pdf` renders a PDF in Nitro's reader directly inside the Claude conversation (MCP-App iframe — no browser popup, no network, no second login). Implementation: `src/tools/viewer.ts`.
+
+- The viewer is the `nitro-pdf-reader` Angular web component from the frontend repo (`apps/public-reader`, `nx build public-reader`).
+- The MCP-App sandbox has no origin and blocks all network, so `scripts/inline-viewer.mjs` folds the multi-file build into ONE self-contained HTML (`src/assets/mcp-app.html`, ~26 MB): app chunks collapsed via esbuild, worker as Blob URL, Pdfium WASM injected via emscripten `wasmBinary` (sandbox CSP blocks `fetch()` of even `data:` URLs), Kendo CSS + IBM Plex fonts inlined as `data:` URLs.
+- `scripts/viewer-bridge.js` (bundled into the HTML) connects via `@modelcontextprotocol/ext-apps`, receives `{filePath}` from the tool result, calls `get_pdf_for_viewer` over the MCP transport, and assigns the bytes to the component's `file` property.
+- To rebuild the viewer after a frontend change: `node scripts/inline-viewer.mjs /dist/apps/public-reader/app/en`
+- `save_pdf_edits` is a stub — round-trip (viewer edits back to Claude) is pending the component exposing a save event.
+
## Commands
Run from the repo root using the `n:` namespace (aliased from `node-version/Taskfile.yml`):
diff --git a/docs/claude-mcp-app-build-guide.md b/docs/claude-mcp-app-build-guide.md
new file mode 100644
index 0000000..4fb3dd6
--- /dev/null
+++ b/docs/claude-mcp-app-build-guide.md
@@ -0,0 +1,121 @@
+# Building for Claude's Inline MCP-App Sandbox
+
+A guide for frontend developers integrating a viewer/editor component into Claude Desktop via the MCP-App extension API.
+
+---
+
+## What is the MCP-App Sandbox?
+
+Claude Desktop supports a feature called **MCP Apps** (`@modelcontextprotocol/ext-apps`). An MCP tool can register a UI resource (`text/html;profile=mcp-app`) and Claude will render it inline in the conversation — no browser popup, no second login.
+
+The renderer is a sandboxed iframe. The sandbox has one hard constraint:
+
+> **All outbound network is blocked by CSP.** No CDN fetches, no API calls, no localhost, no external fonts or stylesheets.
+
+Everything the viewer needs must be **self-contained inside the HTML string** that the MCP server returns.
+
+---
+
+## The Single-File Requirement
+
+The MCP resource API returns a single string of HTML. There is no concept of sibling files — you cannot reference `main.js`, `styles.css`, or a `.wasm` file sitting next to the HTML. If your build output is a folder of files, none of those files are reachable from the iframe.
+
+**This means:**
+- All JavaScript must be inlined into `',
+ );
+ // Drop the dev-harness script (it sets `showOpenButton = true` for local
+ // file picking). In Claude the file arrives over MCP — no Open button.
+ html = html.replace(/` : '') +
+ `` +
+ ``;
+ html = html.replace('
+
+
+
+