* perf(pdf-server): lazy form extraction via range transport + incremental viewer scans
Server: display_pdf now opens the document via PDFDataRangeTransport
(disableAutoFetch) and only runs the per-page form/annotation walk when
getFieldObjects() is non-empty, so form-free PDFs are probed with ~10-25%
of bytes instead of a full download. The unused viewFieldInfo Map is
removed.
Viewer: getDocument sets disableAutoFetch/disableStream; baseline
annotation scan and field-name mapping run lazily per rendered page
instead of walking every page after load, so first paint no longer
schedules a full-file pull.
E2E: new range-counting HTTPS fixture (W-9 for forms, generated
text+image PDF for no-forms) with stallAfterBytes control, and four
regression tests asserting form fields are returned, <30% served on
no-forms display_pdf, first page renders while later ranges are stalled,
and overlap stays bounded.
* fix(pdf-server): handle separated field/widget trees in extractFormSchema
pdfjs getFieldObjects() returns the full field-tree array. For PDFs with
a separated structure (pdf-lib, some authoring tools) the typed widget
sits at fields[1+] behind a typeless container at fields[0]; the previous
code only inspected fields[0] and skipped them all. Pick the first entry
with a non-empty type instead.
Makes the e2e forms.pdf fixture fully generated (no checked-in third-party
asset on the hot path); fw9.pdf stays as a unit-test fixture for the
hierarchical/XFA case.
* fix(pdf-server): address range-transport hangs and lazy-scan tombstone loss
PdfCacheRangeTransport:
- abort() is a no-op stub on PDFDataRangeTransport (it's the hook pdfjs
calls *on* the transport, not an upstream error channel). Expose a
.failed promise that rejects on the first fetch error and race every
pdfjs await against it in display_pdf, so transient network errors
surface into the existing catch instead of hanging the tool call.
- pdfjs coalesces adjacent missing chunks into one unbounded
requestDataRange; readPdfRange clamps each call to MAX_CHUNK_BYTES.
Loop and deliver in slices so every requested chunk is marked loaded.
Viewer (mcp-app.ts):
- The lazy per-page baseline scan left pdfBaselineAnnotations incomplete,
so persistAnnotations and getAnnotatedPdfBytes silently dropped
restoredRemovedIds tombstones for unvisited pages. Union those ids into
the computed diff and removedRefs.
Test fixture: release stalled handlers before resetStats/close so a
failing stalled test doesn't hang afterAll; fail fast if started with
NODE_ENV=production. NODE_TLS_REJECT_UNAUTHORIZED scope documented (full
per-process scoping needs a validateUrl localhost allow, tracked
separately).
* test(pdf-server): bridge coverage gaps and switch fixture to gated loopback HTTP
PdfCacheRangeTransport.deliver(): pdf.js's reader is keyed by the
original begin and removed after one delivery, so accumulate slices and
call onDataRange once with the full buffer (the previous multi-call
approach threw inside pdfjs). Covered by a new integration test that
drives getDocument()/getPage(1) on a >1MB PDF through a clamping
in-memory readPdfRange.
validateUrl: allow http://127.0.0.1|localhost|[::1] only when
PDF_SERVER_ALLOW_LOOPBACK_HTTP is set, so a remote deploy can't be made
to probe its own ports. Covered by env-on/off unit tests.
Fixture switched to plain HTTP (no openssl, no
NODE_TLS_REJECT_UNAUTHORIZED). Adds /error.pdf (500s after 50KB) and two
e2e tests: page 2 renders after stall release (>512KB object path), and
display_pdf returns gracefully on mid-load 500. Existing <30% test now
samples stats before the iframe loads.
New unit tests: display_pdf returns (not hangs) on mid-load fetch
failure via in-memory MCP client; computeDiff/serializeDiff contract
tests pinning the restoredRemovedIds tombstone-preservation behaviour.
* test(pdf-server): e2e for restoredRemovedIds tombstone preservation through viewer
Adds /with-native-annot.pdf fixture (2 pages, native /Text annot on
page 2) and a Playwright test that drives delete on page 2 → iframe
reload → interact add_annotations on page 1 (persist with page 2
unscanned) → assert localStorage diff still contains the removed id →
page 2 still shows it gone. Covers the mcp-app.ts glue the unit tests
can't reach.
* refactor(pdf-server): simplify per round-2 review and fix false-pass test
server.ts: extract probeFormFields() so display_pdf calls one helper
instead of inlining 40 lines of transport/orFail plumbing; make
extractFormSchema's fieldObjects param required (the optional branch
only existed for a test helper).
server.test.ts: the >1MB integration test now forces the image XObject
fetch via getOperatorList() and asserts max requestDataRange span >
MAX_CHUNK_BYTES, so it can't go vacuous. Dedupe makeRandomJpeg by
importing from the fixture.
mcp-app.ts: restore per-annotation try/catch isolation in the lazy
baseline scan (a throw was skipping AnnotationLayer.render); only
carry forward restoredRemovedIds while the baseline scan is incomplete
so a stale id can't pin dirty=true once every page is scanned.
tests: bump fixture JPEG to 1.1MB; consolidate 7 e2e tests to 4 (merge
overlap into the byte-budget test, merge stall+page-2, drop the
error-e2e duplicate of the unit integration test); delete
run-fixture.mjs; drop /error.pdf and ERROR_AFTER_BYTES; rename
tombstone test's describe and clarify page-2 covers the viewer
transport.
* fix(pdf-server): preserve partial probeFormFields results when extractFormFieldInfo throws
* test(pdf-server): fix tombstone e2e — deleted native annotations become a cleared card, not removed from DOM
* test(pdf-server): mark tombstone e2e as fixme — needs basic-host iframe-reload replay support
* test(pdf-server): link tombstone fixme to tracking issue #642
* 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.
Bug 1: generate-grid-screenshots.spec.ts was running on every CI E2E
invocation because playwright.config.ts had no testIgnore. This spec
writes examples/*/grid-cell.png as a side effect — it's meant to be
invoked only via npm run generate:screenshots.
Added testIgnore gated on GENERATE_SCREENSHOTS env var (Playwright's
testIgnore is stronger than explicit CLI file args, so a plain ignore
would break generate:screenshots). Passed the env var through the
docker invocation in generate:screenshots.
Before: npx playwright test --list → 82 tests in 3 files
After: npx playwright test --list → 65 tests in 2 files
GENERATE_SCREENSHOTS=1 ... --list → 17 tests in 1 file (still works)
Bug 2: examples/pdf-server/server.test.ts (448 LOC, 34 tests) was
never run — root script was 'bun test src'. Changed to 'bun test src
examples'. Only one .test. file exists in examples/ so no noise.
Before: npm test → 87 pass across 3 files
After: npm test → 121 pass across 4 files
* fix(e2e): add masks for dynamic content and increase timeouts for slow servers
- Add HOST_MASKS for Tool Input/Result panels with dynamic timestamps
- Add say-server to SLOW_SERVERS (10s) for TTS model download
- Add say-server to EXTRA_WAIT_MS (30s) in generate-grid-screenshots
* feat: add update-lock:docker script to regenerate lockfile with public registry
* pin package versions for dev compatibility
* feat(basic-host): add ?theme=hide query param to hide theme toggle in screenshots
- Add hideThemeToggle param to getQueryParams()
- Conditionally render ThemeToggle based on ?theme=hide
- Update e2e tests to use ?theme=hide for consistent screenshots
* update screenshots
* update screenshots
* fix(husky): load Node.js environment for GUI apps like GitHub Desktop
* fix(e2e): increase map-server wait time to 15s for tile loading
* fix(e2e): increase waitForAppLoad timeout to 30s for nested iframe loading
* ci: add workflow to update e2e snapshots
* ci: trigger snapshot update on push to this branch
* chore: update e2e snapshots [skip ci]
* ci: trigger CI after snapshot update
* style: fix prettier formatting in update-snapshots workflow
* fix: pin seroval, seroval-plugins, and solid-js for artifactory compatibility
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
- Remove platform-specific browser channel (was using Chrome on macOS,
Chromium in CI) - now uses default Chromium everywhere
- Add Docker-based test scripts for CI-identical local testing:
- `npm run test:e2e:docker` - run tests in Docker
- `npm run test:e2e:docker:update` - regenerate golden screenshots
- Regenerate golden screenshots using Docker/Linux environment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Playwright E2E tests with screenshot golden testing
- Add E2E tests for all 8 MCP server examples
- Screenshot golden images for visual regression testing
- CI workflow for running E2E tests
- npm scripts: test:e2e, test:e2e:update, test:e2e:ui
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add default demo code for Three.js example
When no code is provided, show a rotating green cube demo.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add E2E testing documentation to CONTRIBUTING.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update Three.js golden screenshot with 3D cube
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add explicit permissions to CI workflow
Set minimal read-only permissions for security best practices.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix test.setTimeout() to be inside describe blocks
Playwright requires setTimeout to be called within a test or describe block.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Simplify E2E tests - remove excessive timeouts
- Remove explicit setTimeout calls (default 30s is sufficient)
- Replace waitForTimeout(5000/6000) with proper waitForAppLoad()
- Wait for inner iframe visibility instead of fixed delays
- Keep only 500ms stabilization for screenshot animations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Auto-generate missing snapshots in CI
Use updateSnapshots: 'missing' in CI to handle cross-platform
screenshot differences (macOS vs Linux).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use platform-agnostic golden screenshots
- Remove -chromium-darwin suffix from snapshot filenames
- Configure snapshotPathTemplate for cross-platform compatibility
- Increase tolerance to 5% for rendering differences between platforms
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor tests to use forEach instead of for-of loop
May help with Playwright test discovery issues in CI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use npm ci and list reporter in CI
- Use npm ci for exact package versions
- Use --reporter=list instead of html to avoid potential re-evaluation issues
- Only upload test-results on failure
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Exclude e2e tests from bun test
Run bun test only on src/ directory to avoid running Playwright tests
with Bun's test runner.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix prettier formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add interaction tests for basic server apps
Test that clicking buttons in the MCP App triggers the corresponding
host callbacks:
- Send Message → host logs "Message from MCP App"
- Send Log → host logs "Log message from MCP App"
- Open Link → host logs "Open link request"
Tests both React and Vanilla JS implementations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Mask dynamic content in E2E screenshot tests
Add Playwright mask option to handle servers with dynamic/random content:
- basic-react/vanillajs: mask server time display
- system-monitor: mask CPU chart, memory stats, uptime
- cohort-heatmap: mask heatmap grid (random data)
- customer-segmentation: mask scatter chart (random data)
This addresses PR feedback about handling examples with non-deterministic
output. Masking replaces the previous 10% tolerance with proper exclusion
of dynamic elements, allowing tighter 1% tolerance for the rest of the UI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add wiki-explorer to E2E tests with default URL param
- Add default URL "https://en.wikipedia.org/wiki/Model_Context_Protocol" to
wiki-explorer-server's get-first-degree-links tool inputSchema
- Update basic-host to automatically populate input field with tool defaults
extracted from inputSchema.properties
- Add wiki-explorer-server to E2E test suite with dynamic masking for the
force-directed graph
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use .default() for threejs tool schema instead of .optional()
This exposes the default code and height values in the JSON Schema,
allowing basic-host to auto-populate the input field with defaults.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Enable parallel E2E tests with timeouts and canvas masking
- Enable fullyParallel with 4 workers locally (2 in CI)
- Add 30s timeout per test
- Mask threejs canvas for stable screenshots
- Update snapshots to reflect default values in input fields
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add pre-commit check for private registry URLs in package-lock.json
Same check as in CI to catch issues before push.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Increase screenshot diff tolerance to 6% for cross-platform rendering
Font rendering differs between macOS and Linux, causing ~5% pixel differences.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Revert pre-commit artifactory check (moved to separate PR #133)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Format threejs server.ts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update CONTRIBUTING.md to reflect platform-agnostic screenshots
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(e2e): use factory pattern for MCP servers to support parallel connections
McpServer only supports one transport at a time. When multiple browser
contexts connected in parallel, calling server.connect(transport) for
each session overwrote the previous transport's callbacks, causing
connection corruption and test timeouts.
Changes:
- server-utils.ts: accept factory function instead of single instance
- All 9 example servers: wrap server creation in createServer() function
- playwright.config.ts: re-enable parallel execution (4 workers)
- servers.spec.ts: add toBeEnabled wait for server connection
- Update screenshot baselines for basic-vanillajs and cohort-heatmap
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>