feat(MCP UI) chat widgets#2052
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end support for rendering interactive MCP UI widgets (“MCP Apps”) inline in the chat UI, including: backend endpoints to proxy MCP app tool/resource calls, agent-side toolset detection and model-payload compaction for UI-capable tools, and frontend components/context to render and broker interactions with the embedded app.
Changes:
- Backend: detect MCP App-capable tools, filter app-only tools, and compact model-visible tool results to avoid repeated render/poll loops.
- Backend: add
/api/mcp-apps/...endpoints to list tools, call tools, and readui://resources fromRemoteMCPServer. - Frontend: render MCP apps within tool call UI; add MCP app inspector route; add (adjacent) file attachments + chat minimap UI.
Reviewed changes
Copilot reviewed 32 out of 33 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/lib/messageHandlers.ts | Preserves raw tool results + surfaces file artifacts and file parts for rendering on reload. |
| ui/src/lib/fileUpload.ts | Adds client-side file allowlist/size guard and conversion to A2A FilePart. |
| ui/src/components/ToolDisplay.tsx | Adds MCP App renderer embedding for completed tool calls with UI resources. |
| ui/src/components/mcp/McpServersView.tsx | Adds “MCP Apps” navigation entry for servers. |
| ui/src/components/mcp-apps/McpAppsInspector.tsx | Adds a UI to browse/invoke MCP App-capable tools and render their resources. |
| ui/src/components/mcp-apps/McpAppRenderer.tsx | Implements the sandboxed MCP app renderer and host↔app bridging. |
| ui/src/components/chat/ToolCallDisplay.tsx | Threads MCP app metadata + raw tool results into the tool call display pipeline. |
| ui/src/components/chat/FileAttachment.tsx | Renders file parts as thumbnails/download chips. |
| ui/src/components/chat/ChatMinimap.tsx | Adds a scroll minimap for chat history navigation. |
| ui/src/components/chat/ChatMessage.tsx | Renders file attachments and forwards MCP app hooks into tool-call rendering. |
| ui/src/components/chat/ChatMcpAppsContext.tsx | Adds chat-level registry and routing logic for MCP App-capable tools. |
| ui/src/components/chat/ChatInterface.tsx | Wires MCP app message/tool-call promotion, file upload UI, and minimap integration. |
| ui/src/components/chat/tests/ChatMcpAppsContext.test.tsx | Unit tests for app/tool registration behavior and app-only filtering. |
| ui/src/app/servers/[namespace]/[name]/apps/page.tsx | Adds a route for the MCP Apps inspector. |
| ui/src/app/api/mcp-apps/[namespace]/[name]/tools/[toolName]/call/route.ts | Next.js API proxy for MCP app tool calls. |
| ui/src/app/api/mcp-apps/[namespace]/[name]/resources/route.ts | Next.js API proxy for MCP app resource reads. |
| ui/src/app/api/mcp-apps/_utils.ts | Shared proxy helpers + OPTIONS handler for MCP apps API routes. |
| ui/src/app/actions/mcp-apps.ts | Server actions for listing tools, calling tools, and reading resources. |
| ui/src/app/actions/tests/mcp-apps.test.ts | Unit tests for MCP apps server actions path encoding and payloads. |
| ui/public/sandbox_proxy.html | Adds the sandbox proxy document used by the MCP app iframe renderer. |
| ui/public/mockServiceWorker.js | Updates MSW generated worker version. |
| ui/package.json | Adds MCP UI / MCP SDK dependencies. |
| ui/package-lock.json | Locks new MCP UI / MCP SDK dependency tree. |
| go/core/internal/httpserver/server.go | Adds route wiring for MCP Apps endpoints. |
| go/core/internal/httpserver/handlers/mcpapps.go | Implements MCP Apps list-tools/call-tool/read-resource handlers. |
| go/core/internal/httpserver/handlers/handlers.go | Registers the new MCPApps handler. |
| go/build.err | Adds a build log artifact (should not be committed). |
| go/adk/pkg/mcp/registry.go | Toolset creation now identifies MCP App tools and filters app-only tools. |
| go/adk/pkg/mcp/registry_test.go | Adds tests for MCP app detection and app-only visibility logic. |
| go/adk/pkg/agent/mcp_apps.go | Adds model-result compaction callback for MCP App tool renders. |
| go/adk/pkg/agent/mcp_apps_test.go | Tests for model-result compaction behavior. |
| go/adk/pkg/agent/agent.go | Wires MCP App callback only when MCP App-capable tools are present. |
| design/EP-2046-chat-mcp-ui-widgets.md | Design doc describing goals, approach, and test plan. |
Files not reviewed (1)
- ui/package-lock.json: Generated file
Comments suppressed due to low confidence (1)
go/build.err:39
go/build.errlooks like a locally-captured build log and is now stale (it reportsCreateToolsetsreturning 2 values while the PR updatesagent.goaccordingly). This file shouldn't be committed; it will also confuse CI/reviewers about the current build state. Please remove it from the PR (and rely on CI logs instead).
# github.com/kagent-dev/kagent/go/adk/pkg/agent
adk/pkg/agent/agent.go:58:14: assignment mismatch: 1 variable but mcp.CreateToolsets returns 2 values
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
4371099 to
794c8f4
Compare
00d6a01 to
9943ffe
Compare
9943ffe to
09d6d13
Compare
|
Addressed review feedback in
Re: collecting MCP app tool names during |
5bd26fc to
ec65548
Compare
Classify MCP App-capable tools during toolset creation and compact model-visible app tool results so the agent does not re-invoke render tools. Mirrors the Go ADK MCP Apps integration. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
Render MCP App tools as interactive UI widgets via @mcp-ui/client, sandboxed through a same-origin proxy. Add chat MCP Apps context, server actions for tool/resource proxying, and inline widget updates so app-initiated tool calls reuse the same chat widget. List MCP Apps alongside tools in the servers view and add chat minimap/layout fixes. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
Add a RemoteMCPServer and Agent that exercise the public Server Everything reference MCP server, including its dual-visibility weather-dashboard UI tool, for verifying the chat MCP UI widget integration end to end. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
caf3e0f to
1831b16
Compare
EItanya
left a comment
There was a problem hiding this comment.
This is looking like a great start. I just have some notes and clarifying questions to make sure we get to a clean final state
| // the model-result compaction callback (see agent.MakeMCPAppModelResultCallback) | ||
| // only to these tools. Collect them from CreateToolsets output via | ||
| // MCPAppToolNamesFromToolsets. | ||
| type MCPAppToolNames map[string]bool |
There was a problem hiding this comment.
Why do we need a type alias for this?
There was a problem hiding this comment.
It's not strictly required — MCPAppToolNames is a named map[string]bool that travels across a few signatures (agentVisibleToolFilter → mcpAppToolset → MCPAppToolNamesFromToolsets → agent.MakeMCPAppModelResultCallback). The alias gives that value one documented meaning ("set of model-visible tool names that render as MCP App widgets; value always true, only key presence matters") instead of a bare map[string]bool at each call site, and it's the natural home for that doc comment. No behavior is attached. Happy to inline it back to map[string]bool if you'd prefer fewer named types — let me know.
There was a problem hiding this comment.
I would prefer removing it. The use of a custom type here is a bit arbitrary and the variables will be named anyway.
| // mcpToolKindApp is an agent-visible tool whose result renders as an | ||
| // interactive MCP App (UI) widget in the chat (declares a ui.resourceUri). | ||
| mcpToolKindApp | ||
| // mcpToolKindAppOnly is hidden from the agent and only callable from within | ||
| // the rendered MCP App (visibility declares "app" but not "model"). | ||
| mcpToolKindAppOnly |
There was a problem hiding this comment.
These names are confusing to me. So to be clear on purpose, mcpToolKindAppOnly is a tool which is only meant to be called from within the UI element itself?
There was a problem hiding this comment.
Yes, exactly — mcpToolKindAppOnly is a tool that is hidden from the model and only invoked from within the rendered MCP App itself (e.g. the widget's own refresh/drill-down buttons). It maps to _meta.ui.visibility: ["app"] without "model".
To make the kinds clearer and align with the spec's visibility vocabulary ("model" / "app"), I renamed mcpToolKindAgent → mcpToolKindModel:
mcpToolKindModel— regular tool visible to the model (the LLM/agent); no UI. (visibility"model"or absent)mcpToolKindApp— model-visible and declares_meta.ui.resourceUri, so it renders an MCP App widget; the model may call it too.mcpToolKindAppOnly— visible to the app only, hidden from the model.
Each const now has a doc comment tying it back to the _meta.ui fields, in the new mcp_ui.go.
There was a problem hiding this comment.
The names are still confusing to me. How about mcpToolKindAppInternal?
There was a problem hiding this comment.
Done in 77501c0 — renamed mcpToolKindAppOnly → mcpToolKindAppInternal (and its String() label to app_internal), and updated the classifier/filter usages, the registry_test.go cases, and the Python mirror's cross-reference comment. No behavior change.
Extract the MCP Apps (MCP UI) classification helpers out of registry.go
into a dedicated mcp_ui.go, with a file header linking the MCP Apps
extension overview and full spec so the _meta.ui contract is tracked in
one place. Rename mcpToolKindAgent -> mcpToolKindModel to match the spec's
_meta.ui.visibility vocabulary ("model" / "app"). No behavior change.
Addresses review feedback on PR kagent-dev#2052.
Extract the MCP Apps (MCP UI) classification helpers out of registry.go
into a dedicated mcp_ui.go, with a file header linking the MCP Apps
extension overview and full spec so the _meta.ui contract is tracked in
one place. Rename mcpToolKindAgent -> mcpToolKindModel to match the spec's
_meta.ui.visibility vocabulary ("model" / "app"). No behavior change.
Addresses review feedback on PR kagent-dev#2052.
Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
Resolve send-guard conflict in ui/src/components/chat/ChatInterface.tsx by adopting upstream's localMessages/comparable-message approach (kagent-dev#2034) over the branch's earlier high-water-mark implementation. Also picks up configurable stream timeout (kagent-dev#1973), go default declarative runtime (kagent-dev#2083), and controller service annotations (kagent-dev#2088).
e768104 to
fe4862a
Compare
…-widgets Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
…-widgets Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
…ge blocks Merging upstream main swapped this branch's server-authoritative send guard for a content-signature comparison (countSendGuardComparableMessages / countBackendBackedComparableMessages). That comparison produced false positives: after a normal turn the UI never reloads from the DB, so live-streamed messages whose toolResultData.raw_result serializes slightly differently from the DB copy (notably MCP App widget payloads) made local < db and wrongly showed "New messages loaded - please review before sending", blocking the send. Restore the content-independent high-water mark: count persisted history items across tasks (a value the DB assigns identically for every tab), set it on load/reload and after each completed turn, and block only when the server has advanced past the synced count (another tab acted). Drop the now-dead comparable-message helpers and rewrite the send-guard tests. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
Restored the high-water-mark send guard (commit 9ab5717)Heads up on the latest push. The merge from Symptom: after a normal chat turn, the next send was blocked with "New messages loaded — please review before sending" even though nothing had changed and no other tab was open. Root cause: that guard compares the content signature of locally rendered messages against messages re-extracted from the DB. After a turn the UI does not call Fix: reverted to the content-independent high-water mark this branch used before the merge. It counts persisted history items across tasks ( Also removed the now-dead comparable-message helpers from |
|
Posting a quick summary from |
…iation Address MCP Apps spec-deviation review on PR kagent-dev#2052: - Reject app-originated tools/call for tools whose _meta.ui.visibility excludes "app" (server-side in HandleCallTool) instead of trusting the client check. Default ["model","app"] stays app-callable. - Sandbox proxy builds and injects a Content-Security-Policy from the resource's _meta.ui.csp (spec restrictive default + object-src 'none' when absent); McpAppRenderer forwards the declared csp via the sandbox prop. - Advertise the io.modelcontextprotocol/ui extension capability (mime type text/html;profile=mcp-app) on both Go MCP clients so capability-gated servers expose UI tools. - KAgentMcpToolset.get_tools hides app-only tools from the model, mirroring the Go ADK filter, while keeping them app-callable. Cross-origin sandbox isolation (serving the proxy from a separate origin) is tracked as a follow-up. Signed-off-by: Dmytro Rashko <dimetron@me.com>
MCP Apps spec-deviation review — addressed in
|
| # | Claim | Status | Severity | Blocks merge? | Fix possible? | Fix description |
|---|---|---|---|---|---|---|
| 1 | Backend lets app-originated calls invoke non-app-visible tools | Valid | High (security) | Yes | Yes — small | HandleCallTool lists tools on the session it already opens, parses _meta.ui.visibility, and rejects only when visibility is present and lacks "app" (default / ["app"] / ["model","app"] stay allowed). Client check kept as defense-in-depth. |
| 2a | Sandbox applies no CSP (ignores ui.csp) |
Valid | High (security) | Yes | Yes — small | @mcp-ui/client already forwards CSP (sandbox-resource-ready {html, csp} + ?csp=). Host reads _meta.ui.csp and passes it via the sandbox prop; sandbox_proxy.html injects a <meta http-equiv="Content-Security-Policy"> with the spec restrictive default + object-src 'none'. Default keeps script-src 'self' 'unsafe-inline', so widgets still render. |
| 2b | Sandbox is same-origin as host (allow-same-origin → no isolation) |
Valid | High (security) | Follow-up | Architectural | Serve the proxy from a distinct origin so allow-same-origin no longer resolves to the host origin. Deploy/topology change — tracked as a follow-up; boundary stays soft until it lands. |
| 3 | MCP Apps capability negotiation missing | Valid (SHOULD) | Low (interop) | No | Yes — tiny | Both Go MCP clients now advertise io.modelcontextprotocol/ui (mimeTypes: ["text/html;profile=mcp-app"]) via ClientCapabilities.Extensions. Purely additive. |
| 4 | Python runtime doesn't hide app-only tools from the model | Valid | Medium (correctness) | Yes | Yes — small | KAgentMcpToolset.get_tools drops tools whose visibility is ["app"] without "model" from the model-visible list (still app-callable), mirroring the Go ADK filter. |
Implemented (1, 2a, 3, 4)
- 1 —
toolAllowsAppCall+visibilityAllowsAppingo/core/internal/httpserver/handlers/mcpapps.go; app calls to non-app-visible tools now return403(404for unknown). Test:TestVisibilityAllowsApp. - 2a —
ui/public/sandbox_proxy.htmlbuilds + injects a spec CSP;McpAppRenderercaptures_meta.ui.cspand forwards it viasandbox. - 3 —
mcpUIClientCapabilitiesingo/adk/pkg/mcp/mcp_ui.goand theconnectclient inmcpapps.go. - 4 — filter in
python/.../_mcp_toolset.py. Test:test_get_tools_hides_app_only_tools_from_model.
Follow-up (2b)
Cross-origin sandbox isolation is a deployment/topology change, left as a tracked follow-up.
Verified locally: Go (core + adk) build & tests, Python unit tests (16 passed), UI lint + jest MCP-apps suites.
…-widgets Signed-off-by: Dmytro Rashko <dimetron@me.com> # Conflicts: # ui/src/components/chat/AskUserDisplay.tsx # ui/src/components/chat/ChatInterface.tsx
Replace the manual visibility loop with slices.Contains in visibilityAllowsApp (go-lint modernize). Also record the same-origin sandbox decision (review item 2b) as a ponytail ceiling note in McpAppRenderer so the accepted trade-off and upgrade path are explicit. Signed-off-by: Dmytro Rashko <dimetron@me.com>
|
FYI @dimetron, this is what's failing the snyk job: |
Address review feedback on PR kagent-dev#2052: "AppOnly" read ambiguously, so use "AppInternal" to make clear this kind is hidden from the model and only callable from within the rendered MCP App itself. Updates the String() label, classification/filter usages, tests, and the Python mirror's cross-reference comment. No behavior change. Signed-off-by: Dmytro Rashko <dimetron@me.com>
Pin qs >=6.15.2 (resolves to 6.15.3) to remediate the qs.stringify DoS advisory GHSA-q8mj-m7cp-5q26 flagged by the Snyk PR check, and bump fast-uri to ^3.1.3. Both are dependency overrides only; no source change. Signed-off-by: Dmytro Rashko <dimetron@me.com>
…-widgets Resolve the send-guard conflict in ui/src/components/chat/__tests__/ChatInterface.sendGuard.test.tsx by keeping this branch's high-water-mark guard suite. Upstream kagent-dev#2115 fixed the content-signature guard's tool-call false positive, but this branch already replaced that guard with a content-independent high-water mark (counts persisted history items), which is immune to the same class of false positives — including the MCP App widget payload serialization differences that regressed the content-signature guard. The upstream test additions exercise the removed content-signature helpers, so they are dropped here. Keep upstream's non-conflicting changes: extractMessagesFromTasks contextId/taskId backfill (messageHandlers.ts), and the A2A executor / Python event_converter id stamping (defense-in-depth, harmless under the high-water-mark guard). Signed-off-by: Dmytro Rashko <dimetron@me.com>
| // resolveRemoteMCPServer locates the MCP endpoint for the given ref, supporting | ||
| // both RemoteMCPServer (external URL) and the kmcp MCPServer CRD (an in-cluster | ||
| // Deployment+Service). An MCPServer is converted to the same RemoteMCPServer | ||
| // shape the controller uses for tool discovery, so both kinds share one connect | ||
| // path. | ||
| func (h *MCPAppsHandler) resolveRemoteMCPServer(ctx context.Context, namespace, name string) (*v1alpha2.RemoteMCPServer, error) { |
There was a problem hiding this comment.
There are a number of issues with this:
- The name makes it seem like it's only
RemoteMcpServer - The behavior is confusing, if there is a
RemoteMCPServerwith the samename/nsas anMCPServerthan it will be queried, even if the user intended to get theMCPServer
| // attaches the model-result compaction callback (see | ||
| // agent.MakeMCPAppModelResultCallback) only to these tools. Collect them from | ||
| // CreateToolsets output via MCPAppToolNamesFromToolsets. | ||
| type MCPAppToolNames map[string]bool |
There was a problem hiding this comment.
I thought we discussed getting rid of this type as it is just cosmetic.
…server by groupKind Address EItanya's review on PR kagent-dev#2052: - Remove the Go MCPAppToolNames type alias (a cosmetic map[string]bool) and use map[string]bool directly across mcpAppToolset, MCPAppToolNamesFromToolsets, and MakeMCPAppModelResultCallback. Variable names already carry the meaning. - Rename resolveRemoteMCPServer -> resolveMCPServerEndpoint and take a groupKind so a RemoteMCPServer and an MCPServer sharing a namespace/name resolve to the CRD the caller actually selected instead of always preferring RemoteMCPServer. The name no longer implies RemoteMCPServer only. The UI threads the selected server's groupKind through the MCP Apps list/call/read endpoints; an empty groupKind preserves the legacy RemoteMCPServer-then-MCPServer fallback. Signed-off-by: Dmytro Rashko <dimetron@me.com>
|
@EItanya ready for another look when you get a chance. Both threads from your Jul 1 review are addressed in edab7e7:
Branch is up to date with |
…-widgets Signed-off-by: Dmytro Rashko <dimetron@me.com>


This pull request adds support for rendering interactive MCP UI widgets (MCP Apps) directly within the chat interface. It introduces backend and frontend changes to detect, handle, and render tools that provide UI resources, ensuring a richer and more interactive user experience. The backend now distinguishes between regular tools and those with UI widgets, compacts tool responses sent to the model to prevent unnecessary repeated calls, and exposes new endpoints for MCP-app interactions. The UI is updated to render these widgets inline and broker interactions between the app and the chat.
Backend: MCP App tool detection and handling
CreateToolsetsfunction inregistry.gonow returns both the toolsets and a set of tool names that support MCP Apps, enabling the agent to treat these tools differently. [1] [2] [3] [4] [5] [6]MakeMCPAppModelResultCallbackinmcp_apps.go) is added to compact the payload sent to the model for MCP App tools, replacing heavy render payloads with a terminal notice to prevent the model from re-invoking rendering tools unnecessarily. [1] [2]Testing and validation
mcp_apps_test.goto verify correct compaction of responses, error handling, and that only MCP App tools are affected by the new logic.Documentation and design
EP-2046-chat-mcp-ui-widgets.md) describes the motivation, goals, implementation details, test plan, and open questions for this feature.