feat(apps): Pass toolInfo to MCP Apps via hostContext#7506
Conversation
c878f0d to
d622225
Compare
There was a problem hiding this comment.
Pull request overview
Adds MCP Apps spec support for exposing the triggering tool definition to apps via hostContext.toolInfo, wiring the data end-to-end from the Rust /agent/tools endpoint through the desktop renderer.
Changes:
- Backend: extend
ToolInfowithinput_schemaand populate it in the/agent/toolshandler. - Frontend: plumb
toolNameintoMcpAppRenderer, fetch the tool definition viagetTools, and sethostContext.toolInfowhen available. - Generated: update OpenAPI + generated TS client types to include
input_schema.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/components/ToolCallWithResponse.tsx | Extracts toolName from the prefixed tool call name and passes it to the app renderer. |
| ui/desktop/src/components/McpApps/McpAppRenderer.tsx | Fetches the tool definition and injects it into hostContext.toolInfo for MCP Apps. |
| ui/desktop/src/api/types.gen.ts | Updates generated API types to include ToolInfo.input_schema. |
| ui/desktop/openapi.json | Updates OpenAPI schema for ToolInfo to include input_schema. |
| crates/goose/src/agents/extension.rs | Extends backend ToolInfo model with optional input_schema and a builder helper. |
| crates/goose-server/src/routes/agent.rs | Populates input_schema when returning tools from /agent/tools. |
| @@ -506,6 +507,7 @@ async fn get_tools( | |||
| get_parameter_names(&tool), | |||
| permission, | |||
| ) | |||
| .with_input_schema(input_schema.unwrap_or_default()) | |||
There was a problem hiding this comment.
get_tools converts tool.input_schema via serde_json::to_value(...).ok().unwrap_or_default(), which adds unnecessary serialization work and silently falls back to null on the (unlikely) error path; since input_schema is already a JSON object, prefer constructing a serde_json::Value::Object(...) directly (or propagate the error / omit the field instead of defaulting).
| const response = await getTools({ | ||
| query: { session_id: sessionId, extension_name: extensionName || undefined }, | ||
| }); | ||
| if (cancelled || !response.data) return; | ||
|
|
||
| const prefixedName = extensionName ? `${extensionName}__${toolName}` : toolName; | ||
| const match = response.data.find((t) => t.name === prefixedName); | ||
| if (match) { | ||
| const tool: Tool = { | ||
| name: toolName, | ||
| description: match.description || undefined, | ||
| inputSchema: (match.input_schema as Tool['inputSchema']) ?? { type: 'object' as const }, | ||
| }; |
There was a problem hiding this comment.
Fetching /agent/tools on every McpAppRenderer mount pulls the full tool list even though only a single tool definition is needed, which can become an N+1 request pattern when multiple apps render in a session; consider caching the tools list per (sessionId, extensionName) in the UI layer or adding a server endpoint to fetch a single tool by name.
There was a problem hiding this comment.
Hi @aharvard this seems to be a valid comment. Would it be helpful if we can cache the tool list?
There was a problem hiding this comment.
Sure, we can cache! Will work on that.
Implement the toolInfo field in McpUiHostContext per the MCP Apps spec (ext-apps 2026-01-26). MCP Apps can now access the tool definition (name, description, inputSchema) that triggered their instantiation. Backend: - Add input_schema field to ToolInfo struct - Populate it from rmcp::model::Tool in the get_tools handler Frontend: - Add toolName prop to McpAppRenderer - Fetch tool definition from /agent/tools and construct MCP SDK Tool - Populate hostContext.toolInfo when available - Extract and pass toolName from McpAppWrapper
d622225 to
8870afc
Compare
|
Addressing Copilot review feedback: Comment 1 (serde_json::to_value round-trip): Good catch — Comment 2 (N+1 fetch pattern): The fetch is already scoped by |
| if (cancelled || !response.data) return; | ||
|
|
||
| const prefixedName = extensionName ? `${extensionName}__${toolName}` : toolName; | ||
| const match = response.data.find((t) => t.name === prefixedName); |
There was a problem hiding this comment.
I'm a bit confused by this. Only a single tool is relevant?
There was a problem hiding this comment.
yeah. the goal is to send metadata of the tool call that instantiated the view. it's a one-to-one relationship.
- Resolve merge conflicts in McpAppRenderer.tsx (imports, useMemo deps) - Add toolsCache.ts module to deduplicate /agent/tools fetches across multiple McpAppRenderer instances in the same session (N+1 fix) - Keep mcpTool in hostContext useMemo deps from PR branch - Adopt main's display mode changes (activeDisplayMode, effectiveDisplayModes, etc.) - Drop appToolResult (removed on main; toolResult passed directly to AppRenderer) Co-authored-by: Goose <[email protected]> Ai-assisted: true
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e13411599e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const tools = await getCachedTools(sessionId, extensionName || undefined); | ||
| if (cancelled || !tools) return; | ||
|
|
||
| const prefixedName = extensionName ? `${extensionName}__${toolName}` : toolName; | ||
| const match = tools.find((t) => t.name === prefixedName); |
There was a problem hiding this comment.
Retry tool lookups when the extension is still loading
/agent/tools is not a safe one-shot lookup here. ExtensionManager::fetch_all_tools turns per-extension list_tools errors into an empty list instead of failing (crates/goose/src/agents/extension_manager.rs:982-990), and this component already documents transient extension startup failures in the resource-fetch effect below. If the first tools request lands during that window, match never resolves and this effect never retries, so the app can render permanently without hostContext.toolInfo even though the resource itself eventually loads.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not a practical concern. McpAppRenderer only mounts inside ToolCallWithResponse — i.e., after the agent has already called the tool and received a result with a resourceUri. For that to happen, the extension must have been fully loaded and its tools listed. So by the time this useEffect fires, the extension's tools are guaranteed to be available from /agent/tools.
The only other mount point (StandaloneAppView) doesn't pass toolName, so the effect early-returns and mcpTool stays null — which is correct since standalone apps aren't tool invocations.
The race condition described here can't happen in the actual call path.
| const key = cacheKey(sessionId, extensionName); | ||
| const existing = cache.get(key); | ||
| if (existing) return existing; |
There was a problem hiding this comment.
Invalidate cached tool definitions after session extension changes
This cache is keyed only by (sessionId, extensionName) and is reused forever, but the desktop UI can add/remove extensions on a live session (ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx:133-149). Because clearToolsCache is never called anywhere in ui/desktop/src, re-enabling or reconfiguring an extension in the same chat will keep serving the old /agent/tools snapshot, so later apps can receive a stale inputSchema or no toolInfo at all for newly added tools.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
True that clearToolsCache is exported but not wired up — but low impact in practice:
- The cache is keyed by
(sessionId, extensionName). Adding a different extension uses a different key — no staleness. - Removing and re-adding the same extension serves the cached tools list, but tool definitions (name, description, inputSchema) for a given extension don't change between disable/enable cycles.
toolInfois optional per spec — worst case the app gets a slightly stale schema, not a crash.
Not worth adding the wiring complexity for this edge case right now. Could revisit if extensions start having mutable schemas mid-session.
@lifeizhou-ap, done, PTAL. Thanks! |
* origin/main: (62 commits) Tweak the release process: no more merge to main (#7994) fix: gemini models via databricks (#8042) feat(apps): Pass toolInfo to MCP Apps via hostContext (#7506) fix: remove configured marker when deleting oauth provider configuration (#7887) docs: add vmware-aiops MCP extension documentation (#8055) Show setup instructions for ACP providers in settings modal (#8065) deps: replace sigstore-verification with sigstore-verify to kill vulns (#8064) feat(acp): add session/set_config and stabilize list, delete and close (#7984) docs: Correct `gosoe` typo to `goose` (#8062) fix: use default provider and model when provider in session no longer exists (#8035) feat: add GOOSE_SHELL env var to configure preferred shell (#7909) fix(desktop): fullscreen header bar + always-visible close controls (#8033) docs: add Claude Code approve mode permission routing documentation (#7949) chatgpt_codex: Support reasoning and gpt-5.4 (#7941) refactor(anthropic): fix N+1 thinking message storage issue (#7958) fix: handle mid-stream error events in OpenAI SSE streaming (#8031) Fix apps extension: coerce string arguments from inner LLM responses (#8030) feat: ability to expand sidebar to see chats names (#7816) Fix config for GOOSE_MAX_BACKGROUND_TASKS (#7940) set MACOSX_DEPLOYMENT_TARGET=12.0 (#7947) ...
…pstream * wpfleger/socket-support: (62 commits) Tweak the release process: no more merge to main (#7994) fix: gemini models via databricks (#8042) feat(apps): Pass toolInfo to MCP Apps via hostContext (#7506) fix: remove configured marker when deleting oauth provider configuration (#7887) docs: add vmware-aiops MCP extension documentation (#8055) Show setup instructions for ACP providers in settings modal (#8065) deps: replace sigstore-verification with sigstore-verify to kill vulns (#8064) feat(acp): add session/set_config and stabilize list, delete and close (#7984) docs: Correct `gosoe` typo to `goose` (#8062) fix: use default provider and model when provider in session no longer exists (#8035) feat: add GOOSE_SHELL env var to configure preferred shell (#7909) fix(desktop): fullscreen header bar + always-visible close controls (#8033) docs: add Claude Code approve mode permission routing documentation (#7949) chatgpt_codex: Support reasoning and gpt-5.4 (#7941) refactor(anthropic): fix N+1 thinking message storage issue (#7958) fix: handle mid-stream error events in OpenAI SSE streaming (#8031) Fix apps extension: coerce string arguments from inner LLM responses (#8030) feat: ability to expand sidebar to see chats names (#7816) Fix config for GOOSE_MAX_BACKGROUND_TASKS (#7940) set MACOSX_DEPLOYMENT_TARGET=12.0 (#7947) ...
* main: (37 commits) fix: handle reasoning content blocks in OpenAI-compat streaming parser (#8078) chore(acp): build native packages on latest mac (#8075) Display delegate sub agents logs in UI (#7519) Update tar version to avoid CVE-2026-33056 (#8073) refactor: consolidate duplicated dependencies into workspace (#8041) tui: set up for publishing via github actions (#8020) feat: feature-gate local inference dependencies (#7976) feat: ability to manage sub recipes in desktop ui (#6360) Tweak the release process: no more merge to main (#7994) fix: gemini models via databricks (#8042) feat(apps): Pass toolInfo to MCP Apps via hostContext (#7506) fix: remove configured marker when deleting oauth provider configuration (#7887) docs: add vmware-aiops MCP extension documentation (#8055) Show setup instructions for ACP providers in settings modal (#8065) deps: replace sigstore-verification with sigstore-verify to kill vulns (#8064) feat(acp): add session/set_config and stabilize list, delete and close (#7984) docs: Correct `gosoe` typo to `goose` (#8062) fix: use default provider and model when provider in session no longer exists (#8035) feat: add GOOSE_SHELL env var to configure preferred shell (#7909) fix(desktop): fullscreen header bar + always-visible close controls (#8033) ...
Summary
Implement the
toolInfofield inMcpUiHostContextper the MCP Apps spec (ext-apps 2026-01-26). MCP Apps can now access the tool definition (name, description, inputSchema) that triggered their instantiation.To test: add https://mcp-app-bench.onrender.com/mcp as an extension and prompt "inspect host info" (refer
hostContext.toolInfoin below screenshot)What changed
Backend (Rust):
input_schemafield toToolInfostructrmcp::model::Toolin theget_toolshandlerFrontend (TypeScript):
toolNameprop toMcpAppRenderer/agent/toolsand constructs an MCP SDKToolobjecthostContext.toolInfowhen available; gracefully omitted if fetch failstoolNamefromMcpAppWrapperGenerated: OpenAPI spec + client types updated to include
input_schema.Notes
toolInfo.id(the JSON-RPC request id of thetools/callrequest) is not yet populated. That id is generated internally by rmcp's transport layer and isn't surfaced through the extension manager or message stream to the frontend. A comment in the code documents the path to plumb it if needed.toolNameis optional —StandaloneAppViewrenders apps without a tool call context, sotoolInfois simply omitted (valid per spec).Test plan