-
Notifications
You must be signed in to change notification settings - Fork 632
feat(MCP UI) chat widgets #2052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dimetron
wants to merge
8
commits into
kagent-dev:main
Choose a base branch
from
dimetron:feature/chat-mcp-ui-widgets
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+4,111
−155
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b2f3ba4
feat(adk,server): classify and serve MCP App (UI) tools
dimetron e3615a8
feat(adk): add Python MCP App tool classification and compaction
dimetron b0984bb
feat(ui): render MCP UI widgets (MCP Apps) inline in chat
dimetron 1831b16
docs(contrib): add server-everything MCP UI example
dimetron 4a77b06
Merge branch 'main' into feature/chat-mcp-ui-widgets
dimetron 6d41a5a
refactor(adk): move MCP App helpers to mcp_ui.go, align naming with spec
dimetron fe4862a
Merge branch 'main' into feature/chat-mcp-ui-widgets
dimetron 525ccba
Merge remote-tracking branch 'upstream/main' into feature/chat-mcp-ui…
dimetron File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| python/.env | ||
| .env | ||
| values.local.yaml | ||
| .playwright* | ||
|
|
||
| test_results | ||
|
|
||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Server Everything MCP | ||
|
|
||
| This directory wires the public [Server Everything](https://servereverything.dev/mcp) | ||
| reference MCP server into kagent as a `RemoteMCPServer`, plus a demo `Agent` that | ||
| exercises its tools — including the `show-weather-dashboard` **MCP App** (an | ||
| interactive UI widget rendered inline in the chat). | ||
|
|
||
| It is useful for verifying the chat MCP UI widget integration (EP-2046) against a | ||
| public server that uses a single dual-visibility (`["model", "app"]`) UI tool. | ||
|
|
||
| ## What it provides | ||
|
|
||
| `server-everything` exposes demo tools; this agent enables a focused subset: | ||
|
|
||
| | Tool | Purpose | | ||
| |------|---------| | ||
| | `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"]`. | | ||
| | `echo` | Reflects text back. | | ||
| | `get-sum` | Adds two numbers. | | ||
| | `get-tiny-image` | Returns a small sample image. | | ||
| | `get-structured-content` | Demonstrates structured tool output. | | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| kubectl apply -f server-everything-remote-mcpserver.yaml | ||
| kubectl apply -f server-everything-agent.yaml | ||
| ``` | ||
|
|
||
| This creates: | ||
| - a `RemoteMCPServer` named `server-everything` pointing at `https://servereverything.dev/mcp` | ||
| - an `Agent` named `server-everything-agent` that uses the tools above | ||
|
|
||
| No extra Helm values are needed — MCP App (UI widget) rendering is detected | ||
| automatically from each tool's `_meta.ui` metadata. | ||
|
|
||
| ## Verify | ||
|
|
||
| ```bash | ||
| # RemoteMCPServer should reach Accepted and discover tools | ||
| kubectl get remotemcpserver server-everything -n kagent -o yaml | ||
|
|
||
| # Agent should become Ready | ||
| kubectl get agent server-everything-agent -n kagent | ||
| ``` | ||
|
|
||
| Then open the agent in the kagent UI and ask: **"show the weather dashboard"**. | ||
| The dashboard renders inline and updates itself in place (it re-calls | ||
| `show-weather-dashboard` from inside the widget). | ||
|
|
||
| ## Learn More | ||
|
|
||
| - [Server Everything](https://servereverything.dev/mcp) | ||
| - [MCP Protocol](https://modelcontextprotocol.io/) | ||
| - MCP UI widgets in kagent: `design/EP-2046-chat-mcp-ui-widgets.md` |
48 changes: 48 additions & 0 deletions
48
contrib/tools/server-everything/server-everything-agent.yaml
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| apiVersion: kagent.dev/v1alpha2 | ||
| kind: Agent | ||
| metadata: | ||
| name: server-everything-agent | ||
| namespace: kagent | ||
| spec: | ||
| type: Declarative | ||
| description: Demo agent exercising Server Everything MCP tools, including the weather-dashboard MCP App (UI widget). | ||
| declarative: | ||
| modelConfig: default-model-config | ||
| runtime: go | ||
| stream: true | ||
| systemMessage: |- | ||
| You are a helpful assistant that demonstrates the Server Everything MCP tools, | ||
| including an interactive weather dashboard rendered as an inline UI widget. | ||
|
|
||
| # Weather (most important) | ||
| - Whenever the user asks ANYTHING about weather, forecast, temperature, or | ||
| conditions for a place, you MUST call the `show-weather-dashboard` tool. | ||
| Never answer weather questions from your own knowledge or with plain text. | ||
| - Pass the city as the `location` argument. For example, "weather in Paris" | ||
| -> call show-weather-dashboard with {"location": "Paris"}. If no city is | ||
| given, use {"location": "New York"}. | ||
| - The tool renders the dashboard inline and keeps itself updated, so after it | ||
| runs just give a one-line confirmation and let the widget show the data — | ||
| do not repeat the numbers yourself. | ||
|
|
||
| # Other tools | ||
| - `echo` reflects text back, `get-sum` adds two numbers, `get-tiny-image` | ||
| returns a sample image, and `get-structured-content` returns structured | ||
| output. | ||
| - Ask for clarification only when the request is genuinely ambiguous. | ||
| - If a tool fails, point the user to https://kagent.dev for support. | ||
|
|
||
| # Style | ||
| - Respond in Markdown and keep replies brief. | ||
| tools: | ||
| - type: McpServer | ||
| mcpServer: | ||
| apiGroup: kagent.dev | ||
| kind: RemoteMCPServer | ||
| name: server-everything | ||
| toolNames: | ||
| - show-weather-dashboard | ||
| - echo | ||
| - get-sum | ||
| - get-tiny-image | ||
| - get-structured-content |
15 changes: 15 additions & 0 deletions
15
contrib/tools/server-everything/server-everything-remote-mcpserver.yaml
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| ## Server Everything MCP (reference/demo MCP server) | ||
| ## https://servereverything.dev/mcp | ||
| ## Exposes a set of demo tools, including the `show-weather-dashboard` | ||
| ## MCP App (UI widget) advertised via tool `_meta.ui.resourceUri`. | ||
| apiVersion: kagent.dev/v1alpha2 | ||
| kind: RemoteMCPServer | ||
| metadata: | ||
| name: server-everything | ||
| namespace: kagent | ||
| spec: | ||
| url: "https://servereverything.dev/mcp" | ||
| protocol: STREAMABLE_HTTP | ||
| timeout: 30s | ||
| sseReadTimeout: 5m0s | ||
| description: "Server Everything reference MCP server with an interactive weather dashboard MCP App" |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| # EP-2046: Chat UI support for MCP UI widgets (MCP Apps) | ||
|
|
||
| * Issue: [#2046](https://github.com/kagent-dev/kagent/issues/2046) | ||
|
|
||
| ## Background | ||
|
|
||
| The Model Context Protocol is gaining an "Apps"/UI extension | ||
| (`@modelcontextprotocol/ext-apps`, rendered via `@mcp-ui/client`) that lets an MCP | ||
| server attach an interactive HTML/UI **resource** to a tool. When an agent calls | ||
| such a tool, the client can render a live widget instead of (or in addition to) raw | ||
| tool-call JSON. | ||
|
|
||
| kagent's chat today renders tool calls as collapsible JSON. This EP makes the chat | ||
| MCP-App–aware: when a tool call maps to an MCP app resource, the chat renders the | ||
| app inline in a sandboxed frame and brokers messages between the app and the chat | ||
| (send a message on the user's behalf, surface "visible" tool calls, proxy resource | ||
| reads and tool calls back to the originating MCP server). | ||
|
|
||
| ## Motivation | ||
|
|
||
| - Let MCP servers deliver rich, interactive results (forms, boards, charts, live | ||
| progress) directly in the kagent chat. | ||
| - Provide the in-chat rendering half of the kagent plugin story (the sidebar/plugin | ||
| half is EP-2047; the first consumer is the Kanban task-progress widget, EP-2048). | ||
|
|
||
| ### Goals | ||
|
|
||
| - Discover MCP app resources per MCP server and associate them with tool calls. | ||
| - Render the app via a sandboxed renderer inside chat messages / tool-call display. | ||
| - Broker host↔app messaging: `sendMessage`, visible tool calls, and proxying of | ||
| resource reads and tool calls to the backend MCP server. | ||
| - Backend endpoints to list an MCP server's tools, read its resources, and call its | ||
| tools on behalf of the UI. | ||
|
|
||
| ### Non-Goals | ||
|
|
||
| - The sidebar plugin/registration mechanism (EP-2047). | ||
| - Shipping a specific MCP app (the Kanban task-progress app is EP-2048). | ||
| - File-upload / artifact handling — note the chat files carry adjacent | ||
| file-upload/minimap code (see "Adjacent code" below); that feature is tracked | ||
| separately and is **not** part of this EP's scope. | ||
|
|
||
| ## Implementation Details | ||
|
|
||
| ### Backend | ||
|
|
||
| - **`go/adk/pkg/mcp/registry.go`** — `CreateToolsets` now also returns the set of | ||
| **MCP-app–capable tool names** (tools whose MCP server advertises a UI resource), | ||
| so the agent can treat their results specially. | ||
| - **`go/adk/pkg/agent/mcp_apps.go`** — `MakeMCPAppModelResultCallback`: for | ||
| MCP-app tools, keep the rich tool payload in chat history for UI rendering while | ||
| compacting what is sent back to the model (avoids redundant polling/tool churn). | ||
| Wired in `agent.go` only when `len(mcpAppToolNames) > 0`. | ||
| - **`go/core/internal/httpserver/handlers/mcpapps.go`** — `MCPAppsHandler` with | ||
| `HandleListTools`, `HandleCallTool`, `HandleReadResource`, exposed under | ||
| `/api/mcp-apps/{namespace}/{name}/...`. (Only the MCP-apps hunks of the shared | ||
| `server.go`/`handlers.go` are included here; the plugins hunks belong to EP-2047.) | ||
|
|
||
| ### UI (`ui/src`) | ||
|
|
||
| - **`components/mcp-apps/McpAppRenderer.tsx`** — renders an MCP app resource via | ||
| `@mcp-ui/client` in a sandbox, wiring its `onUIAction`/resource-read/tool-call | ||
| callbacks to the backend; `McpAppsInspector.tsx` is a standalone inspector view | ||
| (also surfaced at `app/servers/[namespace]/[name]/apps/page.tsx`, and reachable | ||
| from an **"MCP Apps"** entry added to the per-server menu in | ||
| `components/mcp/McpServersView.tsx`). | ||
| - **`components/chat/ChatMcpAppsContext.tsx`** — context that maps a tool name to its | ||
| MCP app (`getMcpAppForTool`) and brokers `sendMessage` / `McpAppVisibleToolCall` | ||
| between an app and the chat. | ||
| - **`components/chat/ChatLayoutUI.tsx`** — mounts `ChatMcpAppsProvider` around the | ||
| chat subtree so the MCP-app context is active for every chat session (without this | ||
| mount, tool calls never resolve to apps and no widget renders). | ||
| - **`components/chat/ChatInterface.tsx`, `ChatMessage.tsx`, `ToolCallDisplay.tsx`, | ||
| `components/ToolDisplay.tsx`** — render the app for MCP-app tool calls and forward | ||
| app actions. | ||
| - **`app/actions/mcp-apps.ts`** + **`app/api/mcp-apps/.../{resources,tools/.../call}`** | ||
| — server actions / BFF routes calling the backend MCP-apps endpoints. | ||
| - **`public/sandbox_proxy.html`** — sandbox proxy document for the app iframe. | ||
|
|
||
| ### New dependencies (`ui/package.json`) | ||
|
|
||
| - `@mcp-ui/client` `^7.1.1` | ||
| - `@modelcontextprotocol/ext-apps` `^1.7.1` | ||
| - `@modelcontextprotocol/sdk` `^1.29.0` | ||
|
|
||
| The lockfile (`ui/package-lock.json`) and the generated `ui/public/mockServiceWorker.js` | ||
| (MSW worker, bumped `2.14.2` → `2.14.6`) are regenerated as a side effect of resolving | ||
| the new dependency tree. | ||
|
|
||
| ### Adjacent code | ||
|
|
||
| Per the agreed split, the chat files (`ChatInterface.tsx`, `ChatMessage.tsx`, | ||
| `messageHandlers.ts`) are taken whole and therefore also carry the chat | ||
| **file-upload** (`lib/fileUpload.ts`, `chat/FileAttachment.tsx`) and **minimap** | ||
| (`chat/ChatMinimap.tsx`) UI that was developed alongside MCP apps. These are | ||
| included so the chat compiles, but are not the subject of this EP; the file-upload | ||
| backend (artifact extraction, `save_artifact`) is intentionally **excluded**. | ||
|
|
||
| ## Test Plan | ||
|
|
||
| - **Unit (Go):** `registry_test.go` (MCP-app tool-name detection) and | ||
| `mcp_apps_test.go` (model-result callback). `go build ./adk/... ./core/...` and | ||
| test compilation pass. | ||
| - **Unit (UI):** `getMcpAppForTool` mapping (`ChatMcpAppsContext.test.tsx`); mcp-apps | ||
| server actions (`actions/__tests__/mcp-apps.test.ts`); and a regression test | ||
| (`chat/__tests__/ChatLayoutUI.test.tsx`) asserting `ChatLayoutUI` mounts | ||
| `ChatMcpAppsProvider` around the chat so widgets can render. | ||
| - **Manual / e2e:** point the chat at an MCP server exposing a UI resource; confirm | ||
| the widget renders inline, `sendMessage` posts to the chat, and resource/tool-call | ||
| proxying reaches the server. The Kanban task-progress widget (EP-2048) is the | ||
| reference end-to-end case. | ||
|
|
||
| ## Alternatives | ||
|
|
||
| - **Render apps only in a side panel (not inline in chat):** loses the | ||
| tool-call→widget association and the conversational flow. | ||
| - **Trust the model with full tool payloads:** causes token bloat and tool churn; | ||
| hence the model-result compaction callback. | ||
|
|
||
| ## Open Questions | ||
|
|
||
| - Should MCP-app rendering be opt-in per MCP server (a `spec` flag) rather than | ||
| inferred from advertised UI resources? | ||
| - How should multiple apps in a single conversation share/scope state? |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package agent | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
|
|
||
| "github.com/kagent-dev/kagent/go/adk/pkg/mcp" | ||
| mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| "google.golang.org/adk/agent" | ||
| "google.golang.org/adk/agent/llmagent" | ||
| adkmodel "google.golang.org/adk/model" | ||
| ) | ||
|
|
||
| // mcpAppRenderedNotice is the terminal message the model sees in place of an | ||
| // MCP App tool's render payload. An MCP App tool (one that declares a UI | ||
| // resourceUri) produces an interactive view that is displayed to the user and | ||
| // refreshes itself in-place via its own app-only tool calls. The model must | ||
| // treat a successful render as a completed, self-updating artifact; otherwise it | ||
| // tends to re-invoke the rendering tool on every "refresh", flooding the chat | ||
| // with duplicate app cards. This notice is protocol-oriented: it applies to any | ||
| // tool carrying a UI resourceUri, independent of the tool's name or payload keys. | ||
| 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." | ||
|
|
||
| // MakeMCPAppModelResultCallback replaces what the model sees for MCP App | ||
| // (UI-rendering) tool results: instead of the heavy render payload it receives a | ||
| // terminal directive (see mcpAppRenderedNotice). The full result is still | ||
| // streamed to the UI separately, so this only changes the model's view and | ||
| // prevents the model from looping on the rendering tool. Errors are passed | ||
| // through so the model can still react to and recover from failures. | ||
| func MakeMCPAppModelResultCallback(appToolNames mcp.MCPAppToolNames) llmagent.BeforeModelCallback { | ||
| return func(_ agent.CallbackContext, req *adkmodel.LLMRequest) (*adkmodel.LLMResponse, error) { | ||
| for _, content := range req.Contents { | ||
| if content == nil { | ||
| continue | ||
| } | ||
| for _, part := range content.Parts { | ||
| if part == nil || part.FunctionResponse == nil || !appToolNames[part.FunctionResponse.Name] { | ||
| continue | ||
| } | ||
| part.FunctionResponse.Response = compactMCPAppModelResponse(part.FunctionResponse.Response) | ||
| } | ||
| } | ||
| return nil, nil | ||
| } | ||
| } | ||
|
|
||
| // compactMCPAppModelResponse rewrites an MCP App tool result for the model. | ||
| // | ||
| // The model exchanges tool results as a generic map (genai | ||
| // FunctionResponse.Response), but the payload is really an MCP | ||
| // [mcpsdk.CallToolResult]. We decode it into that typed result so the logic | ||
| // works against real fields (IsError, Content, Meta, StructuredContent) rather | ||
| // than poking at string keys. If the payload isn't a recognizable MCP result we | ||
| // leave it untouched. | ||
| func compactMCPAppModelResponse(response map[string]any) map[string]any { | ||
| result, err := decodeCallToolResult(response) | ||
| if err != nil { | ||
| return response | ||
| } | ||
|
|
||
| if result.IsError { | ||
| // On error, keep the original content/meta so the model can | ||
| // diagnose and recover; only drop the heavy structured payload. | ||
| result.StructuredContent = nil | ||
| return encodeCallToolResult(result, response) | ||
| } | ||
|
|
||
| // On success, collapse the render payload into a terminal directive so the | ||
| // model stops re-invoking the rendering tool. Preserve _meta (e.g. | ||
| // resourceUri) in case downstream tooling relies on it. | ||
| compact := &mcpsdk.CallToolResult{ | ||
| Meta: result.Meta, | ||
| Content: []mcpsdk.Content{&mcpsdk.TextContent{Text: mcpAppRenderedNotice}}, | ||
| } | ||
| return encodeCallToolResult(compact, response) | ||
| } | ||
|
|
||
| // decodeCallToolResult interprets a generic model-facing response map as a typed | ||
| // MCP CallToolResult. | ||
| func decodeCallToolResult(response map[string]any) (*mcpsdk.CallToolResult, error) { | ||
| raw, err := json.Marshal(response) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var result mcpsdk.CallToolResult | ||
| if err := json.Unmarshal(raw, &result); err != nil { | ||
| return nil, err | ||
| } | ||
| return &result, nil | ||
| } | ||
|
|
||
| // encodeCallToolResult converts a typed CallToolResult back into the generic map | ||
| // the model expects, falling back to the original response if conversion fails. | ||
| func encodeCallToolResult(result *mcpsdk.CallToolResult, fallback map[string]any) map[string]any { | ||
| raw, err := json.Marshal(result) | ||
| if err != nil { | ||
| return fallback | ||
| } | ||
| var out map[string]any | ||
| if err := json.Unmarshal(raw, &out); err != nil { | ||
| return fallback | ||
| } | ||
| return out | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.