Skip to content

feat(mcp): render assemble_document inline as an MCP App - #111

Open
ahnafnafee wants to merge 2 commits into
CopilotKit:mainfrom
ahnafnafee:feat/mcp-apps-render
Open

ahnafnafee wants to merge 2 commits into
CopilotKit:mainfrom
ahnafnafee:feat/mcp-apps-render

Conversation

@ahnafnafee

Copy link
Copy Markdown

Summary

assemble_document returns the assembled page as plain text, so MCP Apps hosts such as Claude Desktop show raw HTML instead of rendering it. This PR makes the tool an MCP App: hosts that support the io.modelcontextprotocol/ui extension now render the fragment inline in the conversation. It also fixes skill resources and prompts failing on Windows.

Changes

fix(mcp): load skill documents on Windows

  • loadSkill checked resolved.startsWith(dir + "/"). path.resolve returns backslash paths on Windows, so every skill name failed the traversal guard and all skills:// reads and prompts threw Invalid skill name. It now compares against path.sep.

feat(mcp): render assemble_document inline as an MCP App

  • Registers a ui://open-generative-ui/document.html view (text/html;profile=mcp-app) and links the tool to it via _meta.ui.resourceUri, plus the legacy ui/resourceUri key.
  • The view applies the design-system CSS and the host theme (applyDocumentTheme, applyHostStyleVariables, applyHostFonts). It runs the fragment's scripts in document order, awaiting external classic scripts so later inline code can use their globals. It maps sendPrompt(text) to ui/message and openLink(url) to ui/open-link. The resource declares the existing CDN allowlist as its CSP and sets prefersBorder: false.
  • Clients that advertise the UI extension get a one-line result instead of the roughly 12 KB document, which keeps the boilerplate out of the model's context. Clients without it still receive the full document, so the README contract is unchanged for them.
  • Bumps @modelcontextprotocol/sdk to ^1.32.1: older 1.x versions strip extensions from client capabilities, so the server could not detect MCP Apps hosts.
  • One CDN allowlist is now shared by the iframe CSP and the MCP Apps CSP.

Testing

  • pnpm --filter open-intelligent-ui-mcp test: 10 tests pass. New tests cover the tool's UI metadata; the view resource's MIME type, design system, bridge, CSP, prefersBorder and .sr-only rule; the capability-dependent result in both directions; and skill loading and traversal rejection.
  • pnpm --filter open-intelligent-ui-mcp lint passes, and the @repo/design-system tests still pass.
  • Spawned the built stdio server and ran tools/list, resources/read, tools/call, a skill read and a prompt with an MCP Apps-capable client.
  • Rendered the view through the ext-apps AppBridge in a browser harness. The handshake completes; inline, CDN and module scripts run in order; host theme and style variables apply; sendPrompt arrives as ui/message and link clicks as ui/open-link.
  • Installed the server in Claude Desktop and confirmed the widget renders inline, with the tool returning the one-line result.

Notes

  • The view loads the pinned @modelcontextprotocol/ext-apps@2.0.3 app-with-deps bundle from jsDelivr when it opens, so rendering needs network access, as the CDN-based widgets already do. Inlining the bundle into the view would remove that dependency if offline hosts matter.
  • CONTRIBUTING asks for an issue before larger changes. I went straight to a PR because the fix was small and verifiable, but I'm happy to open an issue or adjust the scope.

loadSkill guards against path traversal by checking that the resolved
path starts with the skills directory followed by "/". On Windows,
path.resolve returns backslash-separated paths, so every skill name
failed the check and all skill resources and prompts threw "Invalid
skill name". Compare against path.sep instead, and cover loading and
traversal rejection in tests.
assemble_document returned the assembled page as plain text, so MCP Apps
hosts such as Claude Desktop showed raw HTML instead of rendering it.

- Register a ui://open-generative-ui/document.html view
  (text/html;profile=mcp-app) and link the tool to it through
  _meta.ui.resourceUri, plus the legacy ui/resourceUri key.
- The view applies the design system and the host theme, runs the
  fragment's scripts in order, and maps sendPrompt and openLink to the
  MCP Apps bridge. It loads the pinned ext-apps app-with-deps bundle from
  jsDelivr, which is already on the CDN allowlist, and asks hosts for no
  border.
- Clients that advertise the io.modelcontextprotocol/ui extension get a
  one-line result instead of the roughly 12 KB document, which keeps it
  out of the model's context. Other clients still receive the full
  document.
- Bump @modelcontextprotocol/sdk to ^1.32.1, whose ClientCapabilities
  schema keeps the extensions field.
- Share one CDN allowlist between the iframe CSP and the MCP Apps CSP.

This branch has not been deployed

No deployments
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.

1 participant