Skip to content

[draft / experiment] MCP Apps prototype: hyperdx_query structured content + iframe widget#2195

Draft
alex-fedotyev wants to merge 6 commits intomainfrom
alex/mcp-app-experiment
Draft

[draft / experiment] MCP Apps prototype: hyperdx_query structured content + iframe widget#2195
alex-fedotyev wants to merge 6 commits intomainfrom
alex/mcp-app-experiment

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

Summary

Phase 0 of an MCP Apps experiment. The hyperdx_query MCP tool returns structuredContent plus a _meta reference to a sandboxed iframe widget, so MCP-Apps-capable hosts (Claude Desktop, Cursor) render observability results inline instead of dumping JSON into the chat.

This is a prototype, not for merge. Pushing it as a Draft so it's easy to review commit-by-commit. End-to-end browser verification was deferred; see agent_docs/mcp-apps.md for the full state.

What's here

Six logical commits, in order:

  1. chore(deps): wire MCP Apps SDK and the new mcp-widget workspace into root scripts.
  2. feat(chart-presenters): new package. Pure React presenters with no Mantine, no Jotai, no Next router. Shared by the dashboard and the MCP widget so both render identically.
  3. feat(mcp-widget): new package. Vite-built single-file HTML bundle that runs inside an MCP Apps sandboxed iframe, completes the App handshake using the official @modelcontextprotocol/ext-apps SDK, and renders tool results.
  4. feat(api/mcp): server side of the protocol. mcpServer.ts registers the widget resource with mimeType: text/html;profile=mcp-app. tools/query/index.ts advertises it on the tool via _meta["ui/resourceUri"] (the canonical slash-key form Claude Desktop reads) and _meta.ui.resourceUri (nested form for backwards compat).
  5. feat(api/external): expose granularity on external-API time-chart configs so the conversion path used by MCP doesn't drop the time-bucket size and aggregate every series to a single point.
  6. test(api/mcp) + docs: jsdom-backed widget tests, queryTool structured-content coverage, and agent_docs/mcp-apps.md with the full design rationale.

How to investigate

  • The design rationale is in agent_docs/mcp-apps.md. Start there.
  • The protocol contract (what structuredContent looks like) is documented at the top of packages/mcp-widget/src/mcp-app.tsx.
  • The MIME type and _meta key conventions, including why both forms ship, are documented in packages/api/src/mcp/ui/widget.ts.

Status

Working:

  • Build pipeline (api builds widget bundle first, then itself).
  • Tool-result handshake via the official ext-apps SDK.
  • line / stacked_bar / table / search / number / pie display types.

Deferred:

  • End-to-end browser verification through Claude Desktop. Hit a Docker BuildKit issue with the otel-collector image locally.
  • Production wiring (URL config, CSP, link-out behavior).
  • Search detail panel (currently renders rows like the table view).

Branch hygiene

Branch is 45 commits behind origin/main because the prototype was started off an older commit. Merge base is on main, so the diff itself is clean. We can rebase before any real merge consideration.

- Preapprove @modelcontextprotocol/ext-apps and @modelcontextprotocol/sdk
  in .yarnrc.yml so the 7-day npmMinimalAgeGate doesn't block recently
  published versions of the official SDK packages.
- Add @hyperdx/mcp-widget to root dev/build/concurrently scripts.
- Add jsdom + @types/jsdom to packages/api dev deps for widget tests
  that load the bundled HTML and assert on the parsed DOM.
- packages/api build now invokes the mcp-widget build first so the API
  ships with a current widget bundle.
…widget

Adds @hyperdx/chart-presenters: tree of pure React presenter components
with no data-fetching, no Mantine, no Jotai, no Next router. Built with
tsup and consumed both by packages/app (DBTimeChart wraps the time-series
presenter with its data hook) and packages/mcp-widget (sandboxed iframe
renders the same component for MCP Apps results).

Constraints:
  * No data fetching; callers wire their own.
  * No Mantine / Jotai / Next router (would balloon the widget bundle
    and break inside MCP Apps sandboxed iframes).
  * Recharts and React are peer dependencies.

Currently exports TimeSeriesView (line + stacked-bar). Table, number,
and pie remain inline in the widget for now; they're simple enough that
extraction doesn't pay back yet.
Adds @hyperdx/mcp-widget: a Vite-built single-file HTML bundle that
renders observability query results inline inside MCP-Apps-capable
hosts (Claude Desktop, Cursor, etc.).

How it works:
  1. Host fetches the widget resource (URI is advertised on the
     hyperdx_query tool via _meta.ui.resourceUri).
  2. Host loads the HTML in a sandboxed iframe.
  3. Widget completes the App handshake using the official ext-apps SDK
     and waits for the host to push the tool result.
  4. On tool result, widget extracts structuredContent and renders one
     of: line, stacked_bar (via @hyperdx/chart-presenters), table,
     search, number, pie.

The bundle is built with vite-plugin-singlefile and inlines all assets
into a single HTML file. No data-fetching of its own; the host pushes
the tool result.
Wires hyperdx_query into the MCP Apps profile so capable hosts render
results inline instead of dumping JSON.

Server side:
  * mcpServer.ts registers a single resource (HYPERDX_WIDGET_URI) that
    serves the bundled mcp-widget HTML with mimeType
    "text/html;profile=mcp-app". The strict mimeType is what hosts
    look for to distinguish App resources from arbitrary HTML.
  * tools/query/index.ts: tool registration sets _meta["ui/resourceUri"]
    (slash-key form, the canonical key Claude Desktop reads) and
    _meta.ui.resourceUri (nested form for backwards compat) so any
    MCP Apps host can find the widget.
  * tools/query/helpers.ts: builds the structuredContent payload
    (displayType + config + data + links) the widget renders from.
    Also hydrates source fields for raw SQL tiles so $__sourceTable /
    $__filters macros expand correctly.
  * tools/query/schemas.ts: tightens schema descriptions so the LLM
    picks the right displayType and aggFn.
  * ui/widget.ts: HTTP route that serves the widget bundle. Reads the
    file once at process start, re-reads on dev mtime changes so
    `yarn workspace @hyperdx/mcp-widget dev` hot-reloads.
  * api-app.ts: mounts the widget route.

This is Phase 0 of the MCP Apps experiment. End-to-end browser
verification was deferred (see agent_docs/mcp-apps.md for state).
The external-API conversion utilities silently dropped `granularity`
(the time-bucket size for line / stacked_bar configs). Without it the
renderer omits the toStartOfInterval column and rows aggregate to a
single point per series, so the MCP query tool (which goes through
this conversion path) couldn't produce time-bucketed output.

Changes:
  * externalDashboardTimeChartConfigSchema accepts an optional
    granularity string (max 64 chars).
  * convertToExternalTileChartConfig forwards granularity for line and
    stacked_bar branches.
  * convertToInternalTileConfig picks granularity through to the
    internal config.

Format is "<number> <unit>" where unit is second, minute, hour, day.
Tests:
  * jest.setup.ts: default HYPERDX_APP_PORT so config.ts captures a
    valid FRONTEND_URL at module load (without it tests that import
    helpers crashed on `new URL("http://localhost:undefined")`).
  * queryTool.test.ts: extend existing tests to cover the
    structuredContent shape returned for each displayType.
  * widget.test.ts: new suite verifying the widget resource is served
    with the App profile mimeType, the bundle is non-trivial, the App
    handshake string ("ui/initialize") is present, and the tool
    advertises both _meta keys (slash-form and nested-form).

Docs:
  * agent_docs/mcp-apps.md: developer notes on Phase 0 of the MCP Apps
    experiment, why each architectural decision was made, and what
    remains for Phase 1+.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 5, 2026

⚠️ No Changeset found

Latest commit: 01824b3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 5, 2026 5:23pm

Request Review

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