Skip to content

pdf-server: validate annotations with per-type schemas (fixes stamps rendering as "undefined") - #832

Merged
ochafik merged 1 commit into
mainfrom
pdf-annotation-schema
Oct 27, 2026
Merged

ochafik merged 1 commit into
mainfrom
pdf-annotation-schema

Conversation

@ochafik

@ochafik ochafik commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Stamps often rendered as the literal text "undefined". Root cause: the annotations arrays were typed z.record(z.string(), z.any()), so a stamp sent with content (note/freetext's field) instead of label sailed straight through — and every render path then coerced the missing label (el.textContent = undefined and ctx.fillText(undefined, …) draw "undefined", and the save path baked it into the PDF bytes via PDFHexString.fromText).

Rather than normalizing aliases, this tightens the contract:

  • New src/annotation-schemas.ts, shared by both tool surfaces: a strict discriminated union per annotation type for add_annotations (stamp requires label, note/freetext require content, highlight/underline/strikethrough require rects, image requires imageUrl or imageData; unknown or misspelled keys are errors, not silent strips) and a partial union for update_annotations (id+type required, text fields clearable to "", imported annotations accept position patches).
  • The view's app tools enforce the schemas through their input schemas (so the model sees the per-type requirements in the tool's JSON schema), and the server-mediated interact validates per def, naming the failing index, type and field — e.g. add_annotations: annotations[0] (stamp): label: Invalid input: expected string, received undefined.
  • Compile-time drift guards: each schema's output is statically asserted against its pdf-annotations.ts interface, so a field added there without updating the schema becomes a compile error instead of a silently dropped key.
  • Render/save fallbacks (label ?? "STAMP", content ?? "") so annotations persisted to localStorage before this validation no longer draw "undefined" — and no longer throw in the appearance-stream builder, which called def.label.replace(...) unguarded.
  • Tool descriptions fix the rects shorthand ({x,y,w,h} → {x,y,width,height}) that the schemas now enforce, and mark the stamp's label as required.

Test plan

  • Unit (bun test examples/pdf-server, 201 pass): new cases for the server-side validation — stamp without label rejected naming the field, valid stamp queued, unknown type rejected, update partials accepted while still requiring id.
  • e2e: new case in pdf-app-interact.spec.ts driving the view's interact with the classic content-instead-of-label stamp and asserting the error names label; the full annotation CRUD suite (pdf-annotations.spec.ts) passes under the strict schemas. 12/13 pdf e2e locally — the one failure (display_pdf result includes viewUUID and interactEnabled meta) reproduces identically on clean main.
  • tsc --noEmit on both configs, prettier clean.

Generated by Claude Code

Stamps often rendered as the literal text "undefined": the annotation
arrays were typed z.record(z.string(), z.any()), so a stamp sent with
content (note/freetext's field) instead of label sailed through, and
every render path coerced the missing label — textContent and fillText
drew "undefined" and the save path baked it into the PDF bytes.

Replace the loose shape with strict per-type zod schemas shared by both
tool surfaces (new src/annotation-schemas.ts): a discriminated union for
add_annotations (stamp requires label, note/freetext require content,
image requires imageUrl or imageData, unknown keys are errors rather
than silent strips) and a partial union for update_annotations (id and
type required, text fields clearable to "", imported annotations accept
position patches). The app tools enforce them through their input
schemas; the server-mediated interact validates per def and names the
failing index, type and field. Compile-time guards assert each schema's
output against its pdf-annotations.ts interface so the two cannot
drift silently.

The render and save paths also get explicit fallbacks (label ?? STAMP,
content ?? "") so annotations persisted before this validation no
longer draw "undefined" or throw while saving. Tool descriptions fix
the rects shorthand ({x,y,w,h} -> {x,y,width,height}) that the schemas
now enforce.

Co-authored-by: ochafik <ochafik@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Oct 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@832

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@832

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@832

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@832

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@832

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@832

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@832

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@832

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@832

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@832

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@832

@modelcontextprotocol/server-lazy-auth

npm i https://pkg.pr.new/@modelcontextprotocol/server-lazy-auth@832

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@832

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@832

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@832

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@832

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@832

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@832

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@832

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@832

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@832

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@832

commit: 22a32c9

@ochafik
ochafik merged commit af037be into main Oct 27, 2026
21 checks passed
ochafik added a commit that referenced this pull request Oct 27, 2026
Minor release: the SDK gains WebMCP interoperability — tools registered
with App.registerTool are mirrored into document.modelContext and tools
the page registers there are served over the Apps tools/list channel,
behind a new webMcpBridge option (default on), with
installWebMcpPolyfill for views that need a drop-in model context
(#829). The pdf-server example validates annotations with strict
per-type schemas, fixing stamps rendering as the literal text
"undefined" (#832). Workspace dependency ranges move to ^2.1.0 per the
bump script.

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants