Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ npm install -S @modelcontextprotocol/ext-apps

The SDK serves three roles: app developers building interactive Views, host developers embedding those Views, and MCP server authors registering tools with UI metadata.

| Package | Purpose | Docs |
|---------|---------|------|
| `@modelcontextprotocol/ext-apps` | Build interactive Views (App class, PostMessageTransport) | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/app.html) |
| `@modelcontextprotocol/ext-apps/react` | React hooks for Views (useApp, useHostStyles, etc.) | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/_modelcontextprotocol_ext-apps_react.html) |
| `@modelcontextprotocol/ext-apps/app-bridge` | Embed and communicate with Views in your chat client | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/app-bridge.html) |
| `@modelcontextprotocol/ext-apps/server` | Register tools and resources on your MCP server | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/server.html) |
| Package | Purpose | Docs |
| ------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `@modelcontextprotocol/ext-apps` | Build interactive Views (App class, PostMessageTransport) | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/app.html) |
| `@modelcontextprotocol/ext-apps/react` | React hooks for Views (useApp, useHostStyles, etc.) | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/_modelcontextprotocol_ext-apps_react.html) |
| `@modelcontextprotocol/ext-apps/app-bridge` | Embed and communicate with Views in your chat client | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/app-bridge.html) |
| `@modelcontextprotocol/ext-apps/server` | Register tools and resources on your MCP server | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/server.html) |

There's no _supported_ host implementation in this repo (beyond the [examples/basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example).

Expand Down
50 changes: 48 additions & 2 deletions examples/pdf-server/mcp-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
<div id="loading" class="loading">
<div class="spinner"></div>
<p id="loading-text">Loading PDF...</p>
<div id="progress-container" class="progress-container" style="display: none">
<div
id="progress-container"
class="progress-container"
style="display: none"
>
<div id="progress-bar" class="progress-bar"></div>
</div>
<p id="progress-text" class="progress-text"></p>
Expand Down Expand Up @@ -57,16 +61,58 @@
<button id="zoom-in-btn" class="zoom-btn" title="Zoom in (+)">
+
</button>
<button id="fullscreen-btn" class="fullscreen-btn" title="Toggle fullscreen">
<button
id="search-btn"
class="search-btn"
title="Search (Ctrl+F)"
></button>
<button
id="fullscreen-btn"
class="fullscreen-btn"
title="Toggle fullscreen"
>
</button>
</div>
</div>
<!-- Search bar below toolbar, right-aligned -->
<div id="search-bar" class="search-bar" style="display: none">
<input
id="search-input"
class="search-input"
type="text"
placeholder="Search..."
autocomplete="off"
/>
<span id="search-match-count" class="search-match-count"></span>
<button
id="search-prev-btn"
class="search-nav-btn"
title="Previous (Shift+Enter)"
>
&#x25B2;
</button>
<button
id="search-next-btn"
class="search-nav-btn"
title="Next (Enter)"
>
&#x25BC;
</button>
<button
id="search-close-btn"
class="search-nav-btn"
title="Close (Esc)"
>
&#x2715;
</button>
</div>

<!-- Single Page Canvas Container -->
<div class="canvas-container">
<div class="page-wrapper">
<canvas id="pdf-canvas"></canvas>
<div id="highlight-layer" class="highlight-layer"></div>
<div id="text-layer" class="text-layer"></div>
</div>
</div>
Expand Down
157 changes: 153 additions & 4 deletions examples/pdf-server/src/mcp-app.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
--text200: light-dark(#999999, #888888);

/* Shadows */
--shadow-page: light-dark(0 2px 8px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.4));
--shadow-page: light-dark(
0 2px 8px rgba(0, 0, 0, 0.15),
0 2px 8px rgba(0, 0, 0, 0.4)
);
--selection-bg: light-dark(rgba(0, 0, 255, 0.3), rgba(100, 150, 255, 0.4));
}

Expand Down Expand Up @@ -114,6 +117,7 @@ body {
flex-direction: column;
flex: 1;
overflow: visible;
position: relative;
}

/* Toolbar */
Expand Down Expand Up @@ -256,7 +260,10 @@ body {
display: block;
}

/* Text Layer for Selection */
/* Text Layer for Selection
* Critical: must include font-size and transform rules that PDF.js TextLayer
* relies on via CSS custom properties (--font-height, --scale-x, --rotate).
* Without these, text layer spans won't align with the canvas rendering. */
.text-layer {
position: absolute;
left: 0;
Expand All @@ -270,17 +277,35 @@ body {
forced-color-adjust: none;
transform-origin: 0 0;
z-index: 2;
/* PDF.js TextLayer sets --min-font-size on container */
--min-font-size-inv: calc(1 / var(--min-font-size, 1));
}

.text-layer span,
.text-layer br {
.text-layer :is(span, br) {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}

/* PDF.js sets --font-height, --scale-x, --rotate as inline styles on each span.
* These rules apply proper font size and transforms to match the canvas. */
.text-layer > :not(.markedContent),
.text-layer .markedContent span:not(.markedContent) {
z-index: 1;
--font-height: 0;
font-size: calc(var(--scale-factor, 1) * var(--font-height));
--scale-x: 1;
--rotate: 0deg;
transform: rotate(var(--rotate)) scaleX(var(--scale-x))
scale(var(--min-font-size-inv, 1));
}

.text-layer .markedContent {
display: contents;
}

.text-layer ::selection {
background: var(--selection-bg);
}
Expand Down Expand Up @@ -310,3 +335,127 @@ body {
min-height: 0; /* Allow flex item to shrink below content size */
overflow: auto; /* Scroll within the document area only */
}

/* Search Button */
.search-btn,
.nav-btn,
.zoom-btn,
.fullscreen-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid var(--bg200);
border-radius: 4px;
background: var(--bg000);
color: var(--text000);
cursor: pointer;
font-size: 1rem;
transition: all 0.15s ease;
}

.search-btn:hover,
.nav-btn:hover:not(:disabled),
.zoom-btn:hover:not(:disabled),
.fullscreen-btn:hover {
background: var(--bg100);
border-color: var(--bg300);
}

/* Search Bar */
.search-bar {
display: flex;
align-items: center;
position: absolute;
top: 47px; /* below toolbar (48px - 1px border overlap) */
right: -1px; /* align with outer border */
padding: 0.375rem 0.5rem;
background: var(--bg000);
border: 1px solid var(--bg200);
border-top: none;
border-radius: 0 0 6px 6px;
gap: 0.5rem;
z-index: 10;
}

.search-input {
width: 200px;
padding: 0.25rem 0.5rem;
border: 1px solid var(--bg200);
border-radius: 4px;
font-size: 0.85rem;
background: var(--bg000);
color: var(--text000);
}

.search-input:focus {
outline: none;
border-color: var(--text100);
}

.search-match-count {
font-size: 0.8rem;
color: var(--text100);
white-space: nowrap;
min-width: 60px;
}

.search-nav-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid var(--bg200);
border-radius: 4px;
background: var(--bg000);
color: var(--text000);
cursor: pointer;
font-size: 0.8rem;
transition: all 0.15s ease;
}

.search-nav-btn:hover:not(:disabled) {
background: var(--bg100);
border-color: var(--bg300);
}

.search-nav-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}

/* Highlight Layer */
.highlight-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 1;
}

.search-highlight {
background: rgba(255, 255, 0, 0.4);
mix-blend-mode: multiply;
border-radius: 2px;
pointer-events: none;
}

.search-highlight.current {
background: rgba(255, 165, 0, 0.6);
}

@media (prefers-color-scheme: dark) {
.search-highlight {
background: rgba(255, 255, 0, 0.3);
mix-blend-mode: screen;
}

.search-highlight.current {
background: rgba(255, 165, 0, 0.5);
mix-blend-mode: screen;
}
}
Loading
Loading