Serve the run report UI to MCP clients as an MCP App - #210
Merged
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: Cursor <cursoragent@cursor.com>
The origins came from presigning a throwaway key per bucket. That is async, so `registerResource` snapshotted a possibly-empty cache for `resources/list` while `resources/read` awaited the real list, and a cold instance advertised no origins on its first listing. `s3BucketOrigin` now composes the origin beside the `S3Client`, so one synchronous `runReportUiMeta` serves both paths and they cannot disagree. It also skips the S3 hosts under `LOCAL_S3_MIRROR`, where the mirror already serves bytes from the app's own origin. Expands `developer-docs/mcp-apps.md` into a full implementation reference: host render flow, the three app-only tools, the shared data source, the build, and known limitations. Co-authored-by: Cursor <cursoragent@cursor.com>
`report_view_table` and `report_view_artifact` streamed CSVs and JSON artifacts into the function, parsed them, and returned rows. The web app has always read the same files straight from S3 through the download redirect, so one `report_view_file_url` now replaces both and returns a signed URL that the view fetches and parses in the iframe. That removes the 50,000-row scan cap, the `truncated` flag three of four callers discarded, and the double serialization into `structuredContent` and pretty-printed text. Resolving an uncached file id drops from four tool calls to one. Tools go from 35 to 34. The content security policy listed only the raw and archives buckets, but processed artifacts sit in their own bucket and are most of what the report renders, so images, video, and CSVs would have been blocked in production. It now lists raw plus processed through a new `S3_PROCESSED_BUCKET`, drops archives, and repeats those origins in `connectDomains`. Both buckets move to a `*` GET CORS rule because the sandbox origin is chosen by the host and cannot be allowlisted ahead of time. Also trims the comment blocks in `template.yaml` to two lines each. Co-authored-by: Cursor <cursoragent@cursor.com>
…plate maps. Co-authored-by: Cursor <cursoragent@cursor.com>
`fetchReportItems` now takes an optional `AbortSignal`. The controllers in `useReportItems` were only gating `setState`, so a debounced search or a held-down paging button left every superseded request running to completion. The REST source hands the signal to `fetch`; the View hands it to `callServerTool`, which tells the host to drop the in-flight `tools/call`. The post-resolve `aborted` checks stay, because the signal is optional and an implementation may ignore it. `clearSearch` built a controller that nothing ever aborted, so its guards could never fire and unmounting mid-request still wrote to dead state. It now shares the controller `extend` uses: both load one window for the current query, so the later action cancels the earlier one. An unset `S3_RAW_DATA_BUCKET` or `S3_PROCESSED_BUCKET` silently produced a content security policy that blocks that bucket's files, and the only symptom was blank images in someone else's chat client. It now warns once per process, naming the variable. It deliberately does not throw: `mcp-handler` rebuilds the server on every request, so throwing would fail every tool rather than just the run report View. Also folds in comment edits across the View, CSP, and infra files that were already sitting in the working tree. Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Summary
When someone asks a chat client for a Data Hub run report today, the client gets JSON back and describes it in words. After this change, a client that supports MCP Apps shows the same interactive report the Data Hub website shows: the image carousel, the plate map, the Aunty plate with its well dialog.
MCP Apps (SEP-1865) is an extension to the Model Context Protocol. The server publishes an HTML page at an address beginning with
ui://, a tool result points at that page, and the client runs it in a locked-down iframe on a hostname separate from its own. Throughout this description, host means the chat client doing that work and View means the HTML page.The same React components render the website and the View. The website feeds them over the REST API; the View feeds them over MCP tool calls.
How a report reaches the screen
get_run_reportfor an instrument and a run.ui://data-hub/run-report.get_run_reportresult.report_view_itemsorreport_view_file_urlfor anything else it needs.No file contents pass through the web server. The server checks permissions and signs a link; S3 serves the bytes. This is what the website has always done through the
/api/v1/files/:id/downloadredirect.Server changes
ui://data-hub/run-reportis a new resource. Its body is one self-contained HTML file served with the media typetext/html;profile=mcp-app.get_run_reportnow carries a pointer to that resource, so a host knows to render the page.report_view_itemsreturns one window of report items with fresh download links.report_view_file_urlreturns a link for a single file, found by numeric id or by filename suffix. Both are marked visible to the page rather than the model, and both run the same permission checks the REST API runs.report_view_file_urlre-checks that the file belongs to the run before signing anything, because the id lookup is not run-scoped on its own.S3_RAW_DATA_BUCKETorS3_PROCESSED_BUCKETunset now logs a warning naming the variable, once per process. Without that warning the only symptom is blank images in someone else's chat client. It does not throw, becausemcp-handlerrebuilds the server on every request and a throw would fail every tool rather than just the View./mcp/v1now answers preflight requests and sendsAccess-Control-Allow-Origin: *, because browser-based hosts reach it from a different origin. It authenticates with a bearer token only and never setsAccess-Control-Allow-Credentials, so browsers will not attach a session cookie.Shared component changes
The seven instrument renderers used to call
fetch("/api/v1/…")directly, which only works inside Next.js. They now receive a data source through React context, and each surface supplies its own.web/lib/runs/view-data-source.tsdefines the contract; there is a REST implementation for the website and an MCP implementation for the View.Requests for a window of report items now carry a cancellation signal. The search box in the item seeker fires on a 300 ms debounce and the paging buttons can be held down, so without one every superseded request ran to completion. The REST source passes the signal to
fetch; the View passes it to the tool call so the host can drop the request in flight.Two changes are visible on the website, not only in the View:
RunReportSectionandRunVideoPlayerbecame client components. The REST data source returns download paths synchronously, so server-rendered HTML still contains thesrcand there is no loading flash.Build
The View is a Vite and Tailwind app under
web/mcp-apps/.npm run buildrunsmcp-apps:buildfirst, thennext build.vite-plugin-singlefileinlines all JavaScript and CSS, so the output is one file that makes no external requests: 1,309,067 bytes, or 353,014 bytes gzipped. Recharts and the shadcn component tree account for most of that. Hosts may cache the page.The built file is gitignored. In development a missing build falls back to a placeholder page and logs a warning. In production it throws.
S3 bucket CORS is now open to any origin
Both data buckets go from three named origins to
*ininfra/template.yaml. A sandboxed iframe's origin is chosen by the host and can benull, so no fixed list can match it.The objects stay private and are only reachable with a signed link. What changes is that a web page holding a signed link can now read the file's contents with
fetch, where before it could only start a download. That has an effect only if a signed link leaks.Testing
Checked in a browser against a local MCP Apps host:
Not checked, because no local fixture data exists: the microscopy carousel, Raman spectra, qPCR, and a PDF with real page content.
Automated:
make check-allpasses, and 339 unit and MCP protocol tests pass.Open item before merge
web/lib/mcp/run-report-html.tsreads the built page from disk at runtime, using a path assembled fromprocess.cwd(). Nothing inweb/next.config.mjstells Next.js to include that file in the deployed function. Next's file tracer can sometimes work out this pattern on its own, so it may already work.If it does not, the page is missing in production,
resources/readthrows, and only a host that supports MCP Apps would ever notice. Confirm on a preview deploy. If the file is missing, add:Known limits
tools/listfor every client. Hiding them is the host's job under the spec, andmcp-handlerbuilds a fresh server per request and registers tools beforeinitialize, so this server cannot vary its list by client.nullon failure and React skips the re-render that would retry.Docs
developer-docs/mcp-apps.mdis new and covers the design, the data flow, the content security policy, and local development.getting-started.mdandfirst-time-deployment.mdgain a note thatS3_PROCESSED_BUCKETmust be set on a real deploy, since the policy has to name that origin even though download links never read the variable.