Skip to content

Commit 4371099

Browse files
committed
refactor(mcp): clarify MCP App tool classification (#2046)
Address review feedback on the MCP UI widgets toolset code: - Name the previously untyped map[string]bool returned by CreateToolsets as MCPAppToolNames, with a doc comment describing what it stores (peterj). - Replace the isMCPAppOnlyTool/isMCPAppTool booleans with a single mcpToolKind classifier (agent / app / app_only) so call sites read clearly and new tool kinds can be added later without reworking signatures (peterj). Behavior is unchanged; registry tests updated to cover mcpToolKindOf. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
1 parent 4bc1353 commit 4371099

2 files changed

Lines changed: 99 additions & 74 deletions

File tree

go/adk/pkg/mcp/registry.go

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,18 @@ type mcpServerParams struct {
7878
TLSDisableSystemCAs *bool
7979
}
8080

81+
// MCPAppToolNames is the set of MCP tool names whose results render as
82+
// interactive MCP App (UI) widgets in the chat (the tool declares a
83+
// ui.resourceUri and is visible to the agent). It is used as a set, so the bool
84+
// value is always true and only key presence is meaningful. The agent attaches
85+
// the model-result compaction callback (see agent.MakeMCPAppModelResultCallback)
86+
// only to these tools.
87+
type MCPAppToolNames map[string]bool
88+
8189
// CreateToolsets creates toolsets from all configured HTTP and SSE MCP servers,
82-
// returning the accumulated toolsets. Errors on individual servers are logged
83-
// and skipped.
90+
// returning the accumulated toolsets and the set of agent-visible tools that
91+
// render as MCP App widgets (see MCPAppToolNames). Errors on individual servers
92+
// are logged and skipped.
8493
//
8594
// When propagateToken is true, Authorization is forwarded to every MCP server
8695
// independently of AllowedHeaders, mirroring the Python ADKTokenPropagationPlugin
@@ -94,10 +103,10 @@ func CreateToolsets(
94103
sseTools []adk.SseMcpServerConfig,
95104
propagateToken bool,
96105
headerProvider DynamicHeaderProvider,
97-
) ([]tool.Toolset, map[string]bool) {
106+
) ([]tool.Toolset, MCPAppToolNames) {
98107
log := logr.FromContextOrDiscard(ctx)
99108
var toolsets []tool.Toolset
100-
appToolNames := make(map[string]bool)
109+
appToolNames := make(MCPAppToolNames)
101110

102111
log.Info("Processing HTTP MCP tools", "httpToolsCount", len(httpTools))
103112
for i, httpTool := range httpTools {
@@ -153,7 +162,7 @@ func CreateToolsets(
153162
}
154163

155164
// addToolset logs, initializes, and returns a single MCP toolset.
156-
func addToolset(ctx context.Context, log logr.Logger, params mcpServerParams, tools []string, label string, index int) (tool.Toolset, map[string]bool, error) {
165+
func addToolset(ctx context.Context, log logr.Logger, params mcpServerParams, tools []string, label string, index int) (tool.Toolset, MCPAppToolNames, error) {
157166
if params.Headers == nil {
158167
params.Headers = make(map[string]string)
159168
}
@@ -263,10 +272,53 @@ func createTransport(ctx context.Context, params mcpServerParams) (mcpsdk.Transp
263272
return mcpTransport, nil
264273
}
265274

266-
// isMCPAppOnlyTool reports whether a tool is callable only from the rendered
267-
// MCP app (hidden from the agent). A tool is app-only when its visibility
268-
// declares "app" but not "model". Absent visibility defaults to agent-visible.
269-
func isMCPAppOnlyTool(meta map[string]any) bool {
275+
// mcpToolKind classifies how an MCP tool is surfaced to the agent and the UI,
276+
// derived from the tool's MCP-UI metadata (the "ui" block). Modeling this as a
277+
// single kind rather than a set of overlapping booleans keeps the call sites
278+
// readable and leaves room for additional kinds (e.g. new tool surfaces) without
279+
// reworking signatures.
280+
type mcpToolKind int
281+
282+
const (
283+
// mcpToolKindAgent is a regular tool exposed to the agent/model with no
284+
// interactive UI rendering.
285+
mcpToolKindAgent mcpToolKind = iota
286+
// mcpToolKindApp is an agent-visible tool whose result renders as an
287+
// interactive MCP App (UI) widget in the chat (declares a ui.resourceUri).
288+
mcpToolKindApp
289+
// mcpToolKindAppOnly is hidden from the agent and only callable from within
290+
// the rendered MCP App (visibility declares "app" but not "model").
291+
mcpToolKindAppOnly
292+
)
293+
294+
func (k mcpToolKind) String() string {
295+
switch k {
296+
case mcpToolKindApp:
297+
return "app"
298+
case mcpToolKindAppOnly:
299+
return "app_only"
300+
default:
301+
return "agent"
302+
}
303+
}
304+
305+
// mcpToolKindOf classifies a tool from its MCP metadata. App-only takes
306+
// precedence: a tool hidden from the model is never surfaced to the agent even
307+
// if it also declares a UI resource.
308+
func mcpToolKindOf(meta map[string]any) mcpToolKind {
309+
if isAppOnlyVisibility(meta) {
310+
return mcpToolKindAppOnly
311+
}
312+
if hasUIResource(meta) {
313+
return mcpToolKindApp
314+
}
315+
return mcpToolKindAgent
316+
}
317+
318+
// isAppOnlyVisibility reports whether a tool's visibility hides it from the
319+
// agent: it declares "app" but not "model". Absent visibility defaults to
320+
// agent-visible.
321+
func isAppOnlyVisibility(meta map[string]any) bool {
270322
ui, _ := meta["ui"].(map[string]any)
271323
if ui == nil {
272324
return false
@@ -283,9 +335,9 @@ func isMCPAppOnlyTool(meta map[string]any) bool {
283335
return hasApp
284336
}
285337

286-
// isMCPAppTool reports whether a tool declares an MCP-UI resourceUri,
287-
// meaning its result should be rendered as an interactive app in the chat UI.
288-
func isMCPAppTool(meta map[string]any) bool {
338+
// hasUIResource reports whether a tool declares an MCP-UI resourceUri, meaning
339+
// its result should be rendered as an interactive app in the chat UI.
340+
func hasUIResource(meta map[string]any) bool {
289341
if ui, ok := meta["ui"].(map[string]any); ok {
290342
if uri, _ := ui["resourceUri"].(string); uri != "" {
291343
return true
@@ -318,7 +370,7 @@ func normalizeVisibility(value any) []string {
318370
// agentVisibleToolFilter lists tools from the MCP server, filters out app-only
319371
// tools and any not in the configured allow-list, and returns a predicate the
320372
// toolset can apply plus the set of remaining tools that render as MCP apps.
321-
func agentVisibleToolFilter(ctx context.Context, params mcpServerParams, configuredFilter map[string]bool) (tool.Predicate, map[string]bool, error) {
373+
func agentVisibleToolFilter(ctx context.Context, params mcpServerParams, configuredFilter map[string]bool) (tool.Predicate, MCPAppToolNames, error) {
322374
mcpTransport, err := createTransport(ctx, params)
323375
if err != nil {
324376
return nil, nil, fmt.Errorf("failed to create transport for %s: %w", params.URL, err)
@@ -337,20 +389,23 @@ func agentVisibleToolFilter(ctx context.Context, params mcpServerParams, configu
337389
}
338390

339391
allowedTools := make([]string, 0, len(result.Tools))
340-
appToolNames := make(map[string]bool)
392+
appToolNames := make(MCPAppToolNames)
341393
for _, t := range result.Tools {
342394
if t == nil || t.Name == "" {
343395
continue
344396
}
345397
if len(configuredFilter) > 0 && !configuredFilter[t.Name] {
346398
continue
347399
}
348-
if isMCPAppOnlyTool(t.Meta) {
400+
switch mcpToolKindOf(t.Meta) {
401+
case mcpToolKindAppOnly:
402+
// Hidden from the agent; only the rendered MCP App calls it.
349403
continue
350-
}
351-
allowedTools = append(allowedTools, t.Name)
352-
if isMCPAppTool(t.Meta) {
404+
case mcpToolKindApp:
405+
allowedTools = append(allowedTools, t.Name)
353406
appToolNames[t.Name] = true
407+
default: // mcpToolKindAgent
408+
allowedTools = append(allowedTools, t.Name)
354409
}
355410
}
356411

@@ -411,7 +466,7 @@ func (rt *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
411466

412467
// initializeToolSet fetches tools from an MCP server using Google ADK's mcptoolset.
413468
// Returns the created toolset on success.
414-
func initializeToolSet(ctx context.Context, params mcpServerParams, toolFilter map[string]bool) (tool.Toolset, map[string]bool, error) {
469+
func initializeToolSet(ctx context.Context, params mcpServerParams, toolFilter map[string]bool) (tool.Toolset, MCPAppToolNames, error) {
415470
mcpTransport, err := createTransport(ctx, params)
416471
if err != nil {
417472
return nil, nil, fmt.Errorf("failed to create transport for %s: %w", params.URL, err)

go/adk/pkg/mcp/registry_test.go

Lines changed: 25 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -177,96 +177,66 @@ func TestAllowedRequestHeaders_EmptyAllowedList(t *testing.T) {
177177
}
178178
}
179179

180-
func TestIsMCPAppOnlyTool(t *testing.T) {
180+
func TestMCPToolKindOf(t *testing.T) {
181181
t.Parallel()
182182

183183
tests := []struct {
184184
name string
185185
meta map[string]any
186-
want bool
186+
want mcpToolKind
187187
}{
188188
{
189-
name: "app visibility list",
189+
name: "app visibility list is app-only",
190190
meta: map[string]any{"ui": map[string]any{"visibility": []any{"app"}}},
191-
want: true,
191+
want: mcpToolKindAppOnly,
192192
},
193193
{
194-
name: "app visibility string",
194+
name: "app visibility string is app-only",
195195
meta: map[string]any{"ui": map[string]any{"visibility": "app"}},
196-
want: true,
196+
want: mcpToolKindAppOnly,
197197
},
198198
{
199-
name: "model and app visibility list - not app-only",
200-
meta: map[string]any{"ui": map[string]any{"visibility": []any{"model", "app"}}},
201-
want: false,
202-
},
203-
{
204-
name: "app and model visibility list - not app-only",
205-
meta: map[string]any{"ui": map[string]any{"visibility": []any{"app", "model"}}},
206-
want: false,
199+
name: "app-only wins over a declared resource uri",
200+
meta: map[string]any{"ui": map[string]any{"visibility": []any{"app"}, "resourceUri": "ui://forms/form.html"}},
201+
want: mcpToolKindAppOnly,
207202
},
208203
{
209-
name: "model only visibility - not app-only",
210-
meta: map[string]any{"ui": map[string]any{"visibility": []any{"model"}}},
211-
want: false,
204+
name: "model and app visibility without resource is a plain agent tool",
205+
meta: map[string]any{"ui": map[string]any{"visibility": []any{"model", "app"}}},
206+
want: mcpToolKindAgent,
212207
},
213208
{
214-
name: "assistant visible app tool",
215-
meta: map[string]any{"ui": map[string]any{"resourceUri": "ui://forms/form.html"}},
216-
want: false,
209+
name: "model and app visibility with resource renders as app",
210+
meta: map[string]any{"ui": map[string]any{"visibility": []any{"app", "model"}, "resourceUri": "ui://forms/form.html"}},
211+
want: mcpToolKindApp,
217212
},
218213
{
219-
name: "plain tool",
220-
meta: map[string]any{},
221-
want: false,
214+
name: "model only visibility is a plain agent tool",
215+
meta: map[string]any{"ui": map[string]any{"visibility": []any{"model"}}},
216+
want: mcpToolKindAgent,
222217
},
223-
}
224-
225-
for _, tt := range tests {
226-
t.Run(tt.name, func(t *testing.T) {
227-
t.Parallel()
228-
if got := isMCPAppOnlyTool(tt.meta); got != tt.want {
229-
t.Fatalf("isMCPAppOnlyTool() = %v, want %v", got, tt.want)
230-
}
231-
})
232-
}
233-
}
234-
235-
func TestIsMCPAppTool(t *testing.T) {
236-
t.Parallel()
237-
238-
tests := []struct {
239-
name string
240-
meta map[string]any
241-
want bool
242-
}{
243218
{
244-
name: "resource uri in ui object",
219+
name: "resource uri in ui object renders as app",
245220
meta: map[string]any{"ui": map[string]any{"resourceUri": "ui://forms/form.html"}},
246-
want: true,
221+
want: mcpToolKindApp,
247222
},
248223
{
249-
name: "legacy resource uri key",
224+
name: "legacy resource uri key renders as app",
250225
meta: map[string]any{"ui/resourceUri": "ui://forms/form.html"},
251-
want: true,
252-
},
253-
{
254-
name: "app only without resource uri",
255-
meta: map[string]any{"ui": map[string]any{"visibility": []any{"app"}}},
256-
want: false,
226+
want: mcpToolKindApp,
257227
},
258228
{
259229
name: "plain tool",
260230
meta: map[string]any{},
261-
want: false,
231+
want: mcpToolKindAgent,
262232
},
263233
}
264234

265235
for _, tt := range tests {
266236
t.Run(tt.name, func(t *testing.T) {
267237
t.Parallel()
268-
if got := isMCPAppTool(tt.meta); got != tt.want {
269-
t.Fatalf("isMCPAppTool() = %v, want %v", got, tt.want)
238+
if got := mcpToolKindOf(tt.meta); got != tt.want {
239+
t.Fatalf("mcpToolKindOf() = %v, want %v", got, tt.want)
270240
}
271241
})
272242
}

0 commit comments

Comments
 (0)