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
ec65548 to
a80dfc6
Compare
| import { NextRequest } from "next/server"; | ||
| import { mcpAppsBackendPath, optionsResponse, proxyMcpAppsRequest } from "@/app/api/mcp-apps/_utils"; | ||
|
|
||
| export async function POST( |
There was a problem hiding this comment.
can we put all these to the mcp-apps.ts actions instead? or do they need to be exposed here as API endpoints?
(asking because we don't have any API routes so far and this would be the first ones)
There was a problem hiding this comment.
These need to be real HTTP route handlers rather than server actions. The rendered MCP App runs in a sandboxed iframe and talks to the host over fetch / postMessage (per the MCP Apps spec — the app/host channel is JSON-RPC over postMessage, and tool calls are proxied to stable URLs). That requires addressable endpoints (list tools / call tool / read resource) plus OPTIONS/CORS, which server actions can't provide — a server action is RPC bound to a server-component render and isn't invokable from the sandboxed app frame. So yes, intentionally the first API routes; they're the host-broker surface for the iframe, not general data fetching. Spec ref: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/extensions/apps/overview.mdx
| params: Promise<{ namespace: string; name: string }>; | ||
| }) { | ||
| const { namespace, name } = await params; | ||
| return <McpAppsInspector namespace={decodeURIComponent(namespace)} name={decodeURIComponent(name)} />; |
There was a problem hiding this comment.
Agreed — will fold MCP App tools into the existing server tools list with a distinct icon/badge instead of a separate /apps page + context-menu entry, and drop the dedicated route. One question: do you want the standalone inspector removed entirely, or kept as the target the icon links to? I'll default to removing it and rendering inline unless you'd rather keep an inspector view.
There was a problem hiding this comment.
Follow-up: this is now implemented on the current branch (came in with the latest main merge), so the separate page/menu approach you commented on is gone:
- MCP App tools are listed inline in the server tools grid in ui/src/components/mcp/McpServersView.tsx — each tool picks
const ToolIcon = isApp ? AppWindow : FunctionSquare(~L347-348) and app tools get anAppbadge. App-ness is resolved lazily on expand via listMcpAppTools. - The standalone /apps route (ui/src/app/servers/[namespace]/[name]/apps/page.tsx), the McpAppsInspector component, and the "MCP Apps" context-menu entry have all been removed. Only McpAppRenderer.tsx remains (it renders the widget inline in chat).
So no separate page and no extra menu item — apps and tools share one list, differentiated by icon + badge, as you suggested. Could you take a look and resolve if it matches what you had in mind?
| } | ||
|
|
||
| func (h *MCPAppsHandler) HandleListTools(w ErrorResponseWriter, r *http.Request) { | ||
| namespace, name, ok := h.remoteMCPServerRef(w, r) |
There was a problem hiding this comment.
are we only supporting RemoteMCPServer CRD types?
I created a MCPServer CRD that has UI, but the /apps URL fails to render it because it's expecting the RemoteMCPServer
There was a problem hiding this comment.
Fixed — the handler now resolves both kinds. resolveRemoteMCPServer first looks up a RemoteMCPServer, and on NotFound falls back to the kmcp MCPServer CRD, converting it via ConvertMCPServerToRemoteMCPServer so both share one connect path (mcpapps.go, resolveRemoteMCPServer/connect). An MCPServer with UI metadata should now render at /apps. Could you re-test with your CRD and confirm before we resolve this thread?
|
another issue I noticed -- I am rendering a UI for weather (using https://servereverything.dev/mcp as the MCP server). The UI renders; but then when I interact wit hthe UI (e.g. click the New York button to show weather), a new request/chat is sent to teh agent and the UI re-renders as a new chat item/response. I was expecting we only render 1 instance of the MCP UI -- if user interacts with the UI we should re-render/update that same instance, not get another UI instance in the chat.
Also seeing this in the console (not sure if related to the UI re-rendering or not): |
|
@peterj - Interesting, I was testing with my own MCP's - let me test this one. Need to make sure we are on the same standard of UI libraries, it might be related to the routing - UI need to go via internal /plugins/ or we should allow Origin |
|
here's the implementation of the MCP UI I've been using: https://github.com/peterj/servereverything.dev/blob/main/src/widget.html |
05448a2 to
9fc528c
Compare
Detect MCP tools that advertise a ui/resourceUri in their _meta and classify them as app-only, agent-visible, or both. Wire MCP App tool names through the ADK agent and compact app tool results so the model does not re-invoke them. Add MCP Apps HTTP API handlers that proxy to RemoteMCPServer and MCPServer CRDs. Fix Ollama streaming tool-call aggregation across intermediate chunks. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
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.
| // 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.
| Visibility []string | ||
| } | ||
|
|
||
| func parseMCPUIMetadata(meta mcpsdk.Meta) mcpUIMetadata { |
There was a problem hiding this comment.
Can you link to the part in the MCP spec that defines where these fields are supposed to live.
There was a problem hiding this comment.
These fields are not part of the core MCP spec — they come from the MCP Apps extension and live under the tool's _meta.ui object (_meta.ui.resourceUri, _meta.ui.visibility). I've added the links in the new mcp_ui.go header:
- Overview (defines
_meta.ui.resourceUri/ preloading / sandbox): https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/extensions/apps/overview.mdx - Full spec (
2026-01-26): https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
Per the overview, the tool description carries _meta.ui.resourceUri pointing at a ui:// resource that the host preloads and renders in a sandboxed iframe.
There was a problem hiding this comment.
Can we move these mcp_ui related functions/helpers to a new file, alongside links to the MCP APPs spec so users can read where it is defined, and so we can properly track changes in the future.
There was a problem hiding this comment.
Done — moved all the MCP-UI helpers (mcpToolKind + kinds, mcpToolKindOf, parseMCPUIMetadata/mcpUIMetadata, isAppOnlyVisibility, normalizeVisibility, agentVisibleToolFilter, plus MCPAppToolNames/mcpAppToolset) into a new go/adk/pkg/mcp/mcp_ui.go. The file header documents the metadata contract and links the spec:
- Overview: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/extensions/apps/overview.mdx
- Full spec: https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
registry.go now only keeps transport/toolset wiring. Pushing shortly.
There was a problem hiding this comment.
Just so I'm clear on this file. The UI element is essentially making calls to MCP resources via raw HTTP rather than actually using the MCP protocol? Why can't it just use the existing MCP server inside of the controller?
There was a problem hiding this comment.
Good question — the file is actually doing the opposite of bypassing MCP: it's the controller acting as an MCP client proxy so the browser doesn't have to.
Two hops:
- browser ↔ controller is plain REST/JSON (
/api/mcp-apps/.../tools,/call,/resources). The MCP App runs in a sandboxed iframe (mcp-ui client) and can't open the agent's MCP transport to arbitrary upstreams; it also can't hold the server's auth/headers. So the host exposes a small REST surface, gated by the usual RBACCheckonToolServer. - controller ↔ MCP server is the real MCP protocol:
connect()buildsmcp.NewClient,Connects overStreamableClientTransport/SSEClientTransport, then callssession.ListTools/CallTool/ReadResource. Headers/secrets are resolved server-side viaRemoteMCPServer.ResolveHeaders.
Why not the existing MCP server in the controller: that server exposes the agent's own (A2A) tools. The MCP App tools and their ui:// resources live on the configured RemoteMCPServer/MCPServer the agent connects to (often external, e.g. servereverything.dev). The in-controller server doesn't proxy those upstream ui:// reads or arbitrary upstream tool calls with per-server resolved auth — that's exactly the gap this handler fills (resolve the CR → resolve headers → MCP-client proxy).
If you'd prefer to fold this into the existing MCP server as a dedicated proxy capability instead of a separate handler, I'm open to it — happy to discuss the shape.
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




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.