From dae3025b33999be948d8b2ac189c74bfc93e5dad Mon Sep 17 00:00:00 2001 From: ochafik Date: Thu, 12 Feb 2026 10:02:15 +0000 Subject: [PATCH 01/13] Add PDF search feature to pdf-server Adds full-text search functionality to the PDF viewer: - Search button in toolbar (Ctrl/Cmd+F to open) - Search bar with input, prev/next navigation, match count - Text extraction and caching across all pages - Highlight overlays using DOM Range API - Case-insensitive search across entire document - Keyboard shortcuts: Ctrl+F (open), Enter (next), Shift+Enter (prev), Escape (close) Changes: - mcp-app.html: Add search button, search bar, highlight layer - src/mcp-app.css: Add search/highlight styles and PDF.js text layer rules - src/mcp-app.ts: Add search state, functions, event listeners Co-Authored-By: Claude Opus 4.6 --- examples/pdf-server/mcp-app.html | 10 + examples/pdf-server/src/mcp-app.css | 147 +++++++++++++- examples/pdf-server/src/mcp-app.ts | 303 +++++++++++++++++++++++++++- 3 files changed, 454 insertions(+), 6 deletions(-) diff --git a/examples/pdf-server/mcp-app.html b/examples/pdf-server/mcp-app.html index cf217235d..cb97002c9 100644 --- a/examples/pdf-server/mcp-app.html +++ b/examples/pdf-server/mcp-app.html @@ -57,16 +57,26 @@ + + +
+
diff --git a/examples/pdf-server/src/mcp-app.css b/examples/pdf-server/src/mcp-app.css index 7600be515..1aa476e29 100644 --- a/examples/pdf-server/src/mcp-app.css +++ b/examples/pdf-server/src/mcp-app.css @@ -256,7 +256,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; @@ -270,10 +273,11 @@ 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; @@ -281,6 +285,22 @@ body { 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); } @@ -310,3 +330,124 @@ 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; + padding: 0.375rem 1rem; + background: var(--bg000); + border-bottom: 1px solid var(--bg200); + flex-shrink: 0; + gap: 0.5rem; +} + +.search-input { + flex: 1; + min-width: 120px; + max-width: 300px; + 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; + } +} diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index ad2b8bf0f..93beecb52 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -69,6 +69,30 @@ const fullscreenBtn = document.getElementById( const progressContainerEl = document.getElementById("progress-container")!; const progressBarEl = document.getElementById("progress-bar")!; const progressTextEl = document.getElementById("progress-text")!; +const searchBtn = document.getElementById("search-btn") as HTMLButtonElement; +searchBtn.innerHTML = ``; +const searchBarEl = document.getElementById("search-bar")!; +const searchInputEl = document.getElementById("search-input") as HTMLInputElement; +const searchMatchCountEl = document.getElementById("search-match-count")!; +const searchPrevBtn = document.getElementById("search-prev-btn") as HTMLButtonElement; +const searchNextBtn = document.getElementById("search-next-btn") as HTMLButtonElement; +const searchCloseBtn = document.getElementById("search-close-btn") as HTMLButtonElement; +const highlightLayerEl = document.getElementById("highlight-layer")!; + +// Search state +interface SearchMatch { + pageNum: number; + index: number; + length: number; +} + +let searchOpen = false; +let searchQuery = ""; +let searchDebounceTimer: ReturnType | null = null; +const pageTextCache = new Map(); +const pageTextItemsCache = new Map(); +let allMatches: SearchMatch[] = []; +let currentMatchIndex = -1; // Track current display mode let currentDisplayMode: "inline" | "fullscreen" = "inline"; @@ -106,16 +130,231 @@ function requestFitToContent() { const paddingBottom = parseFloat(containerStyle.paddingBottom); // Calculate required height: - // toolbar + padding-top + page-wrapper height + padding-bottom + buffer + // toolbar + search-bar + padding-top + page-wrapper height + padding-bottom + buffer const toolbarHeight = toolbarEl.offsetHeight; + const searchBarHeight = searchOpen ? searchBarEl.offsetHeight : 0; const pageWrapperHeight = pageWrapperEl.offsetHeight; const BUFFER = 10; // Buffer for sub-pixel rounding and browser quirks const totalHeight = - toolbarHeight + paddingTop + pageWrapperHeight + paddingBottom + BUFFER; + toolbarHeight + searchBarHeight + paddingTop + pageWrapperHeight + paddingBottom + BUFFER; app.sendSizeChanged({ height: totalHeight }); } +// --- Search Functions --- + +async function extractAllPageText() { + if (!pdfDocument) return; + for (let i = 1; i <= totalPages; i++) { + if (pageTextCache.has(i)) continue; + try { + const page = await pdfDocument.getPage(i); + const textContent = await page.getTextContent(); + const items = (textContent.items as Array<{ str?: string }>).map( + (item) => item.str || "", + ); + pageTextItemsCache.set(i, items); + pageTextCache.set(i, items.join("")); + } catch (err) { + log.error("Error extracting text for page", i, err); + } + } +} + +function performSearch(query: string) { + allMatches = []; + currentMatchIndex = -1; + searchQuery = query; + + if (!query) { + updateSearchUI(); + clearHighlights(); + return; + } + + const lowerQuery = query.toLowerCase(); + for (let pageNum = 1; pageNum <= totalPages; pageNum++) { + const pageText = pageTextCache.get(pageNum); + if (!pageText) continue; + const lowerText = pageText.toLowerCase(); + let startIdx = 0; + while (true) { + const idx = lowerText.indexOf(lowerQuery, startIdx); + if (idx === -1) break; + allMatches.push({ pageNum, index: idx, length: query.length }); + startIdx = idx + 1; + } + } + + // Set current match to first match on or after current page + if (allMatches.length > 0) { + const idx = allMatches.findIndex((m) => m.pageNum >= currentPage); + currentMatchIndex = idx >= 0 ? idx : 0; + } + + updateSearchUI(); + renderHighlights(); + + // Navigate to match page if needed + if (allMatches.length > 0 && currentMatchIndex >= 0) { + const match = allMatches[currentMatchIndex]; + if (match.pageNum !== currentPage) { + goToPage(match.pageNum); + } + } +} + +function renderHighlights() { + clearHighlights(); + if (!searchQuery || allMatches.length === 0) return; + + const spans = Array.from( + textLayerEl.querySelectorAll("span"), + ) as HTMLElement[]; + if (spans.length === 0) return; + + const pageMatches = allMatches.filter((m) => m.pageNum === currentPage); + if (pageMatches.length === 0) return; + + const lowerQuery = searchQuery.toLowerCase(); + const lowerQueryLen = lowerQuery.length; + + // Position highlight divs over matching text using Range API. + const wrapperEl = textLayerEl.parentElement!; + const wrapperRect = wrapperEl.getBoundingClientRect(); + + let domMatchOrdinal = 0; + + for (const span of spans) { + const text = span.textContent || ""; + if (text.length === 0) continue; + const lowerText = text.toLowerCase(); + if (!lowerText.includes(lowerQuery)) continue; + + // Find all match positions within this span + const matchPositions: number[] = []; + let pos = 0; + while (true) { + const idx = lowerText.indexOf(lowerQuery, pos); + if (idx === -1) break; + matchPositions.push(idx); + pos = idx + 1; + } + if (matchPositions.length === 0) continue; + + const textNode = span.firstChild; + if (!textNode || textNode.nodeType !== Node.TEXT_NODE) continue; + + for (const idx of matchPositions) { + const isCurrentMatch = + domMatchOrdinal < pageMatches.length && + allMatches.indexOf(pageMatches[domMatchOrdinal]) === currentMatchIndex; + + try { + const range = document.createRange(); + range.setStart(textNode, idx); + range.setEnd(textNode, Math.min(idx + lowerQueryLen, text.length)); + const rects = range.getClientRects(); + + for (let ri = 0; ri < rects.length; ri++) { + const r = rects[ri]; + const div = document.createElement("div"); + div.className = "search-highlight" + (isCurrentMatch ? " current" : ""); + div.style.position = "absolute"; + div.style.left = `${r.left - wrapperRect.left}px`; + div.style.top = `${r.top - wrapperRect.top}px`; + div.style.width = `${r.width}px`; + div.style.height = `${r.height}px`; + highlightLayerEl.appendChild(div); + } + } catch { + // Range errors can happen with stale text nodes + } + + domMatchOrdinal++; + } + } + + // Scroll current highlight into view + const currentHL = highlightLayerEl.querySelector( + ".search-highlight.current", + ) as HTMLElement; + if (currentHL) currentHL.scrollIntoView({ block: "center", behavior: "smooth" }); +} + +function clearHighlights() { + highlightLayerEl.innerHTML = ""; +} + +function updateSearchUI() { + if (allMatches.length === 0) { + searchMatchCountEl.textContent = searchQuery ? "No matches" : ""; + } else { + searchMatchCountEl.textContent = `${currentMatchIndex + 1} of ${allMatches.length}`; + } + searchPrevBtn.disabled = allMatches.length === 0; + searchNextBtn.disabled = allMatches.length === 0; +} + +function openSearch() { + if (searchOpen) { + searchInputEl.focus(); + searchInputEl.select(); + return; + } + searchOpen = true; + searchBarEl.style.display = "flex"; + searchInputEl.focus(); + requestFitToContent(); + extractAllPageText(); +} + +function closeSearch() { + if (!searchOpen) return; + searchOpen = false; + searchBarEl.style.display = "none"; + searchQuery = ""; + searchInputEl.value = ""; + allMatches = []; + currentMatchIndex = -1; + clearHighlights(); + updateSearchUI(); + requestFitToContent(); +} + +function toggleSearch() { + if (searchOpen) { + closeSearch(); + } else { + openSearch(); + } +} + +function goToNextMatch() { + if (allMatches.length === 0) return; + currentMatchIndex = (currentMatchIndex + 1) % allMatches.length; + const match = allMatches[currentMatchIndex]; + updateSearchUI(); + if (match.pageNum !== currentPage) { + goToPage(match.pageNum); + } else { + renderHighlights(); + } +} + +function goToPrevMatch() { + if (allMatches.length === 0) return; + currentMatchIndex = + (currentMatchIndex - 1 + allMatches.length) % allMatches.length; + const match = allMatches[currentMatchIndex]; + updateSearchUI(); + if (match.pageNum !== currentPage) { + goToPage(match.pageNum); + } else { + renderHighlights(); + } +} + // Create app instance // autoResize disabled - app fills its container, doesn't request size changes const app = new App( @@ -391,6 +630,8 @@ async function renderPage() { textLayerEl.innerHTML = ""; textLayerEl.style.width = `${viewport.width}px`; textLayerEl.style.height = `${viewport.height}px`; + // Set --scale-factor so CSS font-size/transform rules work correctly. + textLayerEl.style.setProperty("--scale-factor", `${scale}`); // Render canvas - track the task so we can cancel it // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -430,6 +671,24 @@ async function renderPage() { }); await textLayer.render(); + // Cache page text items if not already cached + if (!pageTextItemsCache.has(pageToRender)) { + const items = (textContent.items as Array<{ str?: string }>).map( + (item) => item.str || "", + ); + pageTextItemsCache.set(pageToRender, items); + pageTextCache.set(pageToRender, items.join("")); + } + + // Size highlight layer to match canvas + highlightLayerEl.style.width = `${viewport.width}px`; + highlightLayerEl.style.height = `${viewport.height}px`; + + // Re-render search highlights if search is active + if (searchOpen && searchQuery) { + renderHighlights(); + } + updateControls(); updatePageContext(); @@ -549,8 +808,34 @@ prevBtn.addEventListener("click", prevPage); nextBtn.addEventListener("click", nextPage); zoomOutBtn.addEventListener("click", zoomOut); zoomInBtn.addEventListener("click", zoomIn); +searchBtn.addEventListener("click", toggleSearch); +searchCloseBtn.addEventListener("click", closeSearch); +searchPrevBtn.addEventListener("click", goToPrevMatch); +searchNextBtn.addEventListener("click", goToNextMatch); fullscreenBtn.addEventListener("click", toggleFullscreen); +// Search input events +searchInputEl.addEventListener("input", () => { + if (searchDebounceTimer) clearTimeout(searchDebounceTimer); + searchDebounceTimer = setTimeout(() => { + performSearch(searchInputEl.value); + }, 300); +}); + +searchInputEl.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + if (e.shiftKey) { + goToPrevMatch(); + } else { + goToNextMatch(); + } + } else if (e.key === "Escape") { + e.preventDefault(); + closeSearch(); + } +}); + pageInputEl.addEventListener("change", () => { const page = parseInt(pageInputEl.value, 10); if (!isNaN(page)) { @@ -568,6 +853,15 @@ pageInputEl.addEventListener("keydown", (e) => { // Keyboard navigation document.addEventListener("keydown", (e) => { + // Ctrl/Cmd+F to open search + if ((e.ctrlKey || e.metaKey) && e.key === "f") { + e.preventDefault(); + openSearch(); + return; + } + + // Don't handle nav shortcuts when search input is focused + if (document.activeElement === searchInputEl) return; if (document.activeElement === pageInputEl) return; // Ctrl/Cmd+0 to reset zoom @@ -579,7 +873,10 @@ document.addEventListener("keydown", (e) => { switch (e.key) { case "Escape": - if (currentDisplayMode === "fullscreen") { + if (searchOpen) { + closeSearch(); + e.preventDefault(); + } else if (currentDisplayMode === "fullscreen") { toggleFullscreen(); e.preventDefault(); } From 9e3ac712c6c962317b0c5abd844d34150a3b3098 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 12 Feb 2026 15:39:53 +0000 Subject: [PATCH 02/13] update screenshots --- examples/threejs-server/grid-cell.png | Bin 6603 -> 6504 bytes examples/threejs-server/screenshot.png | Bin 3023 -> 3452 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/examples/threejs-server/grid-cell.png b/examples/threejs-server/grid-cell.png index d8e2858771560901056a4a24da27bba1effa0dd6..4e7953482cc8ecb7a9f7127ab91a03703c2d9753 100644 GIT binary patch literal 6504 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

~#NuBWL~ep1K%B*d!bREVQ{I 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 literal 3023 zcmeHJ`&Uy}7LKL10wcvhgyID7B9xJfmNEhwL2-pdM6mS{<`R%exeW4%Sj7N^2t`pM zuU={5sKiGFsSpBQnh1o4=mIWA5iukr2ndvik^lvbJPOPWGe6ACf6%r1!&&=$`|+K9 z_ul8M%lk5jzShLXgg_vy_1{Z9NFc0wg0Jt_e1L02?^?rfVSHjQbc#SQwJ@wzgsX+t z_~tu*s_)^XLS65!e3FXX$~woc1B3Vv4;DG^+r&6oXo)$K_Z69g@pRVqA8+|O$ESbf zw<^S3DBoN6fyz0+h)5#GH^1`PW96E7c_C^-IM@C}*grF-u+%@N?`rps@|w}>dcp~7 zX6K$>bLZ`{O8*pDy@m*-+y5bccO`HffAfeLY}Z%&{zX2$B78nR&=&~kcwT0;;JH=U z$el|H<0g%dy$fh6HbWf8G{OxffJ^mZ2ejyA+Vt$hfu@UCd;a|@W7YDX?$BZXniEMK zpQ9P_b3CxgO^=&tnTuZ4XWSxGJaAprLuro4DAZ{;Y@BzGOw$h;g+^6tKSYXCS~CNw z%2iureg=z`*(dtr;n&NLvB|H#2mu0GM@&-&=I!zwD2?B)5zl-`+hg#O<7p^1q6s5o zUD+0y?DgutjPW+3v*yr_Q@ZwC0YVDJ^Wo**(*fgF?st3*+Y28rEdO?D0o(B)vV;fT zbMr7)_q8yCEypfmmuw<#Bf1)?53xl_6)3EG4#2A~BNouX2(8RhvPhFY`&GHY-G;cL z!20U`x))9nmm7sL%nh2QH2`HV+xt!AbpsKdq+W`pn2L3o2Gshg`g&|^%0{%Pr5mA3 zZzwcJv%tGtE4a`IU<&CPCx5M`b=H71Pj8U#mE zS}tneC@#i*0|dZ1!a(#!f5T2@N~S6;wc(}*5}A8Dcxyv< z<`s;{)6V@3PIQwP0IE>#N`7aq?kz1ZL-{;V$mPT~RE}1pbsc~cffbi8GO@fysUCs= zc?%M@P@qaj$m|$h6-pH}PwYyrTePMm!1RBHEQmlP5=F>IV(RJ60k9fGbK6Z?EY(O6 z1C|pZkCch7V>#Y+)t>L0C%!7b1&o)*K|nw`X2uUeTycMUbge063De(Lfsp8_waf+4 zxl8&SesDx#pr<*}tV97BTp>{)D{%|DSfpf>`NPrkFJq^Xz>*aJ-8>1Ox-035Awo8O zoZAnB>`|WL+x#yJu+FiX;HMa;2SUpIVIGw;<(2ojMEqE}2^FRBDx+B?R_Y+U%iDOM zX;$2iRRFU9L(?%QTFB%&oJ^Xh?X1eUYisqhR)pa(V8`@sPMlo<`9O?Lnn4w;rg|tK zS-CwD`Q7=b)lTZbL-^nn$&3hN zIG|t>uj3y%+uFo=qB0a`>7{d~?5?)dQKdFWaWbq5?;}FUuq6^%g^687{+!cM_4J=; z@b-bFf>zgOChc}dcP1`O*W6lW_-Gsr1YvZ&>^;KQL<%LN`|u>HIiobWk$Ohecr{%~A?%Zk%KOgf!PrZ9AGZ(KwPE{oprOS8TG+9)o5O~S8E^m|eJt5trt(7hhsM44 z`~{F3v!9tX$Yx*j`>!vK$gBEO=i0jZ)AWm~Do1=s1=w^G{uaABr05Sc`pjGicV(6h zQD)PlFFE?cuR-*N2#hP3y}qzCuC}Q=`+V3y8Q_WZ)>g(2Exi6^&;>2d0%IL0`&lLA z@E)-lGJG~AyI&#Z#XZYp6Z?mj%xn+gk4`j6{i`MC{vh63G9cHC9u07iS!9xI&y$hcPJc0A<)2wtSs=xx@J0(k^j|IWW2Uxk{O3mKp2imBF(wsqHN!RG<=ZA{@%<#Ya@k>Uw2 zt%7fx1(I4>w_WPjK_ Date: Thu, 12 Feb 2026 15:48:18 +0000 Subject: [PATCH 03/13] Add fullscreen button to threejs-server --- examples/threejs-server/src/global.css | 61 +++++++++++++++++- .../threejs-server/src/mcp-app-wrapper.tsx | 7 +++ examples/threejs-server/src/threejs-app.tsx | 62 ++++++++++++++++++- 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/examples/threejs-server/src/global.css b/examples/threejs-server/src/global.css index 55d812012..c969ccc41 100644 --- a/examples/threejs-server/src/global.css +++ b/examples/threejs-server/src/global.css @@ -6,7 +6,8 @@ box-sizing: border-box; } -html, body { +html, +body { font-family: var(--font-sans, system-ui, -apple-system, sans-serif); font-size: 1rem; margin: 0; @@ -45,3 +46,61 @@ html, body { font-family: var(--font-sans, system-ui); padding: 20px; } + +/* Fullscreen button */ +.fullscreen-btn { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + border: none; + border-radius: 8px; + background: rgba(0, 0, 0, 0.6); + color: white; + cursor: pointer; + display: none; /* Hidden by default, shown when fullscreen available */ + align-items: center; + justify-content: center; + transition: + background 0.2s, + opacity 0.2s; + opacity: 0; /* Initially invisible, shown on hover */ + z-index: 100; +} + +.fullscreen-btn:hover { + background: rgba(0, 0, 0, 0.8); + opacity: 1; +} + +.fullscreen-btn svg { + width: 20px; + height: 20px; +} + +.fullscreen-btn .collapse-icon { + display: none; +} + +.fullscreen-btn.available { + display: flex; +} + +/* Show button on container hover */ +.threejs-container:hover .fullscreen-btn.available { + opacity: 0.7; +} + +/* Fullscreen mode: swap icons and remove border radius */ +.threejs-container.fullscreen .fullscreen-btn .expand-icon { + display: none; +} + +.threejs-container.fullscreen .fullscreen-btn .collapse-icon { + display: block; +} + +.threejs-container.fullscreen canvas { + border-radius: 0 !important; +} diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index 4313fbbd5..3b2660156 100644 --- a/examples/threejs-server/src/mcp-app-wrapper.tsx +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -37,6 +37,8 @@ export interface ViewProps> { openLink: App["openLink"]; /** Send log messages to the host */ sendLog: App["sendLog"]; + /** Request a display mode change (e.g. fullscreen) */ + requestDisplayMode: App["requestDisplayMode"]; } // ============================================================================= @@ -108,6 +110,10 @@ function McpAppWrapper() { (params) => app!.sendLog(params), [app], ); + const requestDisplayMode = useCallback( + (params) => app!.requestDisplayMode(params), + [app], + ); if (error) { return

Error: {error.message}
; @@ -127,6 +133,7 @@ function McpAppWrapper() { sendMessage={sendMessage} openLink={openLink} sendLog={sendLog} + requestDisplayMode={requestDisplayMode} /> ); } diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index 2b99266b0..b1c49074f 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -4,7 +4,7 @@ * Renders interactive 3D scenes using Three.js with streaming code preview. * Receives all MCP App props from the wrapper. */ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"; @@ -203,8 +203,12 @@ export default function ThreeJSApp({ sendMessage: _sendMessage, openLink: _openLink, sendLog: _sendLog, + requestDisplayMode, }: ThreeJSAppProps) { const [error, setError] = useState(null); + const [currentDisplayMode, setCurrentDisplayMode] = useState< + "inline" | "fullscreen" + >("inline"); const canvasRef = useRef(null); const containerRef = useRef(null); const animControllerRef = useRef { + if (hostContext?.displayMode) { + setCurrentDisplayMode(hostContext.displayMode as "inline" | "fullscreen"); + } + }, [hostContext?.displayMode]); + + const toggleFullscreen = useCallback(async () => { + const newMode = isFullscreen ? "inline" : "fullscreen"; + try { + const result = await requestDisplayMode({ mode: newMode }); + setCurrentDisplayMode(result.mode as "inline" | "fullscreen"); + } catch { + // ignore + } + }, [isFullscreen, requestDisplayMode]); + + // Escape key exits fullscreen + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && isFullscreen) toggleFullscreen(); + }; + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [isFullscreen, toggleFullscreen]); + // Visibility-based pause/play useEffect(() => { if (!containerRef.current) return; @@ -269,7 +303,7 @@ export default function ThreeJSApp({ return (
{error &&
Error: {error}
} +
); } From 3d91d10ed8b6276484ab29153d6908d1038489da Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 13 Feb 2026 18:11:35 +0000 Subject: [PATCH 04/13] Fix prettier formatting --- docs/index.html | 22 +- examples/basic-host/index.html | 10 +- examples/basic-host/sandbox.html | 2 +- examples/basic-host/src/global.css | 12 +- examples/basic-host/src/index.module.css | 13 +- examples/basic-server-preact/mcp-app.html | 24 +- examples/basic-server-preact/src/global.css | 18 +- .../src/mcp-app.module.css | 9 +- examples/basic-server-react/mcp-app.html | 24 +- examples/basic-server-react/src/global.css | 18 +- .../basic-server-react/src/mcp-app.module.css | 9 +- examples/basic-server-solid/mcp-app.html | 24 +- examples/basic-server-solid/src/global.css | 18 +- .../basic-server-solid/src/mcp-app.module.css | 9 +- examples/basic-server-svelte/mcp-app.html | 24 +- examples/basic-server-svelte/src/global.css | 18 +- examples/basic-server-vanillajs/mcp-app.html | 64 +-- .../basic-server-vanillajs/src/global.css | 18 +- .../basic-server-vanillajs/src/mcp-app.css | 9 +- examples/basic-server-vue/mcp-app.html | 24 +- examples/basic-server-vue/src/App.vue | 38 +- examples/basic-server-vue/src/global.css | 18 +- examples/budget-allocator-server/mcp-app.html | 62 ++- .../budget-allocator-server/src/global.css | 8 +- .../budget-allocator-server/src/mcp-app.css | 3 +- examples/cohort-heatmap-server/mcp-app.html | 24 +- examples/cohort-heatmap-server/src/global.css | 8 +- .../src/mcp-app.module.css | 7 +- .../customer-segmentation-server/mcp-app.html | 130 +++--- .../src/global.css | 8 +- .../src/mcp-app.css | 19 +- examples/debug-server/mcp-app.html | 425 ++++++++++-------- examples/debug-server/src/global.css | 15 +- examples/debug-server/src/mcp-app.css | 56 ++- examples/integration-server/mcp-app.html | 24 +- examples/integration-server/src/global.css | 18 +- .../integration-server/src/mcp-app.module.css | 9 +- examples/map-server/mcp-app.html | 130 +++--- examples/map-server/test-standalone.html | 236 +++++----- examples/pdf-server/mcp-app.html | 50 ++- examples/pdf-server/src/global.css | 8 +- examples/pdf-server/src/mcp-app.css | 8 +- examples/pdf-server/src/mcp-app.ts | 29 +- examples/quickstart/mcp-app.html | 2 +- examples/say-server/mcp-app.html | 24 +- examples/scenario-modeler-server/mcp-app.html | 22 +- .../scenario-modeler-server/src/global.css | 8 +- examples/shadertoy-server/mcp-app.html | 64 ++- examples/shadertoy-server/src/global.css | 8 +- examples/shadertoy-server/src/mcp-app.css | 7 +- examples/sheet-music-server/mcp-app.html | 46 +- examples/sheet-music-server/src/global.css | 8 +- examples/sheet-music-server/src/mcp-app.css | 1 - examples/system-monitor-server/mcp-app.html | 108 ++--- examples/system-monitor-server/src/global.css | 8 +- .../system-monitor-server/src/mcp-app.css | 16 +- examples/threejs-server/mcp-app.html | 24 +- examples/transcript-server/mcp-app.html | 100 +++-- examples/transcript-server/src/global.css | 8 +- examples/transcript-server/src/mcp-app.css | 5 +- examples/video-resource-server/mcp-app.html | 48 +- examples/video-resource-server/src/global.css | 8 +- .../video-resource-server/src/mcp-app.css | 4 +- examples/wiki-explorer-server/mcp-app.html | 48 +- examples/wiki-explorer-server/src/global.css | 8 +- scripts/typedoc-github-theme-fixes.css | 60 +-- specification/2026-01-26/apps.mdx | 206 +++++---- specification/draft/apps.mdx | 206 +++++---- 68 files changed, 1586 insertions(+), 1163 deletions(-) diff --git a/docs/index.html b/docs/index.html index 609d10e88..0a0cd3e69 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,12 +1,14 @@ - + - - - - Redirecting... - - -
- - + + + + Redirecting... + + +

Redirecting to API Documentation...

+ + diff --git a/examples/basic-host/index.html b/examples/basic-host/index.html index 68e8109f6..21f6ad584 100644 --- a/examples/basic-host/index.html +++ b/examples/basic-host/index.html @@ -1,11 +1,11 @@ - + - - - + + + MCP Apps Host - +
diff --git a/examples/basic-host/sandbox.html b/examples/basic-host/sandbox.html index fb4cd8d2f..a8e7c78a7 100644 --- a/examples/basic-host/sandbox.html +++ b/examples/basic-host/sandbox.html @@ -2,7 +2,7 @@ - + MCP-UI Proxy diff --git a/examples/basic-host/src/global.css b/examples/basic-host/src/global.css index 74b57514c..6639fa244 100644 --- a/examples/basic-host/src/global.css +++ b/examples/basic-host/src/global.css @@ -25,14 +25,20 @@ box-sizing: border-box; } -html, body { +html, +body { margin: 0; padding: 0; - font-family: system-ui, -apple-system, sans-serif; + font-family: + system-ui, + -apple-system, + sans-serif; font-size: 1rem; background-color: var(--color-bg); color: var(--color-text); - transition: background-color 0.2s, color 0.2s; + transition: + background-color 0.2s, + color 0.2s; } code { diff --git a/examples/basic-host/src/index.module.css b/examples/basic-host/src/index.module.css index 0bd09dd05..badc8209b 100644 --- a/examples/basic-host/src/index.module.css +++ b/examples/basic-host/src/index.module.css @@ -12,7 +12,10 @@ font-size: 1.25rem; cursor: pointer; z-index: 1000; - transition: background-color 0.2s, border-color 0.2s, transform 0.1s; + transition: + background-color 0.2s, + border-color 0.2s, + transform 0.1s; &:hover { background-color: var(--color-border); @@ -23,7 +26,8 @@ } } -.callToolPanel, .toolCallInfoPanel { +.callToolPanel, +.toolCallInfoPanel { margin: 0 auto; padding: 1rem; border: 1px solid var(--color-border); @@ -106,7 +110,10 @@ } @keyframes slideDown { - from { opacity: 0; transform: translateY(-12px); } + from { + opacity: 0; + transform: translateY(-12px); + } } .appHeader { diff --git a/examples/basic-server-preact/mcp-app.html b/examples/basic-server-preact/mcp-app.html index 205ff4e7a..e6a1f7bc1 100644 --- a/examples/basic-server-preact/mcp-app.html +++ b/examples/basic-server-preact/mcp-app.html @@ -1,14 +1,14 @@ - + - - - - - Get Time App - - - -
- - + + + + + Get Time App + + + +
+ + diff --git a/examples/basic-server-preact/src/global.css b/examples/basic-server-preact/src/global.css index 801291f46..54a986089 100644 --- a/examples/basic-server-preact/src/global.css +++ b/examples/basic-server-preact/src/global.css @@ -14,8 +14,12 @@ --color-ring-primary: light-dark(#3b82f6, #60a5fa); --border-radius-md: 6px; --border-width-regular: 1px; - --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + --font-sans: + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; --font-weight-normal: 400; --font-weight-bold: 700; --font-text-md-size: 1rem; @@ -49,7 +53,8 @@ box-sizing: border-box; } -html, body { +html, +body { font-family: var(--font-sans); font-size: var(--font-text-md-size); font-weight: var(--font-weight-normal); @@ -82,11 +87,14 @@ h6 { line-height: var(--font-heading-sm-line-height); } -code, pre, kbd { +code, +pre, +kbd { font-family: var(--font-mono); font-size: 1em; } -b, strong { +b, +strong { font-weight: var(--font-weight-bold); } diff --git a/examples/basic-server-preact/src/mcp-app.module.css b/examples/basic-server-preact/src/mcp-app.module.css index ae08a8ccf..6b104b452 100644 --- a/examples/basic-server-preact/src/mcp-app.module.css +++ b/examples/basic-server-preact/src/mcp-app.module.css @@ -49,11 +49,16 @@ cursor: pointer; &:hover { - background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse)); + background-color: color-mix( + in srgb, + var(--color-accent) 85%, + var(--color-background-inverse) + ); } &:focus-visible { - outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary); + outline: calc(var(--border-width-regular) * 2) solid + var(--color-ring-primary); outline-offset: var(--border-width-regular); } } diff --git a/examples/basic-server-react/mcp-app.html b/examples/basic-server-react/mcp-app.html index 205ff4e7a..e6a1f7bc1 100644 --- a/examples/basic-server-react/mcp-app.html +++ b/examples/basic-server-react/mcp-app.html @@ -1,14 +1,14 @@ - + - - - - - Get Time App - - - -
- - + + + + + Get Time App + + + +
+ + diff --git a/examples/basic-server-react/src/global.css b/examples/basic-server-react/src/global.css index 801291f46..54a986089 100644 --- a/examples/basic-server-react/src/global.css +++ b/examples/basic-server-react/src/global.css @@ -14,8 +14,12 @@ --color-ring-primary: light-dark(#3b82f6, #60a5fa); --border-radius-md: 6px; --border-width-regular: 1px; - --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + --font-sans: + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; --font-weight-normal: 400; --font-weight-bold: 700; --font-text-md-size: 1rem; @@ -49,7 +53,8 @@ box-sizing: border-box; } -html, body { +html, +body { font-family: var(--font-sans); font-size: var(--font-text-md-size); font-weight: var(--font-weight-normal); @@ -82,11 +87,14 @@ h6 { line-height: var(--font-heading-sm-line-height); } -code, pre, kbd { +code, +pre, +kbd { font-family: var(--font-mono); font-size: 1em; } -b, strong { +b, +strong { font-weight: var(--font-weight-bold); } diff --git a/examples/basic-server-react/src/mcp-app.module.css b/examples/basic-server-react/src/mcp-app.module.css index ae08a8ccf..6b104b452 100644 --- a/examples/basic-server-react/src/mcp-app.module.css +++ b/examples/basic-server-react/src/mcp-app.module.css @@ -49,11 +49,16 @@ cursor: pointer; &:hover { - background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse)); + background-color: color-mix( + in srgb, + var(--color-accent) 85%, + var(--color-background-inverse) + ); } &:focus-visible { - outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary); + outline: calc(var(--border-width-regular) * 2) solid + var(--color-ring-primary); outline-offset: var(--border-width-regular); } } diff --git a/examples/basic-server-solid/mcp-app.html b/examples/basic-server-solid/mcp-app.html index 205ff4e7a..e6a1f7bc1 100644 --- a/examples/basic-server-solid/mcp-app.html +++ b/examples/basic-server-solid/mcp-app.html @@ -1,14 +1,14 @@ - + - - - - - Get Time App - - - -
- - + + + + + Get Time App + + + +
+ + diff --git a/examples/basic-server-solid/src/global.css b/examples/basic-server-solid/src/global.css index 801291f46..54a986089 100644 --- a/examples/basic-server-solid/src/global.css +++ b/examples/basic-server-solid/src/global.css @@ -14,8 +14,12 @@ --color-ring-primary: light-dark(#3b82f6, #60a5fa); --border-radius-md: 6px; --border-width-regular: 1px; - --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + --font-sans: + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; --font-weight-normal: 400; --font-weight-bold: 700; --font-text-md-size: 1rem; @@ -49,7 +53,8 @@ box-sizing: border-box; } -html, body { +html, +body { font-family: var(--font-sans); font-size: var(--font-text-md-size); font-weight: var(--font-weight-normal); @@ -82,11 +87,14 @@ h6 { line-height: var(--font-heading-sm-line-height); } -code, pre, kbd { +code, +pre, +kbd { font-family: var(--font-mono); font-size: 1em; } -b, strong { +b, +strong { font-weight: var(--font-weight-bold); } diff --git a/examples/basic-server-solid/src/mcp-app.module.css b/examples/basic-server-solid/src/mcp-app.module.css index ae08a8ccf..6b104b452 100644 --- a/examples/basic-server-solid/src/mcp-app.module.css +++ b/examples/basic-server-solid/src/mcp-app.module.css @@ -49,11 +49,16 @@ cursor: pointer; &:hover { - background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse)); + background-color: color-mix( + in srgb, + var(--color-accent) 85%, + var(--color-background-inverse) + ); } &:focus-visible { - outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary); + outline: calc(var(--border-width-regular) * 2) solid + var(--color-ring-primary); outline-offset: var(--border-width-regular); } } diff --git a/examples/basic-server-svelte/mcp-app.html b/examples/basic-server-svelte/mcp-app.html index 6bac3c97b..6c4bf1bc8 100644 --- a/examples/basic-server-svelte/mcp-app.html +++ b/examples/basic-server-svelte/mcp-app.html @@ -1,14 +1,14 @@ - + - - - - - Get Time App - - - -
- - + + + + + Get Time App + + + +
+ + diff --git a/examples/basic-server-svelte/src/global.css b/examples/basic-server-svelte/src/global.css index 801291f46..54a986089 100644 --- a/examples/basic-server-svelte/src/global.css +++ b/examples/basic-server-svelte/src/global.css @@ -14,8 +14,12 @@ --color-ring-primary: light-dark(#3b82f6, #60a5fa); --border-radius-md: 6px; --border-width-regular: 1px; - --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + --font-sans: + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; --font-weight-normal: 400; --font-weight-bold: 700; --font-text-md-size: 1rem; @@ -49,7 +53,8 @@ box-sizing: border-box; } -html, body { +html, +body { font-family: var(--font-sans); font-size: var(--font-text-md-size); font-weight: var(--font-weight-normal); @@ -82,11 +87,14 @@ h6 { line-height: var(--font-heading-sm-line-height); } -code, pre, kbd { +code, +pre, +kbd { font-family: var(--font-mono); font-size: 1em; } -b, strong { +b, +strong { font-weight: var(--font-weight-bold); } diff --git a/examples/basic-server-vanillajs/mcp-app.html b/examples/basic-server-vanillajs/mcp-app.html index 5b6428014..517d41e1d 100644 --- a/examples/basic-server-vanillajs/mcp-app.html +++ b/examples/basic-server-vanillajs/mcp-app.html @@ -1,35 +1,41 @@ - + - - - - - Get Time App - - -
-

Watch activity in the DevTools console!

+ + + + + Get Time App + + +
+

Watch activity in the DevTools console!

-
-

Server Time: Loading...

- -
+
+

+ Server Time: Loading... +

+ +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- - -
-
- - +
+ + +
+
+ + diff --git a/examples/basic-server-vanillajs/src/global.css b/examples/basic-server-vanillajs/src/global.css index 801291f46..54a986089 100644 --- a/examples/basic-server-vanillajs/src/global.css +++ b/examples/basic-server-vanillajs/src/global.css @@ -14,8 +14,12 @@ --color-ring-primary: light-dark(#3b82f6, #60a5fa); --border-radius-md: 6px; --border-width-regular: 1px; - --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + --font-sans: + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; --font-weight-normal: 400; --font-weight-bold: 700; --font-text-md-size: 1rem; @@ -49,7 +53,8 @@ box-sizing: border-box; } -html, body { +html, +body { font-family: var(--font-sans); font-size: var(--font-text-md-size); font-weight: var(--font-weight-normal); @@ -82,11 +87,14 @@ h6 { line-height: var(--font-heading-sm-line-height); } -code, pre, kbd { +code, +pre, +kbd { font-family: var(--font-mono); font-size: 1em; } -b, strong { +b, +strong { font-weight: var(--font-weight-bold); } diff --git a/examples/basic-server-vanillajs/src/mcp-app.css b/examples/basic-server-vanillajs/src/mcp-app.css index b23bd5e3d..f780ce23e 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.css +++ b/examples/basic-server-vanillajs/src/mcp-app.css @@ -49,11 +49,16 @@ cursor: pointer; &:hover { - background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse)); + background-color: color-mix( + in srgb, + var(--color-accent) 85%, + var(--color-background-inverse) + ); } &:focus-visible { - outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary); + outline: calc(var(--border-width-regular) * 2) solid + var(--color-ring-primary); outline-offset: var(--border-width-regular); } } diff --git a/examples/basic-server-vue/mcp-app.html b/examples/basic-server-vue/mcp-app.html index 6bac3c97b..6c4bf1bc8 100644 --- a/examples/basic-server-vue/mcp-app.html +++ b/examples/basic-server-vue/mcp-app.html @@ -1,14 +1,14 @@ - + - - - - - Get Time App - - - -
- - + + + + + Get Time App + + + +
+ + diff --git a/examples/basic-server-vue/src/App.vue b/examples/basic-server-vue/src/App.vue index 196ad64da..e9e829959 100644 --- a/examples/basic-server-vue/src/App.vue +++ b/examples/basic-server-vue/src/App.vue @@ -14,7 +14,6 @@ function extractTime(result: CallToolResult): string { return text; } - const app = ref(null); const hostContext = ref(); const serverTime = ref("Loading..."); @@ -67,7 +66,10 @@ async function handleGetTime() { if (!app.value) return; try { console.info("Calling get-time tool..."); - const result = await app.value.callServerTool({ name: "get-time", arguments: {} }); + const result = await app.value.callServerTool({ + name: "get-time", + arguments: {}, + }); console.info("get-time result:", result); serverTime.value = extractTime(result); } catch (e) { @@ -108,17 +110,22 @@ async function handleOpenLink() {

Redirecting to API Documentation...