Skip to content

Commit 4bc1353

Browse files
committed
fix: address chat MCP UI widgets PR review (#2046)
- Use the typed MCP CallToolResult (modelcontextprotocol/go-sdk) instead of hand-built map[string]any when compacting MCP App tool results for the model (peterj review). - Remove the accidentally committed go/build.err build-log artifact. - ChatMinimap: add a type-only React import so React.RefObject/React.PointerEvent resolve (fixes "Cannot find namespace 'React'"). - sandbox_proxy.html: restrict postMessage to the expected parent origin (passed via ?parentOrigin from McpAppRenderer) instead of accepting/posting to "*", preventing arbitrary HTML/script injection from other origins. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
1 parent 1c2fab7 commit 4bc1353

5 files changed

Lines changed: 68 additions & 61 deletions

File tree

go/adk/pkg/agent/mcp_apps.go

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package agent
22

33
import (
4+
"encoding/json"
5+
6+
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
47
"google.golang.org/adk/agent"
58
"google.golang.org/adk/agent/llmagent"
69
adkmodel "google.golang.org/adk/model"
@@ -39,34 +42,61 @@ func MakeMCPAppModelResultCallback(appToolNames map[string]bool) llmagent.Before
3942
}
4043
}
4144

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.
4253
func compactMCPAppModelResponse(response map[string]any) map[string]any {
43-
// On error, keep the original content so the model can diagnose/recover.
44-
if isErr, ok := response["isError"].(bool); ok && isErr {
45-
out := map[string]any{"isError": true}
46-
if content, ok := response["content"]; ok {
47-
out["content"] = content
48-
}
49-
if meta, ok := response["_meta"]; ok {
50-
out["_meta"] = meta
51-
}
52-
return out
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)
5364
}
5465

5566
// On success, collapse the render payload into a terminal directive so the
56-
// model stops re-invoking the rendering tool.
57-
out := map[string]any{
58-
"content": []any{
59-
map[string]any{
60-
"type": "text",
61-
"text": mcpAppRenderedNotice,
62-
},
63-
},
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}},
6472
}
73+
return encodeCallToolResult(compact, response)
74+
}
6575

66-
// Preserve _meta (e.g. resourceUri) in case downstream tooling relies on it.
67-
if meta, ok := response["_meta"]; ok {
68-
out["_meta"] = meta
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
6986
}
87+
return &result, nil
88+
}
7089

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+
}
71101
return out
72102
}

go/build.err

Lines changed: 0 additions & 38 deletions
This file was deleted.

ui/public/sandbox_proxy.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@
1515
</head>
1616
<body>
1717
<script>
18+
// The host page passes its origin via ?parentOrigin=...; fall back to this
19+
// document's own origin (the proxy is served from the host's origin). We
20+
// never trust "*" — only messages from (and posts to) the expected parent.
21+
var params = new URLSearchParams(window.location.search);
22+
var parentOrigin = params.get("parentOrigin") || window.location.origin;
23+
1824
window.addEventListener("message", (event) => {
25+
// Reject messages from any origin other than the expected parent so a
26+
// page embedding this iframe cannot inject arbitrary HTML/script.
27+
if (event.origin !== parentOrigin) return;
28+
1929
const data = event.data;
2030
if (!data || typeof data !== "object") return;
2131

@@ -32,7 +42,7 @@
3242
window.parent.postMessage({
3343
method: "ui/notifications/sandbox-proxy-ready",
3444
params: {}
35-
}, "*");
45+
}, parentOrigin);
3646
</script>
3747
</body>
3848
</html>

ui/src/components/chat/ChatMinimap.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import type React from "react";
34
import { useCallback, useEffect, useRef, useState } from "react";
45
import { cn } from "@/lib/utils";
56

ui/src/components/mcp-apps/McpAppRenderer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ export function McpAppRenderer({
8888

8989
useEffect(() => {
9090
const id = requestAnimationFrame(() => {
91-
setSandboxUrl(new URL("/sandbox_proxy.html", window.location.origin));
91+
// Tell the sandbox proxy which origin to trust for postMessage, so it can
92+
// reject messages from any other origin instead of accepting "*".
93+
const url = new URL("/sandbox_proxy.html", window.location.origin);
94+
url.searchParams.set("parentOrigin", window.location.origin);
95+
setSandboxUrl(url);
9296
});
9397
return () => cancelAnimationFrame(id);
9498
}, []);

0 commit comments

Comments
 (0)