feat(playground): persist MCP App UI metadata across restarts#2057
Merged
feat(playground): persist MCP App UI metadata across restarts#2057
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Persists the MCP App “tool name → { serverName, resourceUri }” UI metadata map in SQLite and lazily hydrates the in-memory cache on first access, so historical MCP App iframes render immediately after an app restart (without requiring a new chat stream).
Changes:
- Added SQLite migration for
mcp_app_ui_metadataplus reader/writer APIs to replace/clear/read the map. - Updated
main/src/chat/mcp-tools.tsto lazily load UI metadata from SQLite and persist the rebuilt cache after tool discovery. - Added DB unit tests covering read/replace/clear behaviors, and updated the in-memory test DB helper to apply the new migration.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| main/src/db/writers/mcp-app-ui-metadata-writer.ts | Adds transactional “replace all” + clear writer for the UI metadata table. |
| main/src/db/readers/mcp-app-ui-metadata-reader.ts | Adds reader returning the same shape as the in-memory cache. |
| main/src/db/migrator.ts | Registers migration 004. |
| main/src/db/migrations/004-mcp-app-ui-metadata.ts | Creates mcp_app_ui_metadata table and server-name index. |
| main/src/db/tests/writers-readers.test.ts | Adds unit tests for the new reader/writer. |
| main/src/db/tests/test-helpers.ts | Applies migration 004 in the in-memory test DB. |
| main/src/chat/mcp-tools.ts | Adds lazy DB hydration + persistence of the cache after createMcpTools(). |
Introduces migration 004 with mcp_app_ui_metadata(tool_name PK, server_name, resource_uri, updated_at) and an index on server_name. Applies it in the in-memory test DB helper. Groundwork for persisting the Playground MCP App UI metadata cache across app restarts.
Adds readAllMcpAppUiMetadata, replaceAllMcpAppUiMetadata (transactional DELETE + bulk INSERT so each call is atomic and prunes stale rows), and clearAllMcpAppUiMetadata. Covered by 5 new tests: empty read, round-trip, replace-without-accumulation, empty-replace clears the table, and clearAll.
getCachedUiMetadata() now seeds the in-memory cache from SQLite on first access via ensureUiMetadataLoaded(), and createMcpTools() calls replaceAllMcpAppUiMetadata(cachedUiMetadata) at the end of each stream setup (even when empty, so servers that lost all UI tools have their stale rows pruned). Historical tool parts with _meta.ui now remount McpAppView on app restart without first requiring a new streaming session to repopulate the cache.
231f777 to
2cd61b8
Compare
…failure When createMcpTools() used to reset the module-level cachedUiMetadata at the top and unconditionally persist it at the bottom, a transient fetchWorkloads() or getEnabledMcpTools() rejection would wipe the previously persisted rows — defeating the whole point of the cache. Build discovery into a local map and only swap + persist once the required calls have resolved. Made-with: Cursor
…DB read failure ensureUiMetadataLoaded() was latching uiMetadataLoaded=true before the SQLite read, so a transient failure (e.g. database locked at startup) would permanently disable retries for the rest of the process — leaving the cache empty until a new chat stream rebuilt it. Move the latch assignment after the successful read so later callers retry the load. Made-with: Cursor
JAORMX
approved these changes
Apr 21, 2026
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.
The Playground's MCP App iframe map (tool name →
{ serverName, resourceUri }) lived only in a module-level variable inmain/src/chat/mcp-tools.ts(cachedUiMetadata) that was cleared and rebuilt on everycreateMcpTools()call. That meant on a fresh app launch the cache was empty until the user kicked off a new chat stream — so any historical message inmain/src/chat/threads-storage.tswhose tool parts had a_meta.uiresource silently rendered as a plain tool output instead of itsMcpAppViewiframe, becausechat-message.tsxgatesMcpAppViewon the tool name being present in the metadata map consumed byuseMcpAppMetadata.This PR mirrors the cache into SQLite (same DB that already backs chat threads, settings, and feature flags) and seeds the in-memory map lazily from the DB on the first
getCachedUiMetadata()call, so historical MCP App iframes show up immediately after a restart. The renderer and IPC surface are unchanged.Kapture.2026-04-21.at.11.48.26.mp4
DB layer (
main/src/db/)004-mcp-app-ui-metadata.ts—mcp_app_ui_metadata(tool_name TEXT PRIMARY KEY, server_name TEXT NOT NULL, resource_uri TEXT NOT NULL, updated_at INTEGER NOT NULL)plusidx_mcp_app_ui_metadata_serverfor possible future per-server cleanup. Registered as id 4 inmigrator.tsand applied in the in-memory test DB helper.writers/mcp-app-ui-metadata-writer.ts—replaceAllMcpAppUiMetadata(map)runsDELETE+ bulkINSERTinside a singledb.transaction(), so the row set is atomically swapped on each stream and stale entries are pruned in the same operation.clearAllMcpAppUiMetadata()is exposed for symmetry with other writers. No encryption:tool_nameandresource_uriare not secrets (unlike theai_providerstable).readers/mcp-app-ui-metadata-reader.ts—readAllMcpAppUiMetadata()returns aRecord<string, { serverName; resourceUri }>shaped exactly like the existing in-memorycachedUiMetadata, so the cache can be seeded without an adapter.Cache plumbing (
main/src/chat/mcp-tools.ts)uiMetadataLoadedlatch and anensureUiMetadataLoaded()helper that callsreadAllMcpAppUiMetadata()exactly once and populates the in-memory map. Failures are logged and swallowed — the cache is advisory, and the next chat stream will rebuild it anyway.getCachedUiMetadata()callsensureUiMetadataLoaded()before returning, so the first renderer IPC (chat:get-tool-ui-metadata) after startup returns the persisted map.createMcpTools()keeps its existing reset-and-rebuild semantics, marks the cache as "loaded" when it resets (so a concurrent read doesn't trigger a redundant DB fetch), and callsreplaceAllMcpAppUiMetadata(cachedUiMetadata)after the Sentry breadcrumb — including when the map is empty, so uninstalling an MCP workload with UI tools cleans the table on the next stream.How to validate
McpAppViewimmediately (the metadata map is hydrated from SQLite on the firstgetToolUiMetadataIPC).mcp_app_ui_metadataby the transactional replace increateMcpTools().Tests
main/src/db/__tests__/writers-readers.test.tscovering empty-table read, round-trip, replace-without-accumulation (second call prunes entries not in the new map), empty-map replace clearing the table, andclearAllMcpAppUiMetadata.pnpm run type-checkand the fullmain/src/db+main/src/chatVitest suites are green (71 + 56 tests).Not changed in this PR (deliberate)
useMcpAppMetadatastill callschat:get-tool-ui-metadataand listens tochat:stream:tool-ui-metadata; seeding from SQLite happens transparently insidegetCachedUiMetadata().createMcpTools()call rather than reactively on workload delete. If a user reopens a thread that referenced an uninstalled server before any new stream runs, the iframe will render and fail through the existing error path infetchUiResource+McpAppView— same behavior as today when a workload is merely stopped._meta.ui.csp/permissions/prefersBorderfromresources/readare not persisted. They are re-fetched per resource load insideMcpAppViewand aren't part of the tool→server map.