feat(typescript): MCP tool-UI passthrough in MCPToolProvider - #597
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c436a6b73b
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const tool = this.tools.find((t) => t.name === toolName); | ||
| const result = tool ? await tool.func(inputData) : await this.callMCPTool(toolName, inputData); |
There was a problem hiding this comment.
Reject model calls to app-only MCP tools
When a tool has _meta.ui.visibility that excludes model, it is removed from this.tools, but this fallback still executes it if a tool-use block names it. In any environment where the model/provider can emit or replay an arbitrary tool name, an app-only MCP tool becomes invocable through the model tool loop instead of being rejected; keeping such tools callable for the UI needs a separate path that is not toolHandler.
Useful? React with 👍 / 👎.
9a558e6 to
e9facff
Compare
Surface widgets advertised by an MCP server instead of flattening results to text. Each tool's _meta is parsed at connect time (ui resource URI via _meta.ui.resourceUri or the openai/outputTemplate alias, plus visibility); app-only tools are kept callable but filtered out of this.tools. Each MCP tool's func is now real: it calls the server and returns a ToolResult (joined text -> content, structuredContent -> structuredContent, and a UIPayload fetched via readResource + cached per client when the tool advertised a UI). toolHandler invokes func with the full input and routes only the text to the model. This also fixes a latent gap: GroundedAgent captures via the tool's func, which the previous dummy-func + bespoke toolHandler override bypassed, so MCP tool calls were never captured. Additive: a server advertising no _meta.ui sends the same text to the model as before. Mirrors the Swift and Python providers. Closes #596 Code written by Claude (Opus 4.8), architected and approved by @cornelcroi.
Drives an MCP tool through the provider's toolHandler inside a GroundedAgent and asserts the presenter runs on the captured facts. Before the func/toolHandler rework this capture never happened (grounding silently degraded to the gatherer's draft), so this locks the fix in. Code written by Claude (Opus 4.8), architected and approved by @cornelcroi.
toolHandler now only executes tools present in this.tools (model-visible). A tool-use block naming an app-only or unknown tool returns a not-found result instead of being executed, so an app-only MCP tool can never be invoked through the model loop by an emitted/replayed name. App-only tools stay in toolClientMap for a future host-side call path. Code written by Claude (Opus 4.8), architected and approved by @cornelcroi.
bd3668a to
77677c6
Compare
Issue Link
Closes #596
Summary
Makes the TypeScript
MCPToolProvidercarry a server-advertised widget through to the caller instead of flattening the MCP result to text — so a TSGroundedAgentcan render the widgets of the same MCP server that backs a ChatGPT App. Mirrors the Swift and Python (#595) providers.Stacked on #593 (base
feat/typescript-grounded-agent-tool-ui, which adds TSToolResult/UIPayload). Merge #593 first; GitHub retargets this tomain.Changes
_metaat connect time: UI resource URI (_meta.ui.resourceUrior the OpenAIopenai/outputTemplatealias) and visibility. App-only tools (visibility excludesmodel) are filtered out ofthis.tools— never advertised — while staying intoolClientMap.funcis now real: it calls the server and returns aToolResult.callMCPToolreadsstructuredContent, and when the tool advertised a UI it fetches the resource (readResource, cached per client) and builds aUIPayload.toolHandlerinvokesfuncwith the full input and routes only the text to the model.GroundedAgentcaptures tool results via the tool'sfunc; the previous dummyfunc+ bespoketoolHandleroverride bypassed it, so MCP tool calls weren't captured at all. Now they are.User experience
Point a
GroundedAgent's gatherer at an MCP server whose tool advertises a widget; consume the agent's stream and the caller gets the widget (a{ ui }chunk) before the grounded text.Checklist
_meta.uisends the same model-facing text (success / isError / exception / unknown-tool strings all match the old override); full input preserved (nomessages-key drop, no throw on undefined)@modelcontextprotocol/sdkstays an optional peer)messages-key + undefined input);npm run build/lint/coveragegreen (169 tests, 16 suites)processTool) fixedNote: touches the same docs section as #595 (Python MCP) — resolve any overlap by keeping this three-language wording.
Code written by Claude (Opus 4.8), architected and approved by @cornelcroi.