feat: Dynamic View Content SDK support (types, helpers, example)#700
Draft
liady wants to merge 4 commits into
Draft
feat: Dynamic View Content SDK support (types, helpers, example)#700liady wants to merge 4 commits into
liady wants to merge 4 commits into
Conversation
- McpUiResourceMeta.contentMimeTypes: renderer declaration for typed dynamic content payloads - McpUiContentBlockMeta: the _meta.ui.content marker for embedded resource blocks in tool results - McpUiClientCapabilities.contentMimeTypes: capability negotiation setting (supports ["*"] opaque forwarding) - Regenerated Zod schemas and re-exports Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New src/ui-content.ts module, re-exported from the app, app-bridge, and server entry points: - createViewContentBlock(): server-side helper producing an embedded resource block marked with _meta.ui.content (validates text/blob exclusivity and rejects ui:// payload URIs) - getViewContentBlocks() / isViewContentBlock(): view-side extraction of marked payloads from tool results, with mimeType/rendererUri filters - supportsContentMimeType(): capability check honoring the ["*"] wildcard, for servers gating renderer-pattern tools and hosts type-filtering payloads Includes type-checked JSDoc examples (ui-content.examples.ts) and unit tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A full worked example of Dynamic View Content: - Generic renderer view (no domain-specific presentation logic) that declares contentMimeTypes, extracts marked payloads from tool results with getViewContentBlocks(), and renders them via createElement/ textContent only (payloads are untrusted input) - search-flights tool returning a text fallback plus a declarative UI payload built with createViewContentBlock() - select-flight app-visibility tool driven by the renderer's event bridge, whose responses carry new payloads (the interactive loop) - E2E coverage with a golden screenshot generated in the CI-identical Playwright container Also fixes basic-host sandbox.html serving under dot-directory checkouts (e.g. git worktrees in .claude/worktrees): res.sendFile with an absolute path applies the dotfiles=ignore rule to every path segment and 404s; pass the file relative to a root instead. package-lock.json: new workspace entry, plus npm dedup of nested @types/node/undici-types entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous lockfile update was generated with a newer npm that deduped the nested examples/*/node_modules/@types/node and undici-types entries, which npm 10 (Node 22, used in CI) still resolves as required - so npm ci failed with 'lock file out of sync'. Regenerated from main's lockfile via npm install --package-lock-only in a linux/amd64 node:22 container; the diff vs main is now purely additive (the dynamic-content-server workspace entries only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
📖 Docs Preview Deployed
Includes drafts and future-dated posts. All pages served with |
@modelcontextprotocol/ext-apps
@modelcontextprotocol/server-basic-preact
@modelcontextprotocol/server-basic-react
@modelcontextprotocol/server-basic-solid
@modelcontextprotocol/server-basic-svelte
@modelcontextprotocol/server-basic-vanillajs
@modelcontextprotocol/server-basic-vue
@modelcontextprotocol/server-budget-allocator
@modelcontextprotocol/server-cohort-heatmap
@modelcontextprotocol/server-customer-segmentation
@modelcontextprotocol/server-debug
@modelcontextprotocol/server-lazy-auth
@modelcontextprotocol/server-map
@modelcontextprotocol/server-pdf
@modelcontextprotocol/server-scenario-modeler
@modelcontextprotocol/server-shadertoy
@modelcontextprotocol/server-sheet-music
@modelcontextprotocol/server-system-monitor
@modelcontextprotocol/server-threejs
@modelcontextprotocol/server-transcript
@modelcontextprotocol/server-video-resource
@modelcontextprotocol/server-wiki-explorer
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Stacked on #699 (the spec change) - this PR's base is
liady/dynamic-view-content-spec, so the diff here shows only the SDK implementation. Retarget tomainonce #699 merges.SDK implementation of Dynamic View Content: typed presentation payloads carried in tool results as embedded resource content blocks marked with
_meta.ui.content, forwarded by hosts to the tool's predeclared renderer view.Three commits, layered:
1. Types + schemas (
src/spec.types.ts, regeneratedsrc/generated/)McpUiResourceMeta.contentMimeTypes?: string[]- the renderer declarationMcpUiContentBlockMeta- the_meta.ui.contentmarker interface (optionalrendererUrifor future multi-view results)McpUiClientCapabilities.contentMimeTypes?: string[]- the extension setting (supports the["*"]opaque-forwarding wildcard)src/types.tsNotably,
AppandAppBridgeneed no behavioral changes: both already transportCallToolResultopaquely, so the payload channel exists - the spec constrains hosts from filtering it and keeps payloads out of model context.2. Helpers (
src/ui-content.ts, re-exported from the app, app-bridge, and server entry points)createViewContentBlock()- server-side: builds a correctly-marked embedded resource block; validatestext/blobexclusivity and rejectsui://payload URIs (reserved for renderable UI resources)getViewContentBlocks()/isViewContentBlock()- view-side: extracts marked payloads from tool results in array order, withmimeType/rendererUrifilters; works on bothontoolresultparams andcallServerTool()resultssupportsContentMimeType()- capability check honoring the["*"]wildcard, for servers gating renderer-pattern tools and hosts type-filtering payloadsWith unit tests and type-checked JSDoc examples (
ui-content.examples.tscompanions, synced viasync:snippets).3. Worked example (
examples/dynamic-content-server)A generic renderer view with no domain-specific presentation logic, driven end-to-end by declarative payloads:
contentMimeTypesin its_meta.uisearch-flights(model-visible) returns a text fallback for model context plus a marked payload built withcreateViewContentBlock()select-flight(visibility: ["app"], hidden from the model) is called by the renderer's event bridge - payload-level button actions becometools/callrequests, and responses carry the next payload, closing the interactive loopcreateElement/textContentonly, neverinnerHTMLof payload-derived stringsThe example uses a deliberately tiny stand-in format (
application/vnd.example.dynamic-ui+json, defined indynamic-ui.ts) so the plumbing stays easy to follow - the pattern is identical for real generative UI formats likeapplication/a2ui+json.Testing
tests/e2e/servers.spec.ts, exercising the full pipeline (server → marked embedded resource → host forwards viasendToolResult→ renderer extracts and renders). Golden screenshot generated and verified in the CI-identical Linux Playwright container.typedoc --treatValidationWarningsAsErrorscleanAlso includes a small
basic-hostfix discovered while testing:res.sendFilewith an absolute path applies express'sdotfiles: "ignore"rule to every path segment, 404ingsandbox.htmlin any checkout under a dot-directory (e.g. git worktrees under.claude/worktrees); passing the file relative to arootfixes it.package-lock.jsonpicks up the new workspace entry plus an npm dedup of nested@types/node/undici-typesentries.Follow-ups (out of scope here)
basic-hostadvertisingcontentMimeTypesin its client capabilities, to model the negotiation as a reference host (forwarding already works unmodified today)grid-cell.png+ table placement)userAction↔ tool-call mapping conventions) layered on this generic mechanism🤖 Generated with Claude Code