Skip to content

Commit 2db93af

Browse files
dimetronEItanyapeterj
authored
feat(MCP UI) chat widgets (#2052)
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** - The `CreateToolsets` function in `registry.go` now returns both the toolsets and a set of tool names that support MCP Apps, enabling the agent to treat these tools differently. [[1]](diffhunk://#diff-aa78af986baa8c0d72522820c58c6a60eb1d656aff48985d4ff3fd683a4312bfL97-R100) [[2]](diffhunk://#diff-aa78af986baa8c0d72522820c58c6a60eb1d656aff48985d4ff3fd683a4312bfL116-R124) [[3]](diffhunk://#diff-aa78af986baa8c0d72522820c58c6a60eb1d656aff48985d4ff3fd683a4312bfL138-R156) [[4]](diffhunk://#diff-aa78af986baa8c0d72522820c58c6a60eb1d656aff48985d4ff3fd683a4312bfL165-R178) [[5]](diffhunk://#diff-aa78af986baa8c0d72522820c58c6a60eb1d656aff48985d4ff3fd683a4312bfL313-R422) [[6]](diffhunk://#diff-aa78af986baa8c0d72522820c58c6a60eb1d656aff48985d4ff3fd683a4312bfL335-R435) - A new callback (`MakeMCPAppModelResultCallback` in `mcp_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]](diffhunk://#diff-47f0265e8fa992a1daf1e3aa04f2ed446630baac255502ac688dc430f6d19887R1-R72) [[2]](diffhunk://#diff-5e48253d455263dd808978b30a15e6f419f326b1b1561a46c67e51b88e3b071aR119-R124) - The agent is updated to wire in the MCP App callback only when MCP App tools are present, ensuring efficient handling. **Testing and validation** - Comprehensive unit tests are added in `mcp_apps_test.go` to verify correct compaction of responses, error handling, and that only MCP App tools are affected by the new logic. **Documentation and design** - A detailed design document (`EP-2046-chat-mcp-ui-widgets.md`) describes the motivation, goals, implementation details, test plan, and open questions for this feature. --------- Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com> Signed-off-by: Dmytro Rashko <dimetron@me.com> Signed-off-by: Peter Jausovec <peter.jausovec@solo.io> Co-authored-by: Eitan Yarmush <eitan.yarmush@solo.io> Co-authored-by: Peter Jausovec <peter.jausovec@solo.io>
1 parent 0b136cd commit 2db93af

50 files changed

Lines changed: 4996 additions & 495 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
python/.env
33
.env
44
values.local.yaml
5+
.playwright*
56

67
test_results
78

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Server Everything MCP
2+
3+
This directory wires the public [Server Everything](https://servereverything.dev/mcp)
4+
reference MCP server into kagent as a `RemoteMCPServer`, plus a demo `Agent` that
5+
exercises its tools — including the `show-weather-dashboard` **MCP App** (an
6+
interactive UI widget rendered inline in the chat).
7+
8+
It is useful for verifying the chat MCP UI widget integration (EP-2046) against a
9+
public server that uses a single dual-visibility (`["model", "app"]`) UI tool.
10+
11+
## What it provides
12+
13+
`server-everything` exposes demo tools; this agent enables a focused subset:
14+
15+
| Tool | Purpose |
16+
|------|---------|
17+
| `show-weather-dashboard` | Renders an interactive weather dashboard MCP App (UI widget). Advertised via `_meta.ui.resourceUri = ui://server-everything/weather-dashboard`, visibility `["model", "app"]`. |
18+
| `echo` | Reflects text back. |
19+
| `get-sum` | Adds two numbers. |
20+
| `get-tiny-image` | Returns a small sample image. |
21+
| `get-structured-content` | Demonstrates structured tool output. |
22+
23+
## Installation
24+
25+
```bash
26+
kubectl apply -f server-everything-remote-mcpserver.yaml
27+
kubectl apply -f server-everything-agent.yaml
28+
```
29+
30+
This creates:
31+
- a `RemoteMCPServer` named `server-everything` pointing at `https://servereverything.dev/mcp`
32+
- an `Agent` named `server-everything-agent` that uses the tools above
33+
34+
No extra Helm values are needed — MCP App (UI widget) rendering is detected
35+
automatically from each tool's `_meta.ui` metadata.
36+
37+
## Verify
38+
39+
```bash
40+
# RemoteMCPServer should reach Accepted and discover tools
41+
kubectl get remotemcpserver server-everything -n kagent -o yaml
42+
43+
# Agent should become Ready
44+
kubectl get agent server-everything-agent -n kagent
45+
```
46+
47+
Then open the agent in the kagent UI and ask: **"show the weather dashboard"**.
48+
The dashboard renders inline and updates itself in place (it re-calls
49+
`show-weather-dashboard` from inside the widget).
50+
51+
## Learn More
52+
53+
- [Server Everything](https://servereverything.dev/mcp)
54+
- [MCP Protocol](https://modelcontextprotocol.io/)
55+
- MCP UI widgets in kagent: `design/EP-2046-chat-mcp-ui-widgets.md`
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
apiVersion: kagent.dev/v1alpha2
2+
kind: Agent
3+
metadata:
4+
name: server-everything-agent
5+
namespace: kagent
6+
spec:
7+
type: Declarative
8+
description: Demo agent exercising Server Everything MCP tools, including the weather-dashboard MCP App (UI widget).
9+
declarative:
10+
modelConfig: default-model-config
11+
runtime: go
12+
stream: true
13+
systemMessage: |-
14+
You are a helpful assistant that demonstrates the Server Everything MCP tools,
15+
including an interactive weather dashboard rendered as an inline UI widget.
16+
17+
# Weather (most important)
18+
- Whenever the user asks ANYTHING about weather, forecast, temperature, or
19+
conditions for a place, you MUST call the `show-weather-dashboard` tool.
20+
Never answer weather questions from your own knowledge or with plain text.
21+
- Pass the city as the `location` argument. For example, "weather in Paris"
22+
-> call show-weather-dashboard with {"location": "Paris"}. If no city is
23+
given, use {"location": "New York"}.
24+
- The tool renders the dashboard inline and keeps itself updated, so after it
25+
runs just give a one-line confirmation and let the widget show the data —
26+
do not repeat the numbers yourself.
27+
28+
# Other tools
29+
- `echo` reflects text back, `get-sum` adds two numbers, `get-tiny-image`
30+
returns a sample image, and `get-structured-content` returns structured
31+
output.
32+
- Ask for clarification only when the request is genuinely ambiguous.
33+
- If a tool fails, point the user to https://kagent.dev for support.
34+
35+
# Style
36+
- Respond in Markdown and keep replies brief.
37+
tools:
38+
- type: McpServer
39+
mcpServer:
40+
apiGroup: kagent.dev
41+
kind: RemoteMCPServer
42+
name: server-everything
43+
toolNames:
44+
- show-weather-dashboard
45+
- echo
46+
- get-sum
47+
- get-tiny-image
48+
- get-structured-content
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## Server Everything MCP (reference/demo MCP server)
2+
## https://servereverything.dev/mcp
3+
## Exposes a set of demo tools, including the `show-weather-dashboard`
4+
## MCP App (UI widget) advertised via tool `_meta.ui.resourceUri`.
5+
apiVersion: kagent.dev/v1alpha2
6+
kind: RemoteMCPServer
7+
metadata:
8+
name: server-everything
9+
namespace: kagent
10+
spec:
11+
url: "https://servereverything.dev/mcp"
12+
protocol: STREAMABLE_HTTP
13+
timeout: 30s
14+
sseReadTimeout: 5m0s
15+
description: "Server Everything reference MCP server with an interactive weather dashboard MCP App"
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# EP-2046: Chat UI support for MCP UI widgets (MCP Apps)
2+
3+
* Issue: [#2046](https://github.com/kagent-dev/kagent/issues/2046)
4+
5+
## Background
6+
7+
The Model Context Protocol is gaining an "Apps"/UI extension
8+
(`@modelcontextprotocol/ext-apps`, rendered via `@mcp-ui/client`) that lets an MCP
9+
server attach an interactive HTML/UI **resource** to a tool. When an agent calls
10+
such a tool, the client can render a live widget instead of (or in addition to) raw
11+
tool-call JSON.
12+
13+
kagent's chat today renders tool calls as collapsible JSON. This EP makes the chat
14+
MCP-App–aware: when a tool call maps to an MCP app resource, the chat renders the
15+
app inline in a sandboxed frame and brokers messages between the app and the chat
16+
(send a message on the user's behalf, surface "visible" tool calls, proxy resource
17+
reads and tool calls back to the originating MCP server).
18+
19+
## Motivation
20+
21+
- Let MCP servers deliver rich, interactive results (forms, boards, charts, live
22+
progress) directly in the kagent chat.
23+
- Provide the in-chat rendering half of the kagent plugin story (the sidebar/plugin
24+
half is EP-2047; the first consumer is the Kanban task-progress widget, EP-2048).
25+
26+
### Goals
27+
28+
- Discover MCP app resources per MCP server and associate them with tool calls.
29+
- Render the app via a sandboxed renderer inside chat messages / tool-call display.
30+
- Broker host↔app messaging: `sendMessage`, visible tool calls, and proxying of
31+
resource reads and tool calls to the backend MCP server.
32+
- Backend endpoints to list an MCP server's tools, read its resources, and call its
33+
tools on behalf of the UI.
34+
35+
### Non-Goals
36+
37+
- The sidebar plugin/registration mechanism (EP-2047).
38+
- Shipping a specific MCP app (the Kanban task-progress app is EP-2048).
39+
- File-upload / artifact handling — note the chat files carry adjacent
40+
file-upload/minimap code (see "Adjacent code" below); that feature is tracked
41+
separately and is **not** part of this EP's scope.
42+
43+
## Implementation Details
44+
45+
### Backend
46+
47+
- **`go/adk/pkg/mcp/registry.go`**`CreateToolsets` now also returns the set of
48+
**MCP-app–capable tool names** (tools whose MCP server advertises a UI resource),
49+
so the agent can treat their results specially.
50+
- **`go/adk/pkg/agent/mcp_apps.go`**`MakeMCPAppModelResultCallback`: for
51+
MCP-app tools, keep the rich tool payload in chat history for UI rendering while
52+
compacting what is sent back to the model (avoids redundant polling/tool churn).
53+
Wired in `agent.go` only when `len(mcpAppToolNames) > 0`.
54+
- **`go/core/internal/httpserver/handlers/mcpapps.go`**`MCPAppsHandler` with
55+
`HandleListTools`, `HandleCallTool`, `HandleReadResource`, exposed under
56+
`/api/mcp-apps/{namespace}/{name}/...`. (Only the MCP-apps hunks of the shared
57+
`server.go`/`handlers.go` are included here; the plugins hunks belong to EP-2047.)
58+
59+
### UI (`ui/src`)
60+
61+
- **`components/mcp-apps/McpAppRenderer.tsx`** — renders an MCP app resource via
62+
`@mcp-ui/client` in a sandbox, wiring its `onUIAction`/resource-read/tool-call
63+
callbacks to the backend; `McpAppsInspector.tsx` is a standalone inspector view
64+
(surfaced at `app/apps/[appName]/page.tsx`, reachable by clicking an MCP app
65+
listed alongside a server's tools in `components/mcp/McpServersView.tsx`).
66+
- **`components/chat/ChatMcpAppsContext.tsx`** — context that maps a tool name to its
67+
MCP app (`getMcpAppForTool`) and brokers `sendMessage` / `McpAppVisibleToolCall`
68+
between an app and the chat.
69+
- **`components/chat/ChatLayoutUI.tsx`** — mounts `ChatMcpAppsProvider` around the
70+
chat subtree so the MCP-app context is active for every chat session (without this
71+
mount, tool calls never resolve to apps and no widget renders).
72+
- **`components/chat/ChatInterface.tsx`, `ChatMessage.tsx`, `ToolCallDisplay.tsx`,
73+
`components/ToolDisplay.tsx`** — render the app for MCP-app tool calls and forward
74+
app actions.
75+
- **`app/actions/mcp-apps.ts`** + **`app/api/mcp-apps/.../{resources,tools/.../call}`**
76+
— server actions / BFF routes calling the backend MCP-apps endpoints.
77+
- **`public/sandbox_proxy.html`** — sandbox proxy document for the app iframe.
78+
79+
### New dependencies (`ui/package.json`)
80+
81+
- `@mcp-ui/client` `^7.1.1`
82+
- `@modelcontextprotocol/ext-apps` `^1.7.1`
83+
- `@modelcontextprotocol/sdk` `^1.29.0`
84+
85+
The lockfile (`ui/package-lock.json`) and the generated `ui/public/mockServiceWorker.js`
86+
(MSW worker, bumped `2.14.2``2.14.6`) are regenerated as a side effect of resolving
87+
the new dependency tree.
88+
89+
### Adjacent code
90+
91+
Per the agreed split, the chat files (`ChatInterface.tsx`, `ChatMessage.tsx`,
92+
`messageHandlers.ts`) are taken whole and therefore also carry the chat
93+
**file-upload** (`lib/fileUpload.ts`, `chat/FileAttachment.tsx`) and **minimap**
94+
(`chat/ChatMinimap.tsx`) UI that was developed alongside MCP apps. These are
95+
included so the chat compiles, but are not the subject of this EP; the file-upload
96+
backend (artifact extraction, `save_artifact`) is intentionally **excluded**.
97+
98+
## Test Plan
99+
100+
- **Unit (Go):** `registry_test.go` (MCP-app tool-name detection) and
101+
`mcp_apps_test.go` (model-result callback). `go build ./adk/... ./core/...` and
102+
test compilation pass.
103+
- **Unit (UI):** `getMcpAppForTool` mapping (`ChatMcpAppsContext.test.tsx`); mcp-apps
104+
server actions (`actions/__tests__/mcp-apps.test.ts`); and a regression test
105+
(`chat/__tests__/ChatLayoutUI.test.tsx`) asserting `ChatLayoutUI` mounts
106+
`ChatMcpAppsProvider` around the chat so widgets can render.
107+
- **Manual / e2e:** point the chat at an MCP server exposing a UI resource; confirm
108+
the widget renders inline, `sendMessage` posts to the chat, and resource/tool-call
109+
proxying reaches the server. The Kanban task-progress widget (EP-2048) is the
110+
reference end-to-end case.
111+
112+
## Alternatives
113+
114+
- **Render apps only in a side panel (not inline in chat):** loses the
115+
tool-call→widget association and the conversational flow.
116+
- **Trust the model with full tool payloads:** causes token bloat and tool churn;
117+
hence the model-result compaction callback.
118+
119+
## Open Questions
120+
121+
- Should MCP-app rendering be opt-in per MCP server (a `spec` flag) rather than
122+
inferred from advertised UI resources?
123+
- How should multiple apps in a single conversation share/scope state?

go/adk/pkg/agent/agent.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func CreateGoogleADKAgentWithSubagentSessionIDs(ctx context.Context, agentConfig
5656
dynamicHeaderProvider = stsPlugin.HeaderProvider
5757
}
5858
toolsets := mcp.CreateToolsets(ctx, agentConfig.HttpTools, agentConfig.SseTools, propagateToken, dynamicHeaderProvider)
59+
mcpAppToolNames := mcp.MCPAppToolNamesFromToolsets(toolsets)
5960
subagentSessionIDs := make(map[string]string)
6061

6162
var remoteAgentTools []tool.Tool
@@ -116,6 +117,12 @@ func CreateGoogleADKAgentWithSubagentSessionIDs(ctx context.Context, agentConfig
116117
beforeToolCallbacks = append(beforeToolCallbacks, MakeApprovalCallback(approvalSet))
117118
beforeModelCallbacks = append(beforeModelCallbacks, MakeStripConfirmationPartsCallback())
118119
}
120+
if len(mcpAppToolNames) > 0 {
121+
// For MCP App-capable tools, keep rich tool payloads in chat history for UI rendering,
122+
// but compact what is sent back to the model to avoid redundant polling/tool churn.
123+
log.Info("Wiring MCP App model result callback", "toolCount", len(mcpAppToolNames))
124+
beforeModelCallbacks = append(beforeModelCallbacks, MakeMCPAppModelResultCallback(mcpAppToolNames))
125+
}
119126
beforeToolCallbacks = append(beforeToolCallbacks, makeBeforeToolCallback(log))
120127

121128
llmAgentConfig := llmagent.Config{

go/adk/pkg/agent/mcp_apps.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package agent
2+
3+
import (
4+
"encoding/json"
5+
6+
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
7+
"google.golang.org/adk/agent"
8+
"google.golang.org/adk/agent/llmagent"
9+
adkmodel "google.golang.org/adk/model"
10+
)
11+
12+
// mcpAppRenderedNotice is the terminal message the model sees in place of an
13+
// MCP App tool's render payload. An MCP App tool (one that declares a UI
14+
// resourceUri) produces an interactive view that is displayed to the user and
15+
// refreshes itself in-place via its own app-only tool calls. The model must
16+
// treat a successful render as a completed, self-updating artifact; otherwise it
17+
// tends to re-invoke the rendering tool on every "refresh", flooding the chat
18+
// with duplicate app cards. This notice is protocol-oriented: it applies to any
19+
// tool carrying a UI resourceUri, independent of the tool's name or payload keys.
20+
const mcpAppRenderedNotice = "The interactive UI for this tool has been rendered to the user and now updates live inside the app. Treat this as complete and do not call this tool again unless the user explicitly asks for it."
21+
22+
// MakeMCPAppModelResultCallback replaces what the model sees for MCP App
23+
// (UI-rendering) tool results: instead of the heavy render payload it receives a
24+
// terminal directive (see mcpAppRenderedNotice). The full result is still
25+
// streamed to the UI separately, so this only changes the model's view and
26+
// prevents the model from looping on the rendering tool. Errors are passed
27+
// through so the model can still react to and recover from failures.
28+
func MakeMCPAppModelResultCallback(appToolNames map[string]bool) llmagent.BeforeModelCallback {
29+
return func(_ agent.CallbackContext, req *adkmodel.LLMRequest) (*adkmodel.LLMResponse, error) {
30+
for _, content := range req.Contents {
31+
if content == nil {
32+
continue
33+
}
34+
for _, part := range content.Parts {
35+
if part == nil || part.FunctionResponse == nil || !appToolNames[part.FunctionResponse.Name] {
36+
continue
37+
}
38+
part.FunctionResponse.Response = compactMCPAppModelResponse(part.FunctionResponse.Response)
39+
}
40+
}
41+
return nil, nil
42+
}
43+
}
44+
45+
// compactMCPAppModelResponse rewrites an MCP App tool result for the model.
46+
//
47+
// The model exchanges tool results as a generic map (genai
48+
// FunctionResponse.Response), but the payload is really an MCP
49+
// [mcpsdk.CallToolResult]. We decode it into that typed result so the logic
50+
// works against real fields (IsError, Content, Meta, StructuredContent) rather
51+
// than poking at string keys. If the payload isn't a recognizable MCP result we
52+
// leave it untouched.
53+
func compactMCPAppModelResponse(response map[string]any) map[string]any {
54+
result, err := decodeCallToolResult(response)
55+
if err != nil {
56+
return response
57+
}
58+
59+
if result.IsError {
60+
// On error, keep the original content/meta so the model can
61+
// diagnose and recover; only drop the heavy structured payload.
62+
result.StructuredContent = nil
63+
return encodeCallToolResult(result, response)
64+
}
65+
66+
// On success, collapse the render payload into a terminal directive so the
67+
// model stops re-invoking the rendering tool. Preserve _meta (e.g.
68+
// resourceUri) in case downstream tooling relies on it.
69+
compact := &mcpsdk.CallToolResult{
70+
Meta: result.Meta,
71+
Content: []mcpsdk.Content{&mcpsdk.TextContent{Text: mcpAppRenderedNotice}},
72+
}
73+
return encodeCallToolResult(compact, response)
74+
}
75+
76+
// decodeCallToolResult interprets a generic model-facing response map as a typed
77+
// MCP CallToolResult.
78+
func decodeCallToolResult(response map[string]any) (*mcpsdk.CallToolResult, error) {
79+
raw, err := json.Marshal(response)
80+
if err != nil {
81+
return nil, err
82+
}
83+
var result mcpsdk.CallToolResult
84+
if err := json.Unmarshal(raw, &result); err != nil {
85+
return nil, err
86+
}
87+
return &result, nil
88+
}
89+
90+
// encodeCallToolResult converts a typed CallToolResult back into the generic map
91+
// the model expects, falling back to the original response if conversion fails.
92+
func encodeCallToolResult(result *mcpsdk.CallToolResult, fallback map[string]any) map[string]any {
93+
raw, err := json.Marshal(result)
94+
if err != nil {
95+
return fallback
96+
}
97+
var out map[string]any
98+
if err := json.Unmarshal(raw, &out); err != nil {
99+
return fallback
100+
}
101+
return out
102+
}

0 commit comments

Comments
 (0)