| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
01d826aea0 |
chore: bump ext-apps to 1.6.0 (#613) | ||
|
|
621a70a724 |
chore: bump ext-apps to 1.5.0 (#588)
* chore: bump ext-apps to 1.5.0 * style: auto-fix prettier formatting --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
61fe39b4db |
feat(pdf-server): rasterize imported annotations + form/save consistency follow-ups (#593)
* feat(pdf-server): rasterize imported annotations via annotationCanvasMap
Adds an 'imported' annotation type for anything in a loaded PDF that we
either don't model (Ink, Polygon, Caret, FileAttachment, ...) or can't
faithfully re-render (Stamp with an appearance stream, e.g. an image
signature). These now:
- Appear in the annotation panel as '<Subtype> (from PDF)'
- Render in our layer as a positioned div whose body is the per-
annotation canvas pdf.js produced via page.render({annotationCanvasMap})
- if pdf.js didn't divert it (no hasOwnCanvas), the box is transparent
over the main-canvas pixel and just captures clicks
- Are selectable and draggable (resize/rotate disabled - bitmap would
just stretch)
- Are skipped by addAnnotationDicts; getAnnotatedPdfBytes already
filters baseline ids, so save leaves the original in the PDF.
Move/delete are UI-only for now (documented).
Link (2) and Popup (16) are excluded - navigational/auxiliary, not
markup.
importPdfjsAnnotation tests added for unsupported-type and
appearance-stamp paths; computeDiff round-trip for 'imported'.
* fix(pdf-server): isEditing on page.render so stamps always divert to annotationCanvasMap
StampAnnotation.hasOwnCanvas defaults to noRotate; stamps without that
flag composited onto the main canvas, so deleting the 'imported' overlay
left an unclickable pixel. isEditing forces hasOwnCanvas=true for stamps
(via mustBeViewedWhenEditing) so the appearance lands in the per-id
canvas and our DOM element is the only render.
* fix(pdf-server): refresh annotation panel after save rebases baseline
setDirty(false) updated the title and save button but the panel kept
showing pending-change badges because it diffs against
pdfBaselineAnnotations/FormValues and was never re-rendered.
* fix(pdf-server): imported stamp canvases cropped/2x on retina
renderPage applied devicePixelRatio via ctx.scale(dpr,dpr) instead of
page.render's transform parameter. pdf.js sizes annotationCanvasMap
backing buffers as rectW * outputScaleX * viewport.scale, and
outputScaleX is read from transform[0] (defaults 1). So on retina the
per-annotation canvas got a 1x backing while its internal setTransform
(from the SVD of the already-dpr-scaled ctx) was 2x - the appearance
rendered at 2x into a half-sized buffer, showing only the top-left
quarter.
Pass dpr via transform: [dpr,0,0,dpr,0,0] so outputScaleX matches.
Also filter AnnotationLayer.render() to Widget annotations only so it
stops creating empty pointer-events:auto sections for stamps in
#form-layer that could steal clicks from our overlays.
* feat(pdf-server): strip deleted baseline annotations from /Annots on save
buildAnnotatedPdfBytes takes a removedRefs list and walks each page's
/Annots array (backwards) removing matching PDFRef entries.
getAnnotatedPdfBytes computes that list from baseline annotations no
longer in annotationMap, parsing the ref back from our id via
parseAnnotationRef (handles both pdf-<num>-<gen> and pdf-<num>R).
Panel: removed baseline annotations now stay listed as crossed-out cards
with a revert button (mirrors cleared form fields), so the user can see
and undo the pending deletion before save commits it.
* fix(pdf-server): re-render after seeding storage from AcroForm tree
buildFieldNameMap runs AFTER the first renderPage (perf: don't block the
canvas on an O(numPages) scan). When it detects a widget/field-tree
mismatch and pushes the field-tree value into annotationStorage, the
form layer has already rendered the stale widget value. Re-render once
when that happens so the input shows the AcroForm truth.
* fix(pdf-server): reload PDF after successful save instead of rebasing in place
Rebasing baselines while keeping the old pdfDocument drifts: subsequent
renders rasterize annotations that were just stripped from disk, and the
field/widget split pdf-lib's save can create isn't visible until reload.
Reload makes 'viewer == disk' an invariant. localStorage cleared first;
file_changed echo suppressed by lastSavedMtime as before.
|
||
|
|
3af019144f |
fix(pdf-server): form-field save robustness (#591)
* fix(pdf-server): one bad form field no longer aborts saving the rest #577 dropped the per-field try/catch in buildAnnotatedPdfBytes when it swapped to type-dispatch, so the first field whose pdf-lib write throws (max-length text, missing /Yes appearance, radio buttonValue mapping to neither label nor index, ...) bubbled to the outer catch and silently dropped every subsequent field. Compounded by getAnnotatedPdfBytes passing every baseline field, so even an untouched problematic field in the PDF poisoned the whole save. - Re-wrap each field write in its own try/catch (warn + continue). - getAnnotatedPdfBytes now only sends fields whose value differs from baseline (still includes explicit ''/false sentinels for cleared fields so pdf-lib overwrites the original /V). - Regression test: a maxLength=2 text field fed a long string, followed by a normal field; assert the second one still lands. * fix(pdf-server): radio groups misclassified as PDFCheckBox save the chosen widget Some PDFs (e.g. third-party forms like the IRS f1040 family, and the demo Form.pdf) omit the /Ff Radio flag bit on button fields, so pdf-lib classifies a multi-widget radio as PDFCheckBox. The viewer stores pdf.js's buttonValue (the widget's on-state name, e.g. '0'/'1') as a string. The PDFCheckBox branch did 'if (value) field.check()', which always sets the FIRST widget's on-state - so any choice saved as the first option. When the value is a string on a PDFCheckBox, treat it as a radio on-value: write /V and per-widget /AS directly via the low-level acroField (mirroring PDFAcroRadioButton.setValue minus its first-widget-only onValues guard). Booleans keep check()/uncheck(). Test: build a radio fixture, clear the Radio flag so reload sees PDFCheckBox, save with '1', assert /V = /1 and second widget /AS = /1. * fix(pdf-server): clearing a radio from the panel unchecks all widgets clearFieldInStorage wrote the same string clearValue to every widget's storage, hitting pdf.js's inverted string-coercion bug (the one setFieldInStorage already works around): the wrong widget rendered checked. For radio, write {value:false} to every widget instead. * fix(pdf-server): only treat multi-widget PDFCheckBox as misclassified radio A single-widget checkbox getting a string value (e.g. 'Yes'/'Off' if something upstream stores the export string instead of boolean) was being routed through setButtonGroupValue, which no-ops when no widget's on-state matches the value - leaving the box unchanged. Gate the radio-path on widgets.length > 1; single-widget falls through to check()/uncheck() with liberal truthy/'Off' semantics. * fix(pdf-server): generate missing field appearances on save PDFs whose checkbox/radio widgets lack an /AP/N/<onValue> stream: check() sets /V and /AS but Preview/Acrobat can't render a state with no appearance, so the field looks unchanged. Call form.updateFieldAppearances() after the write loop so pdf-lib synthesizes defaults for dirty fields. * feat(pdf-server): save PDFOptionList (multiselect listbox) values Language field in Form.pdf is a PDFOptionList; the save loop skipped it. Parse the viewer's comma-joined string, select() the subset that exists in getOptions() (pdf-lib throws on unknowns and there's no edit-mode fallback like Dropdown has). * fix(pdf-server): capture all selected options on <select multiple> target.value on a multiselect is only the first option; join selectedOptions so the save path gets the full set. |
||
|
|
f3bf1065db |
fix(pdf-server): import highlight/underline/strike from existing PDFs (#592)
* fix(pdf-server): import highlight/underline/strike from existing PDFs importPdfjsAnnotation expected nested quadPoints arrays, but pdf.js emits a flat Float32Array (8 numbers per quad). Iterating it yielded numbers, qp.length was undefined, rects stayed empty, and the function returned null - so every quad-based annotation in a loaded PDF was dropped from annotationMap and rendered only as unselectable canvas pixels. Shipped this way in #506; not a regression. Parse the flat array in 8-stride chunks, fall through to ann.rect if that yields nothing. Existing tests used the (wrong) nested fixture shape; switched them to Float32Array and added a 2-quad case + a rect-fallback case. * fix(pdf-server): own markup annotations in our layer, not the canvas page.render() defaulted to AnnotationMode.ENABLE, painting every annotation's appearance stream onto the canvas. We ALSO import them into annotationMap and render DOM versions in #annotation-layer, so the user saw two representations: an unclickable canvas pixel that looked right, and our clickable DOM element that looked like our styling. Clicking the 'real' one did nothing. Set annotationMode: DISABLE so the canvas is page-content-only and our layer is the single source of truth for markup. Imported annotations now behave identically to user-created ones (select/drag/delete). Form widgets are unaffected (#form-layer via AnnotationLayer.render). get_screenshot keeps ENABLE_STORAGE so screenshots still show annotations. Known trade-offs: - Stamps with image appearance streams degrade to our text-label render (no rasterization yet). - Annotation types we don't import (Ink, Polygon, Caret, ...) no longer ghost on the canvas - they're invisible. They were never editable anyway; loadBaselineAnnotations already logs them. * Revert "fix(pdf-server): own markup annotations in our layer, not the canvas" This reverts commit 2f989a757d3aa517fa427c7fd59b462326599bfc. |
||
|
|
4fc95131bf |
feat(pdf-server): get_viewer_state interact action (#590)
* feat(pdf-server): get_viewer_state interact action
New interact action that returns a JSON snapshot of the live viewer:
{currentPage, pageCount, zoom, displayMode, selectedAnnotationIds,
selection: {text, contextBefore, contextAfter, boundingRect} | null}.
The viewer already pushes selection passively via setModelContext as
<pdf-selection> tags, but not all hosts surface model-context. This gives
the model an explicit pull.
selection.boundingRect is a single bbox in PDF points (top-left origin,
y-down) so it can be fed straight back into add_annotations. selection is
null when nothing is selected or the selection is outside the text-layer.
Wiring: new PdfCommand variant -> processCommands case ->
handleGetViewerState -> submit_viewer_state (new app-only tool, mirrors
submit_save_data) -> waitForViewerState -> text content block.
Also fills a gap in the display_pdf description: it listed interact
actions but was missing save_as; added that and get_viewer_state.
e2e: two tests covering selection:null and a programmatic text-layer
selection.
* test(pdf-server): fix get_viewer_state e2e race + assertions
readLastToolResult clicked .last() before the interact result panel
existed (callInteract doesn't block), so it expanded the display_pdf
panel instead. Wait for the expected panel count first.
Also: basic-host renders the full CallToolResult JSON, with the state
double-escaped inside content[0].text. Parse instead of regex-matching.
playwright.config.ts: honor PW_CHANNEL env to use system Chrome locally
when the bundled chromium_headless_shell is broken.
|
||
|
|
2e55c842ba |
fix(pdf-server): let zoom-out button go below fit (revert button floor) (#589)
Some hosts overlay UI on the iframe (chat composer, etc.) without reporting it in safeAreaInsets.bottom, so computeFitScale's 'fit' fills the iframe but the page bottom sits under the overlay. Flooring the zoom-out button at fit removed the only escape hatch. Pinch keeps the fit floor + rubber-band (it needs a defined snap point for the exit-to-inline gesture). The button now goes to ZOOM_MIN as before #587. |
||
|
|
721ccd04b8 |
fix(pdf-server): translucent highlights, pinch to/from fullscreen, toolbar layout (#587)
* fix(pdf-server): translucent highlights, pinch to/from fullscreen, toolbar layout Highlights rendered opaque: inline `background: <hex>` overrode the CSS rgba(...,0.35). Now convert def.color via cssColorToRgb and force alpha 0.35. Added a toHaveCSS regression check in pdf-annotations.spec.ts. Pinch-in while inline (wheel ctrlKey deltaY<0 or two-finger spread >1.15x) now enters fullscreen. Pinch-out in fullscreen past 0.9x of fit-scale, when already at/below fit, exits to inline and clears userHasZoomed so refit sizes the inline view. previewScaleRaw tracks the unclamped intent so the exit fires even when fit ~= ZOOM_MIN. A modeTransitionInFlight latch (held 250ms post-toggle) keeps the gesture tail from re-toggling or immediately zooming the new view. Fullscreen toolbar: flex-wrap: nowrap and tighter 0.25rem vertical padding (min-height 40px + safe-top instead of 48px + wrap). Search bar top/right now follow --safe-top/--safe-right so it sits flush below the toolbar instead of overlapping it. Base .canvas-container gets touch-action: pan-x pan-y so the inline pinch is capturable on iOS. * fix(pdf-server): drop --safe-top from fullscreen toolbar padding Hosts wrap fullscreen in their own header (title + close), which already clears the top safe-area. Adding --safe-top to our toolbar padding-top double-dipped, leaving a visible gap between the host header and our toolbar. Keep --safe-left/right (host header doesn't cover the sides). * feat(pdf-server): horizontal swipe changes pages whenever page fits width Previously gated on scale <= 1.0, which blocked page-nav in fullscreen where fit-scale is often >100%. Now gate on actual horizontal overflow (scrollWidth > clientWidth) so swipe works at any fit-to-width scale and still defers to native panning once you zoom past it. * fix(pdf-server): refit to inline width after pinch-out exits fullscreen handleHostContextChanged calls refitScale() before the iframe has actually shrunk, and the ResizeObserver's inline branch only refits on width *growth* (to avoid a requestFitToContent shrink-loop). So the fullscreen->inline shrink never triggered a refit and the page stayed at the fullscreen scale. Add a one-shot forceNextResizeRefit flag, set on fullscreen->inline (both the pinch-out path and handleHostContextChanged), consumed by the ResizeObserver on the next size change. One-shot keeps the shrink-loop guard intact for ordinary inline resizes. * feat(pdf-server): floor fullscreen zoom at fit-to-page In fullscreen, pinch-out and the zoom-out button now floor at the fit-to-page scale instead of ZOOM_MIN, so the page never shrinks below fully-visible (no dead margin around it). previewScaleRaw stays unclamped so a continued pinch-out past fit still triggers exit-to-inline. * fix(pdf-server): wheel pinch-out can exit fullscreen again The fit-floor pinned previewScale at fit, but the wheel handler multiplied the *clamped* previewScale, so it could never accumulate below fit*0.9 to trigger exit. Drive the wheel accumulator off previewScaleRaw instead, and bound previewScaleRaw to [floor*0.7, ZOOM_MAX] so it can cross the 0.9 exit threshold without drifting unboundedly (which would make direction reversal feel sticky). Touch path was unaffected (absolute ratio). * fix(pdf-server): always refit to inline width on fullscreen exit userHasZoomed stayed true after Escape/button/host-× exits, so refitScale() bailed even though forceNextResizeRefit let the ResizeObserver call it. Clear userHasZoomed in handleHostContextChanged whenever we land inline — fullscreen zoom level is meaningless there. Dropped the now-dead requestFitToContent fallback in the same block. * refactor(pdf-server): rubber-band pinch-out instead of dual-tracked clamp The previewScaleRaw side-channel made the gesture feel dead (page pinned at fit, no feedback) and the wheel accumulator interaction was fragile. New model: previewScale is the only tracked value. In fullscreen it may overshoot down to 0.75*fit so the user *sees* the page pull away as they pinch out. On commit: - started near fit (<=1.05*fit) AND preview <0.9*fit -> exit to inline - otherwise clamp committed scale to >=fit (overshoot snaps back) beginPinch seeds fitScaleAtPinchStart synchronously from when !userHasZoomed (the common enter-fullscreen-at-fit case) so the first frame already has the right floor; the async computeFitScale refines it. * test(pdf-server): update inline-pinch e2e for new fullscreen behavior 'trackpad pinch is ignored outside fullscreen' is no longer the contract. Replace with two tests: pinch-in inline -> .main.fullscreen appears; pinch-out inline -> zoom unchanged, no fullscreen. The two pdf-annotations.spec.ts failures in the previous run were flaky (passed on retry). |
||
|
|
9da4a75ff6 |
chore: bump ext-apps to 1.4.0 (#582)
* chore: bump ext-apps to 1.4.0 Changes since 1.3.2: SDK - feat: add addEventListener/removeEventListener with DOM-model on* semantics (#573) pdf-server - feat: add save_as interact action (#580) - feat: fit-to-page on fullscreen + pinch-to-zoom (#583) - fix: npx DOMMatrix crash + broken MCPB bundle (#584) - fix: viewer liveness, 1:1 batch results, fullscreen jitter (#579) - fix: render page before O(numPages) annotation scans (#581) - fix: radio + dropdown in fill_form/save (#577) Dependencies - chore: npm audit fix — sdk 1.29.0, systeminformation 5.31.5, +13 transitives (#585, #586) - fix: bump path-to-regexp 8.3.0 → 8.4.1 to patch ReDoS CVEs (#576) * chore: update e2e snapshots [skip ci] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
8e7206cb15 |
chore(deps): npm audit fix — sdk 1.29.0, systeminformation 5.31.5, +13 transitives (#585)
Clears all 15 npm audit findings (8 high / 6 moderate / 1 low): - @modelcontextprotocol/sdk 1.25.2 → 1.29.0 (GHSA-345p-7cg4-v4c7 cross-client data leak via shared transport reuse). Root had it pinned exact in devDependencies which blocked audit fix. - systeminformation 5.30.6 → 5.31.5 (GHSA-5vv4-hvf7-2h46, GHSA-9c88-49p5-5ggf command injection) - hono, undici, rollup, picomatch, minimatch, lodash-es, svelte, devalue, ajv, brace-expansion, markdown-it, yaml, qs — lockfile bumps via npm audit fix |
||
|
|
04fed524a9 |
fix(pdf-server): npx DOMMatrix crash + broken MCPB bundle (#584)
* fix(pdf-server): stub DOMMatrix/ImageData/Path2D for npx without @napi-rs/canvas
pdfjs-dist/legacy/build/pdf.mjs does `new DOMMatrix()` at module scope.
Its own polyfill loads @napi-rs/canvas (an optionalDependency), but npx
frequently misses the platform-native binary (npm/cli#4828), so the
server crashed on `npx @modelcontextprotocol/server-pdf --stdio`:
ReferenceError: DOMMatrix is not defined
at pdfjs-dist/legacy/build/pdf.mjs:17078
The server only uses pdfjs for text/metadata/form-field extraction —
never canvas rendering — so no-op stubs suffice and avoid shipping
~130MB of native binaries.
The polyfill is a separate ESM module kept --external in the bun bundle
so it executes before pdfjs's hoisted static import; inlined body code
would run too late.
Broken since 1.3.0 (#506). Thanks to Bryan Thompson for the analysis
and fix shape from the npm-to-mcpb pipeline.
* fix(pdf-server): MCPB bundle missing runtime deps + display_name with spaces
`mcpb pack` zips whatever is on disk; in this monorepo all runtime deps
are hoisted to root node_modules, so the published .mcpb shipped without
pdfjs-dist/ajv/express/etc. and crashed on import in Claude Desktop.
build-mcpb.mjs stages dist/ + manifest into a clean temp dir, runs a
non-workspace `npm install --omit=dev --omit=optional` (the new
DOMMatrix polyfill makes @napi-rs/canvas's ~130MB of native binaries
unnecessary), syncs manifest.version to package.json, then packs.
Result: 12.3MB / 38.8MB unpacked, full MCP handshake + tools/list pass.
Also rename display_name "PDF (By Anthropic)" → "pdf-viewer" — spaces
in display_name break MCP App UI rendering for MCPBs in Claude Desktop.
CI now smoke-tests the extracted bundle starts (`dist/index.js --stdio`
prints "Ready") so a deps-less bundle can't ship again.
* fix(pdf-server): list interact in MCPB manifest tools (was stale since #491)
|
||
|
|
68c58b60b7 |
feat(pdf-server): fit-to-page on fullscreen + pinch-to-zoom (#583)
* feat(pdf-server): refit on inline→fullscreen + pinch-to-zoom in fullscreen
Two related fixes for the fullscreen reading experience:
1) Inline → fullscreen left the page at the cramped inline scale.
computeFitToWidthScale() returned null when the natural page
already fit (availableWidth >= pageWidth), so going from a
narrow inline iframe (~67%) to a wide fullscreen container
produced null → no rescale. Now caps at min(1.0, fit) so the
refit handler sees 1.0 ≠ 0.67 and re-renders. Display-mode
handling moves before the containerDimensions refit so the
.fullscreen padding lands before we measure clientWidth, and
the inline→fullscreen edge triggers refit explicitly (some
hosts send displayMode without containerDimensions).
2) Pinch-to-zoom in fullscreen. Two input paths:
- wheel + ctrlKey → trackpad pinch (Chrome/FF/Edge/Safari).
exp(-deltaY/100) per pdf.js; 150ms settle then commit.
- two-finger touch → mobile. Distance ratio against touchstart;
commit on touchend, revert on touchcancel.
Both apply a CSS transform to .page-wrapper for live feedback,
then commit one renderPage() at gesture end (renderPage is
heavy: PDF.js raster + TextLayer + AnnotationLayer rebuild).
touch-action: pan-x pan-y on the fullscreen canvas-container
keeps native panning working while we own the pinch.
Incidental: resetZoom (Cmd+0) now snaps to fit, not blindly 1.0,
so it doesn't overflow a narrow inline iframe; ZOOM_MIN/MAX
constants; shared pageWrapperEl ref.
* test(pdf-viewer-zoom): start narrow — basic-host doesn't emit containerDimensions on resize
The first test loaded at 1400w then resized to 500w expecting the
viewer to refit, but basic-host only sends containerDimensions at
init (implementation.ts:297) — never on window resize. Start narrow
so the initial fit lands <100%, then widen + fullscreen. Matches
the webtop verification flow that confirmed 67% → 100%.
* fix(pdf-server): drive fullscreen refit from ResizeObserver, not host messages
The displayMode hook ran refitToWidth() but Claude Desktop resizes
the iframe element AFTER (or independently of) sending the displayMode
update — clientWidth was still the inline width when we measured.
ResizeObserver on the canvas-container fires after the iframe has
actually laid out at the new size, so the measurement is fresh.
Gated to width-growth only: in inline mode renderPage() →
requestFitToContent() → host shrinks iframe to page width, which
would otherwise loop the scale down to ZOOM_MIN. Growth-only covers
inline→fullscreen and window-widen without the feedback loop.
* fix(basic-host): emit containerDimensions on iframe resize
Per spec, hosts notify views of container dimension changes via
host-context-changed. basic-host only sent containerDimensions once
at init — never on window resize or after the fullscreen panel
toggle. ResizeObserver on the iframe sends {width, maxHeight: 6000}
on every layout change; height stays flexible so views can keep
driving it via sendSizeChanged.
Doesn't replace the viewer-side ResizeObserver (defensive against
hosts we don't control), but makes the reference host spec-compliant.
* fix(pdf-server): scale UP to fill width in fullscreen, not just down
The previous fit cap of 1.0 meant a 612pt page in a 1400px fullscreen
container stayed at 100% — natural size, acres of empty space.
Fullscreen now caps at ZOOM_MAX so the page fills the available
width. Inline keeps the 1.0 cap (the iframe sizes to the page via
sendSizeChanged; growing past 1.0 would just make the iframe huge).
Exit-fullscreen refits back down to ≤1.0; if userHasZoomed (so
refit no-ops) fall back to requestFitToContent so the iframe still
shrinks to whatever scale they left it at.
* test(pdf-viewer-zoom): expect fullscreen to fill width (>100%), pinch out
Fullscreen fit now scales up past 100%, so the entering-fullscreen
assertion is >inlineZoom+50, not =100. The pinch test pinches OUT
(deltaY>0) so it doesn't immediately hit ZOOM_MAX from a ~220%
starting point.
* fix(pdf-server): fullscreen fit-to-PAGE, not fit-to-width
Fit-to-width on a wide screen filled the width but left the page
scrolling vertically — "fits" should mean the whole page is visible.
computeFitScale (renamed) now returns min(widthFit, heightFit) in
fullscreen so the page sits centred without scroll. Inline keeps
fit-to-width capped at 1.0 (height is host-driven via sendSizeChanged).
ResizeObserver now reacts to height changes too in fullscreen — the
inline width-growth-only gate stays (requestFitToContent loop guard),
but in fullscreen requestFitToContent early-returns so any size change
is safe to refit on.
e2e: assert no overflow in fullscreen instead of a magic zoom %.
* fix(pdf-server): respect safe-area insets in fullscreen fit-to-page
The host's chat input / home indicator overlays the bottom of the
fullscreen iframe (reported via safeAreaInsets). The toolbar already
honoured top/left/right; the canvas-container now adds the bottom +
side insets to its padding so the page sits above the overlay.
computeFitScale subtracts padding, so fit-to-page lands clear.
ResizeObserver watches the content box, so inset changes re-trigger
the fit.
* fix(pdf-server): clear pinch transform in same frame as canvas resize
commitPinch cleared the CSS transform BEFORE renderPage's first
await — for one frame the page snapped back to the old rendered
size, then jumped forward when the canvas resized. renderPage now
clears the transform immediately after setting canvas.style.width/
height, so the size handoff is atomic.
Also bump the wheel settle timer 150→200ms; slow trackpad pinches
can leave >150ms gaps and would commit mid-gesture.
* fix(pdf-server): no blank flash on pinch commit; clamp Ctrl+mousewheel; basic-host observer cleanup
Three review follow-ups:
1) Snap on pinch release: setting canvasEl.width clears the backing
store, so between the resize and page.render() completing the
page was blank. Snapshot the old bitmap first and drawImage it
scaled as a placeholder; the real render paints over it. Gated
on a transform being present so page-nav renders aren't affected.
2) Ctrl + physical mouse wheel sent deltaY≈±100 → exp(±1)≈2.7× per
notch, slamming straight to ZOOM_MIN/MAX. Clamp per-event delta
to ±25 (≈±28% per notch); trackpad pinch deltas are ~±1-10 so
the clamp is a no-op there.
3) basic-host's iframe ResizeObserver had no disposal path. Chained
disconnect() into appBridge.onclose (Protocol base class hook).
* simplify(pdf-server): pinch snap was transform-origin mismatch, not bitmap
The snap on pinch release was the page jumping vertically:
transform-origin: center center scales around the wrapper's middle,
but the flex container is align-items: flex-start so on commit the
canvas grows from the TOP. Preview top edge moved up; committed top
edge stays put → jump down. transform-origin: 50% 0 matches the
layout anchor, no jump.
Drop the canvas-snapshot placeholder (over-engineered, wrong fix),
the dead transition='' lines (.page-wrapper has no CSS transition),
and the containerDimensions→refit hook (ResizeObserver covers actual
size changes; the host message isn't load-bearing).
|
||
|
|
800f6d08fd |
fix(pdf-server): viewer liveness, 1:1 batch results, fullscreen jitter (#579)
* fix(pdf-server): fail fast on never-polled viewer + surface batch errors
interact's get_screenshot/get_text waited the full 45s for a viewer that
never mounted (host went idle before iframe reached startPolling), then
timed out with a generic message. Now tracks viewsPolled and fails in ~8s
with a recovery hint. Batch errors were also ordered success-first —
hosts that flatten isError to content[0] showed "Queued: Filled N fields"
and dropped the actual failure entirely. Viewer side: poll even when PDF
load throws, and catch connect() rejection instead of silently blanking.
* fix(pdf-server): squash batch errors into one block, warn on phantom annotation updates
Three issues from a hung interact in production:
1. Batch error swallowed by host flattening. The earlier fix unshifted the
error before successes, but the prefix went on top of THAT — so
content[0] was just "Batch failed at step 2/2 (get_screenshot):" with
the actual timeout text in content[1], which the host dropped. Now all
text joins into one block; images survive after.
2. update_annotations silent no-op. The viewer's processCommands does
`if (!existing) continue` for unknown ids. If a prior add_annotations
failed mid-batch the model gets a happy "Queued" while nothing renders.
Track ids server-side; warn (not error — user might have drawn the
annotation manually in the iframe).
3. Per-def isolation in the viewer. A throw in addAnnotation killed the
whole processCommands batch including any get_pages later in the queue,
so submit_page_data never fired and the server waited the full 45s.
Now each def gets its own try/catch.
Also: stderr logging on interact failure (only — not chatty per-call) so
the next hang shows step timing in mcp-server-*.log instead of nowhere.
* fix(pdf-server): images first on success too, not just on error
The host's content[0] flattening drops images on successful batches just
the same: [add_annotations, get_screenshot] → content = ["Queued: ...",
<image>] → model sees only the text. Observed in audit logs where the
exact same iframe + viewUUID returned an image when the batch was
[get_screenshot, get_screenshot] but only text when add_annotations
came first.
Stable-sort images to the front. The Queued lines still arrive, just
after what the model actually asked to see.
* Revert "fix(pdf-server): images first on success too, not just on error"
This reverts commit 129a2dcd7a04e5bd116e59c4d6fc211db4203e2e.
* refactor(pdf-server): 1:1 batch results, drop isError when there's something to lose
LocalAgentMode SDK collapses isError:true results to a bare string of
content[0].text. The squash-everything-into-one-block defense from
0dbb8ed3 worked but lost the positional contract — the model couldn't
tell which command produced which result.
Cleaner: content[i] is the result of commands[i]. If a step fails the
batch stops there with that slot's text starting with "ERROR", and
content.length tells the model how far it got. No isError on multi-step
failures — there's no way to set it without the SDK eating the prior
images. Single-command failures keep isError:true since there's nothing
before content[0] to lose; the SDK's flatten-to-content[0].text is
exactly the ERROR text we want shown.
Tool description documents the contract.
* fix(pdf-server): isError only for single-command failures, not failedAt===0
failedAt===0 in a multi-step batch still has a positional contract to
honor — content.length=1 vs commandList.length=3 is a signal the model
loses if the SDK flattens the array to a string. The ERROR text encodes
"step 1/3" but the simpler rule ("batches never set isError") is
easier to document and reason about.
* fix(pdf-viewer): read live displayMode in requestFitToContent, guard tiny measurements
currentDisplayMode defaults to "inline" and handleHostContextChanged
only writes it `if (ctx.displayMode)`. If the host's initial context
omits the field — or the update lands one tick after a renderPage() —
requestFitToContent measures a near-empty pageWrapper and sends ~85px
(toolbar + padding only), shrinking a fullscreen iframe to a sliver.
Workaround was exit + re-enter fullscreen to force a context update.
Read app.getHostContext().displayMode directly (always fresh), and
separately refuse to send when pageWrapper measures smaller than the
toolbar — that's never a real layout, just an early render.
* revert(pdf-server): drop viewAnnotationIds — wrong about source of truth
The viewer's annotationMap is baseline (loadBaselineAnnotations from the
PDF) + diff restored from localStorage + manual edits + model adds.
viewAnnotationIds only tracks the last category, so the warning fires
for any update to an annotation the model didn't itself add — including
the entire restored state.
That's not "best-effort" miss rate, it's structurally wrong about
where the data lives. Model would learn to ignore the warning. The
viewer-side log.error on the silent skip (mcp-app.ts:4251) is the right
layer for this.
|
||
|
|
9d55050a00 |
fix(pdf-server): render page before O(numPages) annotation scans (#581)
PR #506 added loadBaselineAnnotations() and buildFieldNameMap() between showViewer() and renderPage(). Both iterate every page calling getPage()+getAnnotations(), so the viewer sat with an unsized empty canvas for ~400ms (15-page doc, scales linearly) before painting. Neither scan blocks the canvas — page.render() only needs canvasContext + viewport. renderAnnotationsForPage() reads annotationMap (graceful empty), AnnotationLayer.render() takes cachedFieldObjects (graceful null). So: render first, scan after, re-render to overlay annotations + form values. Same fix at the reload path. |
||
|
|
4b3f6252a4 |
pdf-server: add save_as interact action (#580)
* pdf-server: add save_as interact action
Adds a model-callable save_as action to the interact tool that writes the
viewer's current annotated state (annotations + form values) to a new file.
Reuses the get_pages request/reply pattern: model's interact call blocks
while the viewer builds bytes via getAnnotatedPdfBytes() and posts them
back through a new submit_save_data app-only tool.
Validation runs synchronously before bothering the viewer:
- path must be absolute or file://
- isWritablePath gate (same as save_pdf)
- existence check; fails with a descriptive error unless overwrite: true
The viewer can report build errors back via submit_save_data({error}),
so the model gets a real message instead of the 45s timeout.
* pdf-server: warn model off app-only tools in descriptions
Hosts that don't filter visibility:["app"] tools from the model's tool
list will still surface these. The save_pdf one in particular is a
near-miss for the new save_as interact action — point the model at the
right one.
* pdf-server: save_as without path overwrites original
When path is omitted, save_as falls back to the displayed PDF's local
path (tracked per-viewUUID by display_pdf). Same writability gate as
the viewer's save button: isWritablePath + OS-level W_OK probe, so it
refuses on read-only mounts before the viewer roundtrip.
Requires explicit overwrite: true since the original always exists —
the model can't accidentally clobber it.
Remote PDFs have no source path stored; the error tells the model to
pass an explicit path.
|
||
|
|
38700569e7 |
fix(pdf-server): radio + dropdown in fill_form/save (#577)
* fix(pdf-server): fail fast on never-polled viewer + surface batch errors
interact's get_screenshot/get_text waited the full 45s for a viewer that
never mounted (host went idle before iframe reached startPolling), then
timed out with a generic message. Now tracks viewsPolled and fails in ~8s
with a recovery hint. Batch errors were also ordered success-first —
hosts that flatten isError to content[0] showed "Queued: Filled N fields"
and dropped the actual failure entirely. Viewer side: poll even when PDF
load throws, and catch connect() rejection instead of silently blanking.
* fix(pdf-server): radio button + dropdown handling in fill_form and save
Three related bugs around non-text form fields:
Viewer fill_form (mcp-app.ts): writing a string to annotationStorage on
every radio widget trips pdf.js's inverted string coercion
(`value !== buttonValue` → true) — the first-rendered widget checks
itself and clears the rest, so whatever you ask for, you get the first
option. Fix: extract a setFieldInStorage() helper that writes
{value: true} only on the widget whose buttonValue matches, {value: false}
on siblings, matching pdf.js's own change handler. Also covers the
syncFormValuesToStorage() path (localStorage restore).
Save (pdf-annotations.ts): buildAnnotatedPdfBytes only knew about
getTextField/getCheckBox — dropdowns and radios threw in the try block
and fell through the catch, so they were silently dropped on save.
Fix: instanceof dispatch on getFieldMaybe(). For radios, accept either
the option label or a numeric buttonValue (indexed into getOptions()),
since the viewer stores the latter for PDFs with /Opt.
display_pdf output (server.ts): radio groups showed as N identical
`name [Btn]` lines with no way to tell them apart; dropdowns didn't
list their options. Fix: surface buttonValue as exportValue and the
options array from the annotation data already in hand.
|
||
|
|
14969c5da2 |
chore: bump ext-apps to 1.3.2 (#569)
Changes since 1.3.1: - Revert fullscreen size-changed skip (#570) - Fix auto-resize destroying horizontal scroll positions (#567) - Remove examples/pdf-server/plugin/ — canonical copy is upcoming in knowledge-work-plugins (#565) |
||
|
|
8961b03163 |
Remove examples/pdf-server/plugin/ — canonical copy is upcoming in knowledge-work-plugins (#565)
This directory duplicated the Claude Code plugin that's being added to the knowledge-work plugin marketplace (anthropics/knowledge-work-plugins#72). Keeping a copy here meant two sources of truth drifting apart — this one is already stale vs. the server's interact/annotation capabilities from #506. The MCPB packaging (manifest.json) remains — that's the distributable server artifact. The Claude Code plugin wrapper belongs in the plugin marketplace repo where it can be versioned and discovered alongside other plugins. Note: committed with --no-verify because the pre-commit hook's 'xargs git add $STAGED' step fails on deletion-only commits (the paths no longer exist). Hook fix tracked separately. |
||
|
|
e8ca859429 |
chore: bump ext-apps to 1.3.1 (#562)
Changes since 1.3.0: - fix(pdf-server): detached-buffer on range re-request + stale e2e assertion (#561) |
||
|
|
e8a4ea5f89 |
fix(pdf-server): detached-buffer on range re-request + stale e2e assertion (#561)
* fix(pdf-server): slice bytes before onDataRange to avoid detached buffer PDF.js transfers the ArrayBuffer to its worker via postMessage, detaching it in the main thread. When the same range is re-requested (common on iOS/WKWebView under memory pressure during scroll/zoom/navigate), the cached Uint8Array now wraps a detached buffer and onDataRange throws 'Buffer is already detached'. Fix: pass result.bytes.slice() so the rangeCache entry stays valid. * test(e2e): update pdf-annotations assertion for new display_pdf prompt text The 91880a6d prompt rewrite changed the interact-enabled result text from 'Displaying PDF (viewUUID: ...)' to 'PDF opened. viewUUID: ...'. The e2e test was still asserting the old string. |
||
|
|
6723c8ff10 |
chore: bump ext-apps to 1.3.0 (#560)
Changes since 1.2.2: - feat: add ui/notifications/request-teardown for view-initiated termination (#215) - feat(pdf-server): annotations, interact tool, page extraction & prompt engineering (#506) - fix(examples): improve mobile support for map, pdf, and threejs examples (#555) |
||
|
|
296b5921fb |
pdf-server: annotations, interact tool, page extraction & prompt engineering (#506)
* pdf-server: add interact tool to allow model to modify existing view
* pdf-server: add annotations, download, form fill, and highlight_text
Add PDF annotation system with 7 annotation types (highlight, underline,
strikethrough, note, rectangle, freetext, stamp), text-based highlighting,
form filling, and annotated PDF download using pdf-lib.
- Server: annotation Zod schemas, extended interact tool with add/update/remove
annotations, highlight_text, and fill_form actions
- Client: annotation layer rendering with PDF coordinate conversion, persistence
via localStorage (using toolInfo.id key), pdf-lib-based download with embedded
annotations and form fills, uses app.downloadFile() SDK with <a> fallback
- Model context includes annotation summary
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add get_pages tool for batch text/screenshot extraction
New tool `get_pages` lets the model get text and/or screenshots from
arbitrary page ranges without navigating the visible viewer.
- Server: `get_pages` tool with interval-based page ranges (optional
start/end, open ranges supported), `getText`/`getScreenshots` flags,
request-response bridge via `submit_page_data` app-only tool
- Client: offscreen rendering (hidden canvas, no visual interference),
text from cache or on-demand extraction, screenshots scaled to 768px
max dimension, results submitted back to server
- Max 20 pages per request, 60s timeout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: move get_pages into interact tool as an action
Fold get_pages into the interact tool to minimize tools requiring
approval. Now accessed via `interact(action: "get_pages", ...)`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: improve interact tool annotation discoverability
- Add concrete per-type schema docs with field names in tool description
- Add JSON example showing add_annotations with highlight + stamp
- Replace opaque z.record(z.string(), z.unknown()) with typed union
of all annotation schemas (full + partial forms) so the model sees
exact field names and types
- Remove redundant manual safeParse since Zod inputSchema validates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: improve annotation discoverability + add E2E tests
- display_pdf result text now explicitly lists annotation capabilities
(highlights, stamps, notes, etc.) instead of vague "navigate, search, zoom, etc."
- Restructured interact tool description: annotations promoted to top,
with clear type reference, JSON example, and bold section headers
- Added pdf-annotations.spec.ts with 6 E2E tests covering:
- Result text mentions annotation capabilities
- interact tool available in dropdown
- add_annotations renders highlight
- Multiple annotation types render (highlight, note, stamp, freetext, rectangle)
- remove_annotations removes from DOM
- highlight_text finds and highlights text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add Claude API prompt discovery tests for annotations
Tests that Claude can discover and use PDF annotation capabilities
by calling the Anthropic Messages API with the tool schemas and
simulated display_pdf result.
Disabled by default — skipped unless ANTHROPIC_API_KEY is set:
ANTHROPIC_API_KEY=sk-... npx playwright test tests/e2e/pdf-annotations-api.spec.ts
3 scenarios tested:
- Model uses highlight_text when asked to highlight the title
- Model discovers annotation capabilities when asked "can you annotate?"
- Model uses interact (add_annotations or get_pages) when asked to add notes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add example prompts, testing docs, and updated tools table to README
- Example prompts for annotations, navigation, page extraction, stamps, forms
- Documents how to run E2E tests and API prompt discovery tests
- Updated tools table to include interact tool
- Updated key patterns table with annotations, command queue, file download
- Added pdf-lib to dependencies list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: simplify interact annotations schema (7802 → 2239 chars)
The typed Zod union (14 anyOf variants: 7 full + 7 partial annotation
types) produced a 5,817-char JSON schema for the annotations field alone.
This bloated the interact tool schema to 7,802 chars, which may cause
the model to struggle with or skip the tool.
Replace with z.record(z.string(), z.any()) — annotation types are
already fully documented in the tool description. Schema drops to
2,239 chars (71% reduction), annotations field to 254 chars (96% reduction).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: enumerate all interact actions in display_pdf result
The display_pdf result text now lists every action by name (navigate,
search, find, search_navigate, zoom, add_annotations, update_annotations,
remove_annotations, highlight_text, fill_form, get_pages) so the model
knows exactly what commands are available without needing to inspect the
interact tool schema.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: validate viewUUID in interact, clarify in description
The model was passing "pdf-viewer" instead of the actual UUID, causing
get_pages to timeout (commands queued under wrong key, client never
picks them up).
- Add activeViewUUIDs set tracking UUIDs issued by display_pdf
- Validate viewUUID at the top of interact handler with clear error
- Add "IMPORTANT: viewUUID must be the exact UUID returned by display_pdf"
to the interact tool description
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix annotation clickability and replace emoji icon with SVG
- Raise annotation-layer z-index above text-layer so note annotations
receive hover/click events (was z-index: 1, now 3; text-layer is 2)
- Replace memo emoji (data-icon attr) with CSS mask SVG document icon
that respects currentColor for consistent cross-platform rendering
* pdf-server: add annotation side panel with bidirectional linking
Right-side panel (250px) shows all annotations grouped by page with
expand/collapse cards. Clicking a card navigates to the page and pulses
the annotation; clicking a note icon on the PDF highlights its card in
the panel. Panel auto-shows on first annotation, remembers user toggle
preference via localStorage, and shows a badge count when collapsed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: replace 300ms interval polling with long-polling
Server now holds poll_pdf_commands requests open (up to 30s) until
commands arrive, waking waiters via enqueueCommand. Client loops
sequentially instead of using setInterval, with 2s backoff on errors.
Reduces idle RPC traffic from ~3 calls/sec to ~2 calls/min.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add interactive form filling via PDF.js AnnotationLayer
Render PDF.js AnnotationLayer with renderForms:true so form fields
appear as interactive HTML inputs. Build a fieldName→annotationID map
via getFieldObjects() to bridge fill_form (which uses field names) with
annotationStorage (which uses annotation IDs). Sync user input back to
formFieldValues for persistence and PDF download.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add elicit_form_inputs to display_pdf for upfront form filling
When enabled and the client supports elicitation, extracts form fields
from the PDF via pdf-lib and prompts the user to fill them before the
viewer loads. Elicited values are returned in content/structuredContent
and enqueued as a fill_form command for the viewer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: replace pdf-lib with pdfjs-dist for server-side form field extraction
Use pdfjs-dist's getDocument() + getFieldObjects() instead of pdf-lib's
PDFDocument.load() + getForm().getFields() in extractFormSchema(). This
removes the pdf-lib import from the server bundle (pdf-lib is still used
client-side for PDF modification in downloadAnnotatedPdf). Uses the legacy
pdfjs-dist build to avoid DOMMatrix dependency in Node.js.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix form field font size and programmatic fill_form
Set --scale-factor/--total-scale-factor CSS variables on the form layer
so AnnotationLayer font-size rules resolve correctly instead of falling
back to browser defaults. Also update live DOM elements directly in
fill_form handler so values appear immediately without waiting for a
full re-render.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix listbox font size overflow in AnnotationLayer
After rendering the annotation layer, shrink select[size] font to fit
within the PDF rect height. The default AnnotationLayer CSS uses a fixed
9px * scale-factor which overflows when many options share a small rect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add trash icon to annotation sidebar cards
Show a delete button on each annotation card that appears on hover.
Clicking it removes the annotation from the PDF and persists the change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix space key captured in form fields
Skip keyboard navigation shortcuts (space, arrows, +/-) when any input,
textarea, or select element is focused, not just the search/page inputs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: show form fields in annotation sidebar with clear buttons
Form field values now appear in the sidebar under a "Form Fields" group,
with trash icons to clear individual values. The badge count and auto-show
logic include form fields. The panel open/close now calls
requestFitToContent with width to avoid overflow in inline layout mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: clarify Y coordinate system in interact tool description
Explicitly state that Y=0 is the bottom edge and Y=792 is the top for
US Letter, with concrete guidance on typical values for top/bottom
placement to prevent models from using top-down screen coordinates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: validate fill_form field names and report valid fields
Extract form field names during display_pdf and cache per viewer UUID.
fill_form now soft-fails on unknown field names (applies valid ones,
reports skipped ones). Both display_pdf and fill_form results include
the list of valid field names so the model can self-correct.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add compact annotation strip for inline mode
In inline mode, replace the 250px side panel with a slim bottom strip
that shows one annotation/form-field at a time with prev/next navigation.
The side panel is still used in fullscreen mode.
- Strip shows item swatch, label, preview + counter (N of M · Page P)
- Click item to navigate to its page; delete single or clear all
- requestFitToContent includes strip height in size calculation
- fieldNameToPage map built during init for form field page context
- Display mode toggle switches between strip and panel automatically
* pdf-server: improve display_pdf description for form/annotation use cases
Add explicit guidance on when to use the tool: filling out forms
(tax forms, applications), annotating PDFs, and interactive review.
This helps models route user requests like 'help me fill out this form'
to the display_pdf tool.
* pdf-server: only show download button when host supports downloadFile
* pdf-server: resizable sidebar + human-readable form field labels
- Sidebar panel is now resizable by dragging its left edge (150px min,
50vw max). Width is persisted in localStorage.
- Form fields show human-readable labels from PDF's TU (alternativeText)
field when available. Mechanical names like ').F1_01[0]' are replaced
with the generic label 'Field' when no TU text exists.
- Empty values are no longer shown in the sidebar.
* pdf-server: fix field label collection to use getAnnotations() API
getFieldObjects() doesn't include alternativeText. Instead, iterate
per-page annotations (only pages with known form fields) to collect
the TU/alternativeText labels that PDF authors set as tooltips.
* pdf-server: move strip below toolbar + follow form field focus
- Strip is now positioned below the toolbar (above search bar) instead
of at the bottom, for better visibility and proximity to the toolbar.
- Form field focus changes automatically sync the strip to show the
focused field's item.
- Search bar top offset adjusts dynamically when strip is shown/hidden.
* pdf-server: add Clear all to fullscreen sidebar + steer model to reuse views
- Add 'Clear all' button in the fullscreen sidebar panel header
- Update display_pdf description to instruct models to use the interact
tool with existing viewUUID rather than calling display_pdf again for
the same PDF (e.g. when user asks to annotate an already-open doc)
* pdf-server: add page dimensions to model context + improve coordinate docs
- Model context now includes 'Page size: 612x792pt (coordinates: origin
at bottom-left, Y increases upward)' so the model knows actual page
dimensions when placing annotations.
- Interact tool description expanded with clearer coordinate guidance,
margin hints, and a tip to use highlight_text for text annotations.
- Download button temporarily shown unconditionally (capability check
commented out with TODO for re-enabling).
* pdf-server: auto-close panel when empty + stronger display_pdf reuse guidance
- Sidebar/strip auto-closes when all items are cleared (via delete or
Clear all), and the annotations toolbar button hides.
- display_pdf description now uses CRITICAL warning to prevent models
from re-calling it to add annotations to an already-open PDF.
* pdf-server: skip elicitation when form fields have mechanical names
Only use elicitation if all editable fields have human-readable names.
Fields with brackets, dots, or ALL_CAPS_UNDERSCORE patterns (and no
alternativeText label from the PDF) are considered mechanical and
would be confusing in an elicitation form. Also use alternativeText
as the elicitation field title when available.
* pdf-server: paint annotations on screenshots + list coords in model context
Screenshots sent to the model (both updateModelContext and get_pages)
now include annotations drawn on top of the PDF canvas, so the model
can visually verify placement. The text context also lists each
annotation on the current page with its coordinates.
* pdf-server: style multiselect/listbox form fields to match viewer theme
* pdf-server: add command batching to interact tool
Extract command processing into a helper function and accept an optional
`commands` array for sequential batch execution. This lets the model
send multiple actions (e.g. add annotations + get screenshot) in a single
tool call, reducing round trips. Stops on first error in a batch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix multiselect rendering by removing font-size override
The custom font-size: inherit on #form-layer select was conflicting with
PDF.js AnnotationLayer's inline font-size, causing doubled/overlapping text
in select options. Remove the override and keep only the checked-option
highlight style.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix form reset button by passing fieldObjects to AnnotationLayer
PDF.js AnnotationLayer needs the fieldObjects parameter to bind reset
form button actions. Cache the result of doc.getFieldObjects() and pass
it through to annotationLayer.render().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: improve annotation labels in sidebar and strip
Use human-readable labels instead of raw type names (e.g. "Rectangle"
instead of "rectangle", "Stamp: APPROVED" instead of "stamp"). Remove
redundant preview text for types that have no additional content (rectangle,
underline, strikethrough, stamp) to avoid "rectangle Rectangle" duplication.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: split get_pages into get_text and get_screenshot commands
Replace the combined get_pages action (with getText/getScreenshots flags)
with two focused commands:
- get_text: extract text from pages (single page or intervals)
- get_screenshot: capture a single page as PNG image
This makes the API cleaner and returns each command's result as its own
content block in batch mode. Also fix download function syntax error
from earlier capability check commenting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix download by using pdfDocument.getData() instead of re-fetching
The download was failing with "No PDF header found" because it re-fetched
the PDF via read_pdf_bytes tool calls, but stateless MCP transport creates
fresh server instances per request with empty caches. Using getData() from
the already-loaded pdf.js document avoids the round-trip entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: increase express body limit to fix get_screenshot hang
The submit_page_data tool sends base64 PNG screenshots that exceed the
default 100KB express.json() body limit from createMcpExpressApp. This
caused the request to silently fail, making waitForPageData timeout and
the get_screenshot command hang. Replaced createMcpExpressApp with manual
express setup using a 50MB body limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: use JPEG for screenshots to fix get_screenshot hang
Switch from PNG to JPEG (quality 0.85) for both offscreen screenshots
(get_screenshot) and model context images. JPEG is much smaller than PNG
for document pages, keeping the submit_page_data payload well within the
MCP SDK's default 100KB express body limit. This also reverts the
temporary workaround of replacing createMcpExpressApp with a custom
express setup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add ' - edited' suffix to download filename when modified
Renames the download from '_annotated' to ' - edited' and also triggers
the suffix when form fields have been filled, not just when annotations
are present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: include viewUUID in display_pdf structuredContent
The viewUUID was only in _meta but not in structuredContent, so the model
couldn't retrieve it from the tool result to pass to interact.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: move interact action list from display_pdf content to description
The content response was bloated with static instructions about interact
actions. Moved that to the tool description where it belongs, keeping
only the dynamic PDF URL and viewUUID in the content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix form reset setting dropdowns to first item
PDF.js resetform handler deselects all options but doesn't re-add the
hidden empty placeholder, so the browser falls back to showing the first
real option. Work around this by listening for resetform on combo selects
and re-inserting a hidden blank option when nothing is selected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: sort form fields in sidebar/strip by intrinsic PDF order
Fields are now sorted by page number then top-to-bottom Y position on
the page, matching the visual reading order of the PDF form rather than
the arbitrary insertion order of formFieldValues.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add viewUUID to display_pdf outputSchema
The viewUUID was added to structuredContent but not to the outputSchema,
causing MCP validation error -32602.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: allow horizontal scrolling in inline mode
When the PDF is zoomed or wider than the container, users can now scroll
horizontally in inline mode instead of the content being clipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add viewUUID to model context, remove activeViewUUIDs validation
Include viewUUID in the updateModelContext header so the model can
reference it when calling interact. Remove the activeViewUUIDs set
validation since it breaks after server restarts — command queue TTL
pruning is sufficient cleanup. Also prune empty queues with no active
pollers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix delete button alignment in sidebar annotation cards
Annotations without preview text had the delete button hugging the type
label instead of right-aligning. Add margin-left: auto so the button is
consistently pushed to the right edge of the row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add interactive annotations (select, drag, resize, rotate, undo/redo)
Enable pointer-events on all annotation types so they can be clicked to
select. Draggable types (rectangle, freetext, stamp, note) support smooth
drag-to-move. Rectangles show corner resize handles, stamps show a rotate
handle. Delete/Backspace removes the selected annotation, Ctrl+Z/Shift+Z
provides unlimited undo/redo, and Escape deselects. Sidebar cards sync
with on-canvas selection state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: clicking sidebar card focuses form field input and scrolls into view
Sidebar annotation cards now scroll the annotation into view, and form
field cards navigate to the correct page and focus the input element.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix form widget double-text by making inputs opaque
PDF.js AnnotationLayer renders interactive form inputs with a
semi-transparent background, letting the canvas's static form appearance
bleed through. Since the widget font (9px * scale, sans-serif) doesn't
match the PDF's actual font metrics, this creates a "double text" misalignment
at most zoom levels. Make text/choice widgets opaque so only the interactive
layer's text is visible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add selection/focus to model context, double-click to send messages, toggle resize/rotate
- Model context now includes selected annotation ID and focused form
field name so the model knows what the user is interacting with.
- Double-clicking a sidebar annotation card sends a message prompting
the model to change that annotation.
- Double-clicking a sidebar form field card sends a message prompting
the model to fill that field.
- Double-clicking an annotation in the renderer toggles between resize
and rotate handles (for rectangles and stamps).
- Rectangles now support rotation via the rotate handle.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: replace inline-mode strip with floating accordion sidebar
Replace the compact horizontal annotation strip with a floating panel
at top-right in inline mode. Uses accordion-style collapsible sections
(per-page annotations + form fields) with at most one section open at
a time. Refactors renderAnnotationPanel into reusable helpers
(appendAccordionSection, createAnnotationCard, createFormFieldCard).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: remove dead annotation strip code
The compact horizontal annotation strip is no longer used since inline
mode now uses a floating accordion sidebar. Remove all strip-related
HTML, CSS, JS (DOM refs, StripItem type, buildStripItems, renderStrip,
navigateToStripItem, deleteStripItem, event listeners, stripHeight
calculations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: multi-select annotations, natural prompts, form reset sync
- Convert selection from single ID to Set for multi-select (Shift+click)
- Deselect annotations when focusing a form input field
- Fix rotation to always use center origin (stamp was using left-bottom)
- Simplify double-click sendMessage prompts (e.g. "update Notes: ")
- Call updateModelContext before sendMessage to guarantee context is sent
- Listen for PDF.js resetform events to clear formFieldValues, update
sidebar badge/count, and hide icon when no items remain
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: draggable floating panel with magnetic corner anchoring
- Reduce floating panel width (220→180px), add border-radius
- Panel header is draggable; on drop, snaps to nearest corner
- Persists corner preference to localStorage
- Auto-docks to opposite side when overlapping selected annotations
- Accordion chevron: ▶ when collapsed, ▼ when expanded
- Slightly reduce section header size for compactness
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: allow all accordion sections to be collapsed
Only auto-open the first section on initial render. Once the user has
interacted with accordion headers, respect their choice to collapse all
sections without forcing one back open.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: polish interactions and UI details
- Remove auto-show annotation panel; wait for user to open it
- Badge color: blue (accent) instead of red
- Search bar: offset floating panel below it when anchored top-right
- Text selection deselects annotations and clears field focus
- Click on text layer / empty area clears field focus too
- Zoom keeps selected annotation visible (scrollIntoView after render)
- Fullscreen button: distinct exit icon + tooltip with Esc hint
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix text deselect, stamp dblclick, rotation direction, export
- Text selection and text-layer mousedown now deselect annotations
- Add user-select: none on draggable annotations (prevents stamp text
selection on double-click, allowing resize/rotate toggle to work)
- Fix rotation direction: remove CSS transform negation so clockwise
drag = clockwise rotation on screen
- Fix stamp export: rotate around center (matching CSS transformOrigin)
and negate rotation for PDF coordinate convention
- Add rotation support to rectangle PDF export
- Remove auto-show annotation panel; badge uses blue accent color
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: allow any text as stamp label, not just predefined enum
The StampLabel was unnecessarily restricted to 6 predefined values
(APPROVED, DRAFT, etc). The PDF format supports arbitrary stamp text,
so open it up to z.string() while keeping common labels as examples
in the schema description.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: show resize + rotate handles simultaneously, dblclick sends message
Remove the interactionMode toggle — instead show both corner resize handles
and the rotate handle at the same time on selected annotations. Rectangle
gets resize corners + rotate handle; stamp gets rotate handle only.
Double-click on annotation elements now sends a message to modify the
annotation (matching sidebar card behavior) instead of toggling modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: SVG fullscreen icons, Y-coord docs, dirty flag
- Replace Unicode fullscreen chars (⛶/⊠) with proper SVG expand/collapse icons
- Add Cmd+Enter keyboard shortcut for fullscreen toggle
- Improve coordinate system docs: emphasize rectangle x,y is BOTTOM-LEFT corner, add worked examples
- Add isDirty state with * prefix on title for unsaved changes
- Track dirty state through persistAnnotations, suppress during restore
* pdf-server: diff-based annotation persistence, proper PDF annotation export, PDF annotation import
Extract annotation logic into pdf-annotations.ts module with comprehensive tests. Replace content-stream drawing (drawRectangle/drawText) with proper PDF annotation dicts (/Type /Annot) that are editable in Acrobat/Preview. Persistence now stores only a diff (additions/removals) relative to the PDF's native annotations, keeping localStorage small. On load, annotations are imported from the PDF via pdf.js and merged with the user's diff.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: enhance page shadow for better depth perception
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: use floating panel in fullscreen, respect insets, direction-aware resize
Always use the floating annotations panel (both inline and fullscreen) instead
of the sidebar in fullscreen mode. Panel anchorage now respects safe area insets
(especially bottom). Resize handle repositions to the correct edge based on
which corner the panel is anchored to. Initial width reduced from 180px to 135px.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add save_pdf tool and save button for local files
Add server-side save_pdf tool that writes annotated PDF bytes back to the
original local file. Client shows a save button (floppy disk icon) next to
the download button for file:// URLs. Ctrl/Cmd+S shortcut saves when dirty.
Saving clears the dirty flag and localStorage diff.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add circle and line annotation types synced with PDF.js
Add circle (ellipse) and line annotation support across all layers:
server Zod schemas, pdf-annotations module (import/export), client
renderers, canvas painting for screenshots, CSS, drag/resize handling,
and tool description documentation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add searchability keywords to display_pdf tool description
Include viewer, render, renderer, display, show, annotate, edit, and
form keywords so the tool is easier to discover by models.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: switch model-facing annotation API to top-left origin coordinates
Convert Y coordinates at the command boundary (add/update_annotations) so the
model uses intuitive top-left origin while internal storage stays in PDF's
native bottom-left origin. Also converts coordinates back to model space in
context strings for consistency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix stamp rotation in PDF export via appearance stream matrix
Stamp rotation was a no-op (empty if-block with a comment). Now computes
a proper rotation matrix around the bounding box center and applies it
to the appearance stream. Also adds verification tests for rectangle
stroke/fill colors and stamp color/rotation in exported PDF dicts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix screenshots to include filled form field values
Screenshots sent to the model were missing form field values because
the canvas render didn't use ENABLE_STORAGE mode. Now both offscreen
renders (model context and get_pages) pass annotationStorage to PDF.js
so filled fields appear in screenshots. Also reuses renderPageOffscreen
for model context screenshots to deduplicate rendering logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: list form fields with page, bounding box, and label in display_pdf result
The model needs field locations to fill forms accurately, especially for
fields with mechanical names or no name at all. Extracts per-page bounding
boxes in model coordinates (top-left origin) and includes labels where
available from PDF alternativeText.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: include full field listing in fill_form result and update UI after fill
fill_form results now include all form fields with names, types, pages, and
bounding boxes so the model can self-correct on unknown field names. Also
updates the annotation panel badge and visibility after programmatic fills.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add formFields to display_pdf structuredContent and output schema
Programmatic consumers need structured access to form field metadata
(names, types, pages, bounding boxes) without parsing text content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add image annotation type with URL/base64 support, drag-drop, and PDF export
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix image annotation click/select, add Shift-resize for aspect ratio, add handle tooltips
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix image annotation to stretch to fill its bounding box
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: fix stamp PDF export to match on-screen appearance
Use non-standard /Name to prevent Preview.app from substituting built-in
stamp graphics, align padding with CSS (4pt/12pt), add ExtGState for 0.6
opacity, and fix text baseline positioning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: improve annotation panel UX and image resize behavior
- Page accordion click navigates to that page
- Form fields appear in their page's accordion section (before annotations)
- Selecting annotation or focusing form field auto-expands page accordion
- Image resize preserves aspect ratio by default (Shift for free resize)
- Add aspect field ("preserve"|"ignore") to ImageAnnotation type
- Fix selection UI regression: remove overflow:hidden that clipped handles
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add signature/signing keywords and image example to tool descriptions
- Add signing/signature keywords to display_pdf description for discoverability
- Add image annotation example (signature placement) to interact tool description
- Clarify that imageUrl supports file paths and HTTP URLs with auto-fetch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: discourage base64 encoding in image annotation, clarify imageUrl accepts file paths
Models tend to encode images as base64 themselves instead of passing file
paths. Make the schema descriptions and tool docs explicit: prefer imageUrl
with a file path, no data: URIs, don't encode images yourself.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: remove imageData from image annotation API, require imageUrl
Models were encoding images as base64 themselves instead of passing file
paths. Remove imageData from the model-facing Zod schema entirely — only
imageUrl (file path or HTTPS URL) is accepted. The server fetches and
embeds the image automatically. imageData remains as an internal field
for client-side rendering and drag-drop. Also mention drag-and-drop as
an alternative in the tool description.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add clipboard support (copy/cut/paste annotations and images)
- Request clipboardWrite permission in resource metadata
- Ctrl/Cmd+C: copy selected annotations as JSON to clipboard
- Ctrl/Cmd+X: cut (copy + delete) selected annotations
- Ctrl/Cmd+V / paste event: paste annotations from clipboard JSON,
or paste images from clipboard (e.g. screenshots, copied images)
- Refactor drag-drop image creation into shared addImageFromFile()
- Pasted annotations get new IDs and slight offset to avoid overlap
- Pasted images are centered on the current page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: add disableInteract option for distributed deployments
Adds a `disableInteract` boolean to `createServer()` options that skips
registering the `interact`, `poll_pdf_commands`, and `submit_page_data`
tools (all relying on an in-memory command queue). Adjusts `display_pdf`
description and schema accordingly when disabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: flip interact to opt-in, enable only for --stdio
Rename disableInteract → enableInteract (default false) so existing
library consumers get read-only mode without code changes. The CLI
entry point passes enableInteract: true only when running with --stdio.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* pdf-server: log client-roots warning once at startup, not per request
* pdf-server: fix form widget transparency, poll spam, and pdf.js warnings
- Form widgets: PDF.js writes inline background-color (from the
annotation /BC entry, typically 'transparent'), defeating our CSS.
Force opaque with !important. Fixes double-text in listboxes and
text inputs at any zoom.
- Poll spam: view was calling poll_pdf_commands in a tight loop when
interact is disabled (HTTP mode) — server returns isError, which
doesn't throw, so the loop never backed off. Server now signals
interactEnabled via _meta; view only polls when true, and stops
entirely on isError results.
- Silence Helvetica / inset-border warnings: set verbosity=ERRORS
for server-side getDocument (only used for form introspection,
no rendering, so font fallback warnings are noise).
* pdf-server: set standardFontDataUrl for server-side pdf.js
Resolve pdfjs-dist/standard_fonts/ at runtime via createRequire. Works
from both source (bun main.ts) and bundled dist/ since pdfjs-dist is
--external. NodeStandardFontDataFactory reads via fs.readFile, so a
filesystem path with trailing separator is what's expected.
Keep verbosity=ERRORS as backstop for unrelated warnings (e.g.
'Unimplemented border style: inset').
* pdf-server: use unpkg CDN for pdf.js standard fonts, add to CSP
- Single CDN URL pinned to the bundled pdfjs-dist version, used in both
server (Node) and viewer (browser)
- Node: default NodeStandardFontDataFactory uses fs.readFile which can't
fetch URLs — pass a minimal FetchStandardFontDataFactory
- Browser: DOMStandardFontDataFactory already fetches, just pass the URL
- Add https://unpkg.com to CSP connectDomains (pdf.js loads font
binaries via fetch(), maps to connect-src)
* pdf-server: guard against missing totalBytes in viewer
Without this, an invalid totalBytes flows to PDFDataRangeTransport.length
→ worker ChunkedStream sized at 0 → opaque 'Bad end offset: N' error.
The guard turns that into an actionable message (rebuild hint).
* pdf-server: re-probe file size at load time, ignore stale totalBytes from history
The display_pdf result (including totalBytes) is baked into conversation
history. If the user saves the annotated PDF (grows the file) then
reloads the conversation, the host replays the stale size. pdf.js
initializes its ChunkedStream at that stale length, then the first live
read_pdf_bytes returns the current (larger) file — every chunk fails
the 'end === bytes.length' check with 'Bad end offset: N'.
Fix: drop the fileSizeBytes param and always probe via a live 1-byte
read_pdf_bytes call before setting up the range transport.
* pdf-server: file watching, save confirmation, disk-change reload
Server:
- fs.watch on local files when interact is enabled; pushes file_changed
via the poll_pdf_commands channel. Re-attaches on rename to survive
atomic writes (vim/vscode). Debounced 150ms. Skips unchanged mtime.
- save_pdf returns {filePath, mtimeMs} so the saving viewer can
recognise its own write's echo. Other viewers on the same file still
get notified (their content is genuinely stale).
- stopFileWatch in pruneStaleQueues TTL sweep.
Viewer:
- In-app confirm dialog (no window.confirm — would block the iframe).
- Save button: hidden until first edit, enabled while dirty, disabled
(but still visible) after save, re-enabled on next edit.
- savePdf() asks for confirmation, records lastSavedMtime.
- loadGeneration counter invalidates stale fetchChunk results and
aborts the preloader when the PDF is reloaded underneath them.
- reloadPdf() clears all per-document state (byte cache, annotations,
form fields, undo/redo, text caches, field maps, localStorage diff)
and loads fresh.
- file_changed handler: suppresses own-save echo via saveInProgress +
mtime match; auto-reloads when clean; prompts Keep/Discard when dirty.
* pdf-server: add file watching tests
Covers: external write → file_changed enqueued via poll_pdf_commands;
debounced rapid writes; stopFileWatch prevents further events; save_pdf
returns mtimeMs; watcher survives atomic rename (vim/vscode pattern).
* pdf-server: fix confirm-btn-primary hover, show filename only in save prompt
- .confirm-btn:hover's background cascaded to primary buttons too
(same specificity, primary has both classes) — turning the blue
button light grey on hover. Re-assert background in primary:hover.
- Save confirm: extract just the filename from pdfUrl (strip file://
prefix, take basename).
* pdf-server: match confirm dialog to host's native style
Use host-provided CSS variables (--color-*, --font-*, --border-radius-*,
--shadow-*) from applyHostStyleVariables, with local fallbacks.
- Larger border-radius (--border-radius-xl), softer shadow (--shadow-lg)
- Larger bold title (--font-heading-md-size, --font-weight-bold)
- Primary button uses inverse colors (dark bg + light text) like the
host's downloadFile dialog, not blue
- New .confirm-detail box: monospace bordered rect for filename
- Button order: Cancel first, primary last (native convention)
- Escape resolves to first non-primary button
* pdf-server: derive dirty from diff — undoing all changes clears save button
persistAnnotations() was unconditionally setDirty(true). It already
computes the diff vs baseline — use isDiffEmpty(diff) to decide instead.
Now undoing all the way back to the original state marks the viewer
clean again (save button disables, title loses asterisk).
* pdf-server: import saved form field values from PDF as baseline
getFieldObjects() returns the PDF's stored form values but we were only
reading field IDs/pages from it. After saving a filled form and reopening,
the panel showed nothing and there was no way to see what was filled.
- New pdfBaselineFormValues map, populated in buildFieldNameMap() from
each field's .value (skipping empty/Off/button values). Seeds
formFieldValues so the panel shows PDF-stored values on open.
- computeDiff takes an optional baselineFormFields param and only
includes values that differ — opening a filled PDF doesn't mark dirty,
editing a field does, reverting to the PDF's value marks clean again.
- importFieldValue() normalises radio-group value lookup (parent entry
has value=undefined, children have the real export value), checkbox→
true, listbox array→joined string.
* pdf-server: fix form Reset button; split panel into Reset vs Clear all
Reset-button regression: buildFieldNameMap() now seeds formFieldValues
from the PDF's stored values, but syncFormValuesToStorage() was pushing
them back into annotationStorage in our normalised repr (checkbox→true,
radio→export string). pdf.js's native repr may differ, and overwriting
it breaks the form's Reset button. Fix: skip syncing values that match
baseline — the PDF's own values are already in storage natively.
Panel buttons:
- Reset: revert to what's in the PDF file. Restores baseline annotations
and form values, clears undo/redo. Empty diff → clean.
Disabled when not dirty.
- Clear all: removes EVERYTHING including PDF-native items. Non-empty
diff (baseline items are 'removed') → dirty; saving writes stripped PDF.
Disabled when nothing to clear.
* pdf-server: fix Clear all to push defaultValue; gate save on writability
Clear all regression: annotationStorage.remove(id) only drops our
override — the widget reverts to the PDF's stored /V (the baseline
value). To actually clear, push each field's defaultValue (/DV) via
setValue instead, which is what the PDF's native Reset button does.
Save button visibility: server now checks fs.access(path, W_OK) and
reports via _meta.writable. Viewer gates save on that instead of just
'is this a local path'. Hides the button for read-only mounts and
Claude Desktop's uploads directory. Removed dead isLocalFileUrl().
* pdf-server: fix annotationStorage keys — use widget annotation IDs
Root cause: getFieldObjects() returns field-dictionary refs (the /T
tree, e.g. '86R'), but annotationStorage is keyed by WIDGET annotation
refs (what page.getAnnotations() returns, e.g. '9R'). These differ
when a PDF's field and its widget /Kids are separate objects.
fieldNameToIds was built from getFieldObjects().id, so EVERY
storage write was keyed wrong and silently no-op'd:
- syncFormValuesToStorage: wrote to dead keys
- fill_form: setValue + querySelector([data-element-id=...]) both missed
- individual field delete: remove() on wrong key
- clearAllItems: setValue to wrong keys
Fix: build fieldNameToIds from page.getAnnotations() which gives the
correct widget IDs. Keep cachedFieldObjects only for type/value/
defaultValue metadata (matched by name, not id) and for passing to
AnnotationLayer.render() where pdf.js uses it internally.
Also extract clearFieldInStorage() helper — pushes defaultValue for a
single field, used by both individual delete and Clear all.
* pdf-server: remap fieldObjects IDs to widget IDs for pdf.js Reset
pdf.js _bindResetFormAction iterates fieldObjects using each entry's .id
to (a) key annotationStorage and (b) querySelector([data-element-id=...]).
Both expect WIDGET annotation IDs. fieldObjects contains field-dict IDs.
Works only when field and widget share a PDF object — which pdf-lib's
save breaks (it splits merged objects on write).
This is a latent pdf.js bug, not a regression from our changes — it was
broken the moment the user first saved via save_pdf.
Fix: rebuild cachedFieldObjects with widget IDs (from fieldNameToIds)
before passing to AnnotationLayer.render({fieldObjects}). Skip parent
entries with no concrete id (radio group /T tree root). Preserve
type/defaultValue/exportValues which Reset needs.
* pdf-server: fix combo reset, scope writability, drop flaky CI assert
Combo reset: pdf.js's resetform handler sets all option.selected =
(option.value === defaultFieldValue); when defaultFieldValue is null
nothing matches. Chrome then synchronously normalises the non-multiple
<select> by auto-selecting option[0] — so by the time our fix-listener
ran, selectedIndex was 0 and the state-check failed. Now: capture
whether a real default exists before the event; if reset didn't land
on it, force-prepend a hidden blank and select it explicitly.
(Was latent — before the ID fix, resetform never dispatched on selects.)
Writability scope: only mark writable if the file is explicitly in
allowedLocalFiles (CLI arg = opt-in) OR strictly under an allowed
root directory (isAncestorDir already excludes the root itself via
rel !== ''), AND fs.access W_OK passes. A root passed by the client
is a boundary, not a target.
CI: drop the post-rename second-write assertion — fs.watch re-attach
semantics differ between kqueue and inotify, inherently racy in CI.
Only assert the rename itself is detected.
* pdf-server: fix Reset bug; show cleared baseline fields w/ revert
Reset-all bug: clearUserFormStorage skipped any field whose name was
in pdfBaselineFormValues — but if the user had EDITED a baseline field,
the edit sits in storage under that name. Skipping it left the widget
showing the stale edit while the panel showed the restored baseline.
Fix: remove ALL storage overrides on reset — every field reverts to the
PDF's /V, which IS baseline. Removed the now-dead helper.
Panel: cleared baseline fields now stay visible instead of vanishing.
State is derived per-field by comparing formFieldValues to
pdfBaselineFormValues (no new data structures — both maps already
exist, the comparison was just never rendered):
- unchanged: current === baseline → solid swatch, trash clears
- modified: baseline exists, current differs → solid swatch, revert
- cleared: baseline exists, current empty/absent → outlined cross
swatch, struck-out label/value, revert restores
- added: no baseline → solid swatch, trash removes
Panel iterates union(formFieldValues, pdfBaselineFormValues) so cleared
items don't disappear. sidebarItemCount uses the same union so the
toolbar button stays visible as long as there are baseline items OR
edits. Per-item revert: formFieldValues.set(name, baseline) +
storage.remove(id) → widget reverts to /V.
* pdf-server: import baseline from widget fieldValue, not field-dict
page.getAnnotations().fieldValue is what AnnotationLayer actually
renders. getFieldObjects().value reads the field-dict /V — which can
be out of sync in an internally-inconsistent PDF (e.g. after a pdf-lib
setText silently failed on a comb field, leaving the field-dict stale
while the widget still shows the older value from the raw bytes).
Your Form.pdf: ID widget shows 'eeeee', field-dict says 'eew2e'.
Panel was importing 'eew2e' → mismatch with what's on screen.
Fix: capture a.fieldValue during the page.getAnnotations scan (we're
already iterating there for widget IDs) and prefer it over the
field-dict value. Fall back to field-dict if the widget doesn't expose
one. normaliseFieldValue() handles the format differences (choice
widgets give arrays; field-dict gives strings).
* pdf-server: fix radio input → 'on'; use export value for consistency
pdf.js creates <input type=radio> without setting .value (pdf.mjs:
18138-18144), so target.value defaults to the HTML spec's 'on'. Our
input listener read that → panel showed 'on' after clicking a radio,
but baseline import correctly used the export value ('0') — so
clicking then reverting showed two different strings for the same
selection.
Fix: store each radio widget's buttonValue during the annotation scan
(we're already there for widget IDs), read it in the input listener
via data-element-id. Now baseline, click, revert, and save all agree
on the export value.
(Form.pdf happens to use '0'/'1' as export values instead of
'Male'/'Female' — that's the form's design; the labels are just PDF
text next to the widgets, not form data.)
* pdf-server: panel Reset/Clear-all as icon buttons, keep tooltips
* pdf-server: file roots from MCP client are read-only
allowedLocalFiles conflated two sources: CLI args (user explicitly
named the file when starting the server — overwriting is intentional)
and MCP file roots (client-uploaded copies in ad-hoc hidden folders
that the client doesn't expect to change).
New cliLocalFiles set tracks only the CLI-sourced files. Writable now
requires: file is in cliLocalFiles OR strictly under a directory root.
Directory roots are mounted folders where saving is expected.
save_pdf enforces the same scope server-side — the viewer hides the
button based on _meta.writable, but we must not trust the client.
* pdf-server: file root stays read-only even under a directory root
Extracted isWritablePath() with the full policy in one place (was
duplicated between display_pdf and save_pdf):
- CLI file → writable (user explicitly named it)
- MCP file root → read-only, ALWAYS. Even when the path happens to
fall inside a mounted directory root — the client sending it as a
file root is the stronger signal ('here's an upload')
- under a directory root at any depth → writable (mounted folder)
- the directory root itself → not writable (rel === '' excluded)
- no roots and no CLI files → nothing writable
8 unit tests cover each case.
* pdf-server: fix e2e (interact for HTTP test harness), skip rename test on Linux
E2E: tests/e2e/pdf-annotations.spec.ts expects 'interact' in the tool
dropdown and calls it for annotation tests. Since 3f2c209c interact is
stdio-only; e2e runs HTTP basic-host → tool absent → all tests fail
(broken since Mar 4). Add --enable-interact CLI flag for HTTP mode and
pass it in 'npm run serve'. The in-memory command queue works fine for
the e2e setup (one long-lived process); the stdio-only restriction is
about stateless multi-instance deployments.
Rename test: inotify watches inodes. When the directory entry is
atomically swapped to a NEW inode, the watcher on the OLD inode gets
no event. kqueue (macOS) watches paths and fires 'rename'. Gate with
skipIf(platform !== 'darwin').
* pdf-server: list_pdfs walks directory roots for *.pdf files
Recurse into allowedLocalDirs (depth ≤ 8, ≤ 500 files) and enumerate
actual .pdf files as file:// URLs alongside the explicitly-registered
ones. Skip dotfiles/dirs and node_modules. Dedup (a file may be both
explicitly registered and under a walked directory). Report truncation.
* pdf-server: fix stale e2e assertion after 2d47f14b
Commit 2d47f14b moved the interact action list from display_pdf's
content to the tool description, but the e2e test still asserted the
result text contained 'add_annotations', 'highlight_text', etc.
Now checks for viewUUID / interactEnabled / 'Displaying PDF' which
are the actual result contents.
* pdf-server: add unpkg to resourceDomains (font-src) for pdf.js FontFace
pdf.js in the browser doesn't just fetch() the .ttf bytes — it also
creates FontFace('name', 'url(https://unpkg.com/...)') which goes
through CSP font-src, not connect-src. Nest's CSP blocked it
('font-src self assets.claude.ai'). pdf.js fell back to system fonts
so nothing broke visually, but the CDN URL was doing nothing.
(Separate finding re: the 'bricked UI' report — logs show fill_form
processed cleanly at 16:27:18, then Nest's router navigated away 13s
later. Poll id=17 returned to an already-unmounted iframe. Normal
teardown, not a brick.)
* pdf-server: validate imageUrl scheme before <img src> assignment
Fixes CodeQL js/xss + js/client-side-unvalidated-url-redirection on
line 1994 (renderImageAnnotation) and the canvas-paint fallback.
The server resolves imageUrl to imageData before enqueueing, so
def.imageUrl only survives to the client if the server-side fetch
fails. safeImageSrc whitelists https/http/data/blob and returns
undefined for javascript: et al.
* pdf-server: fix get_pages timeout race; trim fill_form response
get_pages round-trip (interact → enqueue → poll → render → submit):
- GET_PAGES_TIMEOUT_MS 60s → 45s. The MCP SDK also defaults to 60s, so
if the round-trip got close, the client timed out first and our error
went nowhere. Server rejects first now.
- waitForPageData wires extra.signal. If the client cancels interact
(timeout/user abort), we clean up the pendingPageRequests entry
immediately instead of leaking until our own timeout. Also simplified
to a single settle callback instead of {resolve, reject, timer}.
- handleGetPages .catch() → submit empty pages. Was fire-and-forget
with no error path; any rejection before the inner try/catch meant
no submit_page_data → server waited the full timeout.
fill_form response: dropped the full field listing that was appended
on every call. display_pdf already returns it; echoing it back after
every successful fill is noise. Only list valid field names when the
model got one wrong.
* pdf-server: return URL.href from safeImageSrc so CodeQL sees sanitizer
The previous attempt returned the raw input string after a scheme
check, which CodeQL's taint tracker doesn't recognise as a sanitiser
(it sees tainted in → tainted out). Now parse with the URL constructor
and return the .href property — this is a canonical sanitiser pattern
CodeQL models as a barrier for js/xss and unvalidated-url-redirection.
Also folded the data: URI branch into safeImageSrc for a single call
site responsible for producing the <img src> string.
* feat(pdf-server): treat uploads-root dirs as read-only by default
Dir roots whose basename is 'uploads' (e.g. Claude Desktop's attachment
drop folder) are now treated as read-only unless --writeable-uploads-root
is passed. This prevents the save button from appearing for attached PDFs
that the client doesn't expect to be overwritten.
Also gates the _debug diagnostic block on --debug and adds save/restore
of writeFlags.allowUploadsRoot in test beforeEach/afterEach.
* feat(pdf-server): add --debug flag, fix interact timeout, skip persist for read-only cmds
- Wire --debug CLI flag through to createServer; _debug diagnostic block
in display_pdf _meta is now only emitted when --debug is set.
- Viewer: showDebugBubble renders _debug payload as a fixed overlay.
- Fix interact get_pages timeout: await handleGetPages instead of
fire-and-forget, so submit_page_data doesn't queue behind the next
30s long-poll on serialized host connections.
- Skip persistAnnotations for read-only commands (get_pages, file_changed).
* fix(pdf-server): rebase annotations baseline after save
After a successful save_pdf, update pdfBaselineAnnotations and
pdfBaselineFormValues to reflect what was written to disk. Without
this, removing all annotations after a save produced an empty diff
(compared to the stale pre-save baseline), incorrectly disabling
the save button even though the file on disk still had annotations.
* fix(pdf-server): sync annotation panel on canvas click, fix tooltip z-index
Clicking an annotation on the canvas now re-renders the panel to
expand the correct page section and scrolls the card into view.
Previously only the selection class toggled without updating the
accordion.
Raise tooltip z-index to 100 within the annotation layer and
promote the annotation layer above the form layer on note hover
(:has selector) so tooltips aren't clipped by the form overlay.
* debug(pdf-server): add __pdfDebug() helper + log skipped baseline annotations
Adds window.__pdfDebug() that dumps annotationMap, baseline,
layer children, and localStorage diff to diagnose ghost annotations
(visible on canvas but not in panel, not selectable).
Also logs when importPdfjsAnnotation returns null for non-widget
annotations, and surfaces any thrown errors during baseline import
instead of silently swallowing them.
* debug(pdf-server): expose pdfDocument.annotationStorage + window.__pdf internals
__pdfDebug() now also dumps PDF.js's annotationStorage contents
(editor stamps live there, invisible to our tracking) and all
localStorage keys matching pdf-annot pattern.
After running __pdfDebug(), internals are exposed as
window.__pdf.{pdfDocument, annotationMap, annotationLayerEl, formLayerEl}
for interactive console poking.
* fix(pdf-server): hide viewUUID when interact disabled, widen annotations panel
- display_pdf result text no longer leaks viewUUID when the interact
tool is not registered (model has no use for it).
- Annotations panel default width: 250px -> 500px (max-width:50vw
still caps on narrow viewports).
* refactor(pdf-server): extract src/commands.ts, dedupe PdfCommand type
PdfCommand was defined independently in server.ts and mcp-app.ts with
no compiler check to keep them in sync. Extract to src/commands.ts as
the single source of truth; both sides import the type.
Uses the TS interfaces from pdf-annotations.ts (not Zod-inferred types)
since those already describe the post-resolveImageAnnotation wire shape
(imageData present, x/y/width/height non-optional).
Also deletes ~150 lines of dead Zod schemas (PdfAnnotationDef and the
10 variant schemas that built it) -- they were never used as runtime
validators, only for z.infer<typeof> type inference. Input remains
z.record(z.any()) for model-API forgiveness.
* fix(pdf-server): correctness cleanup bundle
- findTextRectsFromCache: return [] instead of a hardcoded placeholder
rect when text-layer DOM isn't rendered. The placeholder was persisted
as real coordinates; caller already guards on empty.
- onteardown: bump loadGeneration to stop the preloader (reuses the
reload-abort check) and clear searchDebounceTimer.
- startPreloading: re-check loadGeneration inside the pause-wait loop
so teardown mid-pause exits instead of spinning.
- app.sendMessage calls: wrap in .catch(log.error) so host-unsupported
doesn't throw an unhandled rejection.
- Remove dead ternary (Highlight:Highlight), stale TODO and commented
downloadBtn line.
* docs(pdf-server): fix action names in example prompts, document all flags
- Example prompts referenced get_pages with getText/getScreenshots params;
actual model-facing actions are get_text (intervals) and get_screenshot
(single page). Fixed 3 prompts.
- Document --debug, --enable-interact, --writeable-uploads-root flags.
- Add image annotation type to the Annotation Types table.
- Note that interact is disabled in HTTP mode unless --enable-interact.
- Add Deployment section (stdio vs HTTP single-instance vs stateless).
* test(pdf-server): add interact tool unit tests
New describe("interact tool") block with 7 tests covering:
- enqueue -> poll_pdf_commands roundtrip
- missing-arg error paths for navigate, fill_form, add_annotations
- command queue isolation across distinct viewUUIDs
- fill_form passthrough when viewFieldNames not registered
- (skip) unknown-UUID poll (LONG_POLL_TIMEOUT_MS not exported, can't
bypass the 30s wait without changing server.ts)
Surprises found:
- interact never validates viewUUID exists; enqueues to any string
- batch-mode early-exits on first error, silently dropping later commands
* style: auto-fix prettier formatting
* refactor(pdf-server): extract annotation panel to src/annotation-panel.ts
Split ~930 lines from mcp-app.ts into two new modules:
- src/viewer-state.ts (50 lines): shared Maps/Sets/Arrays + types
(annotationMap, formFieldValues, selectedAnnotationIds, fieldNameTo*,
undoStack/redoStack, TrackedAnnotation, EditEntry). Only containers
with stable bindings — both modules mutate contents, never reassign.
- src/annotation-panel.ts (1084 lines): floating panel positioning,
accordion rendering, annotation/form-field cards, reset/clear-all.
Coupling to mcp-app flows through PanelDeps injected at init —
scalars like currentPage/pdfDocument reach the panel via a state()
getter so the ~100 use sites in mcp-app stay unwrapped.
mcp-app.ts: 5666 → 4721 lines. panelState.open replaces
annotationPanelOpen; panelState.openAccordionSection is written by
selectAnnotation. syncSidebarSelection moved to the panel module.
* style: auto-fix prettier formatting
* test(pdf-server): document computeDiff same-id modification behavior
Two new tests:
- User-added annotation modification: captured in diff.added (id not
in baseline, so latest content persists correctly).
- Baseline annotation in-place modification: KNOWN LIMITATION. Same-id
edit produces empty diff (id-set based), so the change vanishes on
reload. Viewer's addAnnotation() works around via remove+add, but
updateAnnotation() (interact tool's update_annotations) does not.
* chore(pdf-server): revert unrelated screenshot regenerations
Pre-commit hook runs build:all which regenerates all example
screenshots. These 24 PNGs have no semantic change; reverting
to keep the PR diff focused on pdf-server.
* fix(pdf-server): gate resolveImageAnnotation on validateUrl (arbitrary file read)
resolveImageAnnotation() did fs.readFile on whatever imageUrl the model
sent, bypassing the allowedLocalFiles/allowedLocalDirs machinery that
protects display_pdf and save_pdf. Model could request
{imageUrl:"/Users/x/.ssh/id_rsa"}, server would base64 the bytes into
the add_annotations command, iframe stores it, get_screenshot reads it
back.
Also: fetch() branch accepted plain http:// (SSRF to local network).
Fix: call validateUrl(imageUrl) before touching the filesystem or
network. Throws on rejection; caller converts to {isError:true} so the
model sees a clear error instead of silent skip.
3 tests: reject local path outside roots, reject http://, accept path
under allowed dir (with real readFile + imageData population).
* fix(pdf-server): deleted-baseline zombies, clear-all form strip, height-only coord shift
Three bugs from PR review, all in viewer state/coord handling:
1. Deleted baseline annotations zombied back on reload (mcp-app.ts:2512).
restoreAnnotations' apply loop was add-only. loadBaselineAnnotations
runs between the two restore calls and re-seeds annotationMap with
every baseline id, including the ones in diff.removed. The zombie
survives, and the next persistAnnotations sees it in currentIds ->
computeDiff produces removed=[] -> deletion permanently lost.
Fix: delete diff.removed ids after the merge loop.
2. Clear All didn't strip form values from the saved PDF (mcp-app.ts:2756).
clearAllItems() empties formFieldValues, but buildAnnotatedPdfBytes
gates on formFields.size > 0 and only writes entries present in the
map. Empty map -> zero setText/uncheck calls -> pdf-lib keeps the
original /V values. Fix: at getAnnotatedPdfBytes time, inject an
explicit clearing sentinel ("" or false by baseline type) for every
baseline field absent from formFieldValues.
3. update_annotations shifted rect/circle/image when patching height
without y (mcp-app.ts:4161). The old code spread existing.def
(internal coords, y = bottom edge) with update (model coords, y =
top-left), then key-filtered back to only update keys. But internal
y = pageHeight - modelY - height; patching height with top fixed
requires rewriting internal y, which the key-filter discarded.
Fix: convert existing to model coords, merge in model space, convert
back, pass the full merged def. Also uses the target page height
when update.page differs (fixes the low-severity page-move bug too).
* fix(pdf-server): sweep aux maps independently of commandQueues; line annotations sort
Two lower-severity findings from review:
Leak (server.ts:265): pruneStaleQueues iterated commandQueues to clean
up viewFieldNames/viewFieldInfo/viewFileWatches, but display_pdf
populates those maps without creating a commandQueues entry (only
enqueueCommand does), and dequeueCommands deletes the entry on every
poll. Net: the sweep found nothing; aux maps leaked every display_pdf.
viewFileWatches entries hold an fs.StatWatcher -> slow FD exhaustion
on HTTP --enable-interact. Fix: new viewLastActivity heartbeat map,
touched at display_pdf/enqueue/dequeue, swept on TTL. Dead second
loop (entry.commands.length===0 was unreachable) removed.
Line-annotation sort (annotation-panel.ts:400): getAnnotationY checked
'y' in def and 'rects' in def, but LineAnnotation has only x1/y1/x2/y2.
Fell through to return 0 -> all line annotations sorted to panel
bottom. Added y1 branch returning max(y1, y2).
* feat(pdf-server): rewrite display_pdf prompts to steer model toward interact
The model was calling display_pdf again to add annotations instead of
using interact with the existing viewUUID. Root cause: the description's
first two sentences invited exactly that --
'Use this to display, annotate, edit, and fill form fields'
'when the user wants to ... annotate, edit, sign, stamp'
-- then the CRITICAL warning arrived three paragraphs later, after the
model had already pattern-matched 'user wants to sign' -> display_pdf.
Three changes:
- First sentence is now narrow: 'Open a PDF. Call this ONCE per PDF.'
- Follow-up-actions-go-through-interact is sentence 2, not paragraph 3.
- Result text is directive at the decision point: tells the model to
call interact with the viewUUID, warns against re-calling display_pdf.
* fix(pdf-server): correct the 'discards state' claim in display_pdf prompts
Both viewers coexist; nothing is discarded. The actual failure mode is
the model now holds a new viewUUID and addresses the wrong viewer --
interact calls land on the fresh empty one, not the one with the user's
existing annotations. Rewording to say that.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||
|
|
83d82a1caa |
fix(examples): improve mobile support for map, pdf, and threejs examples (#555)
* Fix touch gesture conflicts in WebGL example apps on mobile * Format with prettier * fix(examples): improve mobile support for map, pdf, and threejs examples - map-server: fullscreen button respects safe area insets, hover/opacity pattern with always-visible on touch devices - pdf-server: fit-to-width scale for narrow viewports, responsive toolbar with compact controls on mobile, safe area in fullscreen toolbar via CSS custom properties - threejs-server: fullscreen button respects safe area and always visible on touch, ResizeObserver for responsive width, host containerDimensions for fullscreen height, no container padding in fullscreen, replace useHostStyles with inline style application to avoid overwriting onhostcontextchanged handler |
||
|
|
704b6e0af9 |
chore: bump ext-apps to 1.2.2 (#549)
Changes since 1.2.1: - fix(schemas): import from zod/v4 to match SDK's zod API (#548) - test(e2e): fix no-op security injection test via frames[] traversal (#540) |
||
|
|
e13732ad38 |
chore: bump ext-apps to 1.2.1
Changes since 1.2.0: - fix: bundle SDK+zod in react-with-deps (was byte-identical to ./react) (#539) - fix(build): copy schema.json to dist and externalize zod (#534) - fix: skip debug log for high-frequency tool-input-partial notifications (#546) - fix(deps): drop @hono/node-server override to patch GHSA-wc8c-qw6v-h7f6 (#535) - fix(readme): use picture element for theme-aware logo (#545) - fix(ci): require maintainer association for /update-snapshots trigger (#532) - fix: pre-commit stages only originally-staged files; add .npmrc (#538) - ci: use npm ci with caching, validate typedoc links, align Node versions (#533) - test: exclude screenshot-gen from default E2E run; wire pdf-server tests (#537) |
||
|
|
6b12fff1d0 |
chore: bump ext-apps to 1.2.0 (#527)
* chore: bump ext-apps to 1.1.3 Changes since 1.1.2: - fix: autoResize reporting viewport height when content is taller (#525) - fix: remove duplicate --color-text-ghost from McpUiStyleVariableKey (#523) - fix: examples servers' npm run start:stdio (#507) - pdf-server: ignore client roots by default (#510) - docs: add Security documentation section and SEO improvements (#524) - docs: add MCP client configuration examples to CONTRIBUTING.md (#508, #526) - docs: use apps.extensions.modelcontextprotocol.io domain for docs URLs (#499) - docs: add SECURITY.md with GitHub Security Advisories guidance (#472) * chore: switch to minor bump 1.2.0 (new APIs in #470) |
||
|
|
5d6fff2910 |
Update basic-host imports to use package exports (#400)
Change `sandbox.ts` imports from relative paths into `dist/` to use the proper `@modelcontextprotocol/ext-apps/app-bridge` package entry point. This aligns the example with how external consumers would import from the package. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
c000ba6dd5 |
Work around tsx watch hang on Windows in quickstart (#428)
On Windows, `tsx watch` can hang when running under `concurrently` due to stdin handling conflicts (see [privatenumber/tsx#623][]). Adding the `--raw` flag to `concurrently` avoids this by preventing stdin interception. Since `--raw` mode disables concurrently's automatic output prefixing, a custom Vite logger is added to prefix messages with `[vite]` so users can still distinguish which process produced each line of output. [privatenumber/tsx#623]: https://github.com/privatenumber/tsx/issues/623 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
19fe73a08a |
Replace deprecated RESOURCE_URI_META_KEY usage with modern format (#457) | ||
|
|
ef8fc6e2f0 |
fix: make listServerResources() params optional (#528)
Follow-up to #470. Matches MCP SDK Client.listResources(params?) signature so callers can omit the empty {} for the common case. Also reflows a line in app-bridge.test.ts that prettier flagged. |
||
|
|
4f9a7ebc74 |
feat: add readServerResource() and listServerResources() api to App (#470)
* feat: add readServerResource() and listServerResources() api to App class * test: add listServerResources and readServerResource integration tests |
||
|
|
134c71c29c |
Add MCP client configuration examples to CONTRIBUTING.md (#508)
* docs: restore full MCP client JSON configs to CONTRIBUTING.md
Reinstate the copy-pasteable JSON configurations for running all examples
with MCP clients (npx-based and local development), which were removed
from README.md in
|
||
|
|
30f79b9e3f |
ignore client roots by default in pdf-server (#510)
* security: ignore client roots by default in pdf-server When the pdf-server is started with --stdio, MCP clients may advertise roots that refer to directories on the *client's* file system. Because the server resolves those paths locally, accepting them by default would give the remote client access to arbitrary directories on the server's machine. This commit makes client roots opt-in via the --use-client-roots flag. Without the flag, the server logs a notice and skips roots setup entirely. The createServer() function now accepts a CreateServerOptions object with a `useClientRoots` boolean (defaults to false). https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * Enable client roots by default for HTTP, keep off for stdio HTTP mode serves a local client so roots are safe. Stdio mode may have a remote client whose roots would resolve against the server's filesystem, so roots stay off unless --use-client-roots is passed. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * fix: correct transport-aware defaults for useClientRoots stdio = local client (e.g. Claude Desktop) → auto-enable roots HTTP = remote client → ignore roots unless --use-client-roots passed The previous commit had the logic inverted. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * refactor: inline useClientRoots per transport branch Remove the intermediate effectiveUseClientRoots variable — just pass `true` in the stdio branch and the flag value in the HTTP branch. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * docs: update README to match transport-aware client roots defaults Stdio always enables roots (client is local); HTTP ignores them by default. The previous README text had these reversed. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
627ead3805 |
fix examples servers' npm run start:stdio (#507)
* fix examples servers' npm run start:stdio * rm mistakenly added file |
||
|
|
37533ff4c7 |
Merge pull request #499 from jonathanhefner/update-docs-url
Use `apps.extensions.modelcontextprotocol.io` domain for docs URLs |
||
|
|
5373f01d6f |
chore: bump ext-apps to 1.1.2 (#503)
* chore: bump ext-apps to 1.1.2 Changes since 1.1.1: - feat: add ui/download-file method for host-mediated file downloads (#475) - pdf-server: robust path validation + folder/file root support (#497) - ci: auto-fix prettier formatting in pre-commit hook and CI (#498) * fix: sync local SDK build into node_modules for examples npm workspaces hoists example dependencies to the root node_modules, but installs the published registry copy of @modelcontextprotocol/ext-apps instead of linking to the local source. This causes type-check failures when examples use features not yet published to npm. Add scripts/link-self.mjs which copies the freshly-built dist/ and package.json into the hoisted node_modules copy after each SDK build, ensuring examples always type-check against the latest local types. See: https://github.com/npm/feedback/discussions/774 * Revert "fix: sync local SDK build into node_modules for examples" This reverts commit e3387e8434f3def030f5a223a70632092c929119. |
||
|
|
e4a9766592 |
Merge pull request #475 from modelcontextprotocol/feat/download-file
feat: add ui/download-file method for host-mediated file downloads |
||
|
|
281a26b021 |
pdf-server: robust path validation + folder/file root support (#497)
* pdf-server: support folders in cli + files in roots * fix(pdf-server): use path.relative for robust ancestor dir matching - Replace string prefix matching (startsWith) with path.relative-based isAncestorDir() which correctly handles trailing slashes, path normalization, and prevents both .. traversal and prefix attacks. - Use path.resolve consistently when adding paths to allowedLocalFiles and allowedLocalDirs (main.ts was missing this). - Use resolved path for exact file match check in validateUrl. - Improve error message formatting for easier debugging. - Add tests for trailing slashes, grandparent dirs, and isAncestorDir. * chore(pdf-server): remove JSONL interaction logging Remove the withLogging transport wrapper, logInteraction helper, InteractionLogger type, and all log parameter threading that were added for debugging. * fix(pdf-server): support computer:// URL scheme for local files Some clients (e.g. Claude Code) use computer:// instead of file:// for local file references. Handle both schemes in isFileUrl and fileUrlToPath. * fix(pdf-server): add debug logging for rejected files, ignore .jsonl logs * fix(pdf-server): accept bare file paths in addition to file:// URLs Clients like Claude Desktop may pass raw absolute paths (e.g. /Users/.../file.pdf) instead of file:// URLs. Handle these in validateUrl and readPdfRange. * debug(pdf-server): add hex char diagnostics for path mismatch * fix(pdf-server): resolve symlinks to handle sandbox path remapping Use fs.realpathSync to resolve symlinks/bind mounts when comparing file paths against allowed directories. This fixes the path namespace mismatch where Claude Desktop sends container paths (e.g. /sessions/...) while MCP roots use host paths (e.g. /Users/...). Both the file path and directory paths are resolved through symlinks before comparison, so either side can be a symlink. Also stores realpath of roots in allowedLocalDirs/Files so matching works in both directions. Removes verbose hex diagnostics (no longer needed). * fix(pdf-server): decode percent-encoded bare paths (e.g. %20 for spaces) Clients may send bare file paths with percent-encoded characters (e.g., Application%20Support). Apply decodeURIComponent to bare paths before resolving, matching the behavior already used for file:// URLs. * fix(pdf-server): add full diagnostics to client-facing error message Include url, resolved path, realpath, allowedFiles, and per-directory path.relative results in the error returned to the client, so we can diagnose why isAncestorDir fails when paths look identical. * debug(pdf-server): add hex dump to diagnose invisible char differences path.relative returns 10 levels of .. despite paths looking identical, meaning they differ at the byte level. Add hex dump of first 30 chars of both paths to catch invisible Unicode or encoding differences. * fix(pdf-server): use inode comparison for bind-mount path matching The cowork VM remaps paths via bind mounts: the tool input uses VM-internal paths (/sessions/...) while MCP roots use host paths (/Users/...). Since path.relative can't match across bind mounts, compare directory inodes instead (dev+ino from fs.statSync). Walks up the file's parent directories checking each against allowed dirs by inode, which works regardless of path namespace differences. * fix(pdf-server): resolve VM-internal paths via directory basename matching When the MCP server runs on the host but receives unrewritten VM paths (e.g., /sessions/name/mnt/uploads/file.pdf), the tool call url argument isn't rewritten by the cowork VM proxy because it doesn't know which arguments are file paths. Fix: when path validation fails, try to match the file by finding the directory basename (e.g., 'uploads') in the VM path and looking for the relative path suffix under each allowed directory on the host. Also returns the resolved host path so readPdfRange reads the correct file on the host filesystem. * revert(pdf-server): remove resolveVmPath hack The VM path mismatch (VM-internal paths not rewritten in tool call arguments) should be fixed in the cowork VM proxy layer, not worked around in the MCP server. Removes: - resolveVmPath() function - resolvedPath field from validateUrl return type - effectiveUrl remapping in tool handlers - VM path resolution test * nit |
||
|
|
b4642b4338 |
Use apps.extensions.modelcontextprotocol.io domain for docs URLs
Update all documentation links from `modelcontextprotocol.github.io/ext-apps` to the new custom domain across README.md, docs/, and example READMEs. Add `cname` to the `peaceiris/actions-gh-pages` deploy action in `.github/workflows/docs.yml` so the CNAME file persists across deploys. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
ec389f473b |
chore: bump ext-apps to 1.1.1 (#496)
Bug fixes / improvements since 1.1.0: - fix: add audio/video support for basic-host (#460) - fix(pdf-server): remove page-loading overlay (#493) - pdf-server: remove domain allowlist, require HTTPS only (#487) - Set audioSession.type to playback in sheet music app (#489) - Add MCPB packaging and Claude Code plugin for pdf-server (#491) |
||
|
|
82b1f29085 |
Set audioSession.type to playback in sheet music app (#489)
Use the Audio Session API to declare audio as media playback so the platform handles it correctly: background audio, lock screen controls, Dynamic Island integration, and bypassing the iOS silent switch. |
||
|
|
3de035dc8d |
Add MCPB packaging and Claude Code plugin for pdf-server (#491)
* Add MCPB packaging and Claude Code plugin for pdf-server Two distribution paths for the pdf-server: 1. **MCPB bundle** (Claude Desktop MCP Directory): - manifest.json: v0.3 manifest with tools, compatibility, privacy - .mcpbignore: excludes source/tests from bundle - icon.png: 64x64 PDF document icon 2. **Claude Code plugin** (plugin/): - .mcp.json: declares local stdio server via npx - commands: /pdf-research:read, /pdf-research:summarize - skills: pdf-reading skill for document navigation - Tested end-to-end: MCP server connects, all tools available The plugin uses a local stdio MCP server pattern — the first known instance in a knowledge-work plugin (all existing plugins use remote HTTP connectors). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Run prettier:fix to satisfy CI formatting checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4d9ba1ea0e |
pdf-server: remove domain allowlist, require HTTPS only (#487)
Replace the hardcoded list of allowed remote origins with a simple HTTPS protocol check. Any HTTPS URL is now accepted for remote PDFs. Local file allowlisting remains unchanged. |
||
|
|
219b4b1605 |
fix(pdf-server): remove page-loading overlay (#493)
* fix(pdf-server): remove page-loading overlay The overlay had multiple issues: - Hardcoded white background broke dark mode - Could get stuck visible, breaking sizing/layout - Added little value since previous page stays visible during render and the 150ms delay meant most renders completed before it showed * prettier:fix |
||
|
|
c49b4b5e5b |
fix: add audio/video support for basic-host (#460) | ||
|
|
5bbf277882 |
chore: bump ext-apps to 1.1.0 (#485)
* chore: bump ext-apps to 1.1.0 Features: - registerAppResource now returns the registered resource (#370) - Accept UIResourceMeta in both resources/list and resources/read (#410) - callServerTool throws helpful error when called with a string (#449) - Add double-connect guard to prevent protocol message handling errors (#450) Bug fixes: - Only ignore messages that lack jsonrpc 2.0 in message-transport (#448) - Align basic-host dark mode styles (#438) Spec: - Change Host <> Sandbox communication protocol to SHOULD (#435) - Clarify UIResourceMeta in both list and read responses (#410) * fix(debug-server): add missing npm package config (main, bin, types, exports) Align debug-server package.json with all other server examples: - main: dist/server.js (was server.ts) - types: dist/server.d.ts - bin: mcp-server-debug -> dist/index.js - exports with types - files: only dist (remove source server.ts) |
||
|
|
d185099ead |
feat(pdf-server): lazy range-based loading + eager background preloading (#480)
* lazy PDF loading w/ PDFDataRangeTransport * lint * feat: lazy PDF loading via range transport + eager background preloading Replace full-file download with PDFDataRangeTransport so PDF.js fetches byte ranges on demand. First page renders immediately; remaining pages load in background for search. **Range transport (server + client)** - server.ts: add read_pdf_bytes tool returning base64 chunks with offset - mcp-app.ts: AppRangeTransport feeds PDF.js via fetchRange/fetchChunk with parallel sub-requests for large ranges, Promise-based dedup of concurrent requests, and a persistent range cache **Background preloader** - Sequential page 1→N text extraction, pauses during interactive navigation (preloadPaused flag set in goToPage, cleared in renderPage) - Debounced live search refresh (500ms) as pages load - Search UI shows '(loading…)' suffix until all pages processed **Loading indicators** - SVG pie in toolbar: fills as pages load, fades on completion, turns red on errors with tooltip listing failed pages - Debounced page-loading overlay (150ms) for uncached page navigation **Cleanup** - Remove old full-file chunked download and progress bar - Remove extractAllPageText (replaced by preloader) - Remove verbose debug logs (Coverage, RangeReq, cache hit) |
||
|
|
90ba41720e |
pdf: ignore safe area insets in fullscreen mode (#484)
Add padding: 0 !important to .main.fullscreen so that safe area insets applied via inline styles are overridden, making the PDF viewer truly fullscreen edge-to-edge. Also add .build directories to .prettierignore to fix pre-commit hook failures from Swift build artifacts. |
||
|
|
96c0af577d |
feat(pdf-server): use MCP roots to auto-allow local directories (#479)
When the client advertises roots capability, the server queries roots/list on initialization and subscribes to roots/list_changed notifications. Any file:// roots pointing to existing directories are added to an allowedLocalDirs set, enabling access to all PDFs under those directories without needing to enumerate them via CLI args. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> fix: revert unrelated changes from bad rebase Restores files that were accidentally modified alongside the MCP roots feature: README, pdf-server UI (search feature), threejs example, SDK guards (app.ts, app-bridge.ts), typedoc theme, media assets, and e2e snapshots. |
||
|
|
3757271641 |
refactor: use MCP resource types for ui/download-file params
Replace custom {filename, content, mimeType, encoding} params with
standard MCP resource types: EmbeddedResource for inline content and
ResourceLink for host-resolved references.
New params shape:
params: { contents: (EmbeddedResource | ResourceLink)[] }
This reuses existing MCP primitives instead of inventing new ones,
and enables hosts to show resource 'bubbles' or resolve ResourceLink
URIs via resources/read on the MCP server.
|
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01d826aea0 |
||
|
|
621a70a724 |
||
|
|
61fe39b4db |
||
|
|
3af019144f |
||
|
|
f3bf1065db |
||
|
|
4fc95131bf |
||
|
|
2e55c842ba |
||
|
|
721ccd04b8 |
||
|
|
9da4a75ff6 |
||
|
|
8e7206cb15 |
||
|
|
04fed524a9 |
||
|
|
68c58b60b7 |
||
|
|
800f6d08fd |
||
|
|
9d55050a00 |
||
|
|
4b3f6252a4 |
||
|
|
38700569e7 |
||
|
|
14969c5da2 |
||
|
|
8961b03163 |
||
|
|
e8ca859429 |
||
|
|
e8a4ea5f89 |
||
|
|
6723c8ff10 |
||
|
|
296b5921fb |
||
|
|
83d82a1caa |
||
|
|
704b6e0af9 |
||
|
|
e13732ad38 |
||
|
|
6b12fff1d0 |
||
|
|
5d6fff2910 |
||
|
|
c000ba6dd5 |
||
|
|
19fe73a08a |
||
|
|
ef8fc6e2f0 |
||
|
|
4f9a7ebc74 |
||
|
|
134c71c29c |
||
|
|
30f79b9e3f |
||
|
|
627ead3805 |
||
|
|
37533ff4c7 |
||
|
|
5373f01d6f |
||
|
|
e4a9766592 |
||
|
|
281a26b021 |
||
|
|
b4642b4338 |
||
|
|
ec389f473b |
||
|
|
82b1f29085 |
||
|
|
3de035dc8d |
||
|
|
4d9ba1ea0e |
||
|
|
219b4b1605 |
||
|
|
c49b4b5e5b |
||
|
|
5bbf277882 |
||
|
|
d185099ead |
||
|
|
90ba41720e |
||
|
|
96c0af577d |
||
|
|
3757271641 |