Skip to content

Comments

fix: add missing tool result to AppsRenderer#1075

Merged
cliffhall merged 8 commits intomodelcontextprotocol:mainfrom
infoxicator:fix/apps-tool-result
Feb 16, 2026
Merged

fix: add missing tool result to AppsRenderer#1075
cliffhall merged 8 commits intomodelcontextprotocol:mainfrom
infoxicator:fix/apps-tool-result

Conversation

@infoxicator
Copy link
Contributor

@infoxicator infoxicator commented Feb 8, 2026

Summary

Fixes Apps tab missing tool result data causing Budget Allocator Server example not to display correctly.

This was to do with missing tool result not passed to the AppsRenderer component

Note: Inspector V2 is under development to address architectural and UX improvements. During this time, V1 contributions should focus on bug fixes and MCP spec compliance. See CONTRIBUTING.md for more details.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Refactoring (no functional changes)
  • Test updates
  • Build/CI improvements

Changes Made

In Apps tab, apps received ui/notifications/tool-input but not ui/notifications/tool-result. Apps that initialize from ontoolresult (for example, budget allocator) could render empty due to missing tool data

Here’s a PR-ready summary (non-test changes only):

Summary

This PR wires the Tools -> Apps result handoff so app UIs can open with both the submitted input and latest tool result, while also improving app-launch execution flow and state handling.

What changed

  • Added a cross-tab prefill state in App to store the latest app tool invocation payload (toolName, params, and result) when run from the Tools tab.
  • Updated app-tool detection to use getToolUiResourceUri(...), so both _meta.ui.resourceUri and _meta["ui/resourceUri"] metadata are supported.
  • Changed callTool(...) to return CompatibilityCallToolResult so callers can immediately reuse the result.
  • Tools tab execution path now:
    • runs the tool,
    • captures the result,
    • stores prefill payload for Apps when the tool has app UI metadata,
    • clears prefill for non-app tools.
  • AppsTab now accepts:
    • callTool,
    • prefilledToolCall,
    • onPrefilledToolCallConsumed.
  • AppsTab now executes tools before opening the app renderer and manages explicit launch state:
    • loading state (Opening App...) + disabled open button,
    • snapshot of submitted params/result (submittedParams, submittedToolResult) to avoid mid-flight form edits leaking into rendered app input,
    • automatic app opening from prefilled Tools payload,
    • consumption callback to clear one-time prefill state in App.
  • Added launch run-id guards in AppsTab to prevent stale async completions from older launches overriding the current app state.
  • Hardened AppsTab failure behavior: rejected tool calls now reset opening state and keep user in input view instead of leaking unhandled promise errors.
  • AppRenderer now accepts toolResult and normalizes compatibility result shapes (including wrapped { toolResult: ... }) to valid CallToolResult before passing to the MCP UI renderer.

Related Issues

Testing

Added comprehensive unit tests (integration)

  • Tested in UI mode
  • Tested in CLI mode
  • Tested with STDIO transport
  • Tested with SSE transport
  • Tested with Streamable HTTP transport
  • Added/updated automated tests
  • Manual testing performed

Test Results and/or Instructions

Run the budget allocation server example: https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/budget-allocator-server

Broken version:
Screenshot 2026-02-08 at 00 26 25

Fixed version:

image Screenshots are encouraged to share your testing results for this change.

Checklist

  • Code follows the style guidelines (ran npm run prettier-fix)
  • Self-review completed
  • Code is commented where necessary
  • Documentation updated (README, comments, etc.)

Breaking Changes

Additional Context


const runTool = async () => {
try {
const result = await mcpClient.request(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While calling the tool from here works.

I wonder if the tool result is already present and can be passed as a prop to this component instead of making a brand new tool request.

let me know if that's preferable

Copy link
Member

@cliffhall cliffhall Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this PR and it works. However, I would rather this part operate more like the ToolsTab tool calling scheme. To test the theory, I removed these changes and passed down toolResult from App -> AppsTab -> AppRenderer and it works, but only if I run the tool in the tool tab first.

To complete the scenario, we would need to pass down callTool from the App, and in AppsTab.handleSelectTool, where we set the app to be open if there is no form to fill first, call the tool. Also in AppsTab.handleOpenApp which handles the Open App button if there was a form to fill, call the tool. Probably there can be a common function that is called from both those places.

@infoxicator
Copy link
Contributor Author

@cliffhall this should also fix the pdf server and the other examples that were failing

Copy link
Member

@cliffhall cliffhall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that using toolResult and the existing callTool function is better. Following the pattern with ToolsTab I see it can be done.


const runTool = async () => {
try {
const result = await mcpClient.request(
Copy link
Member

@cliffhall cliffhall Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this PR and it works. However, I would rather this part operate more like the ToolsTab tool calling scheme. To test the theory, I removed these changes and passed down toolResult from App -> AppsTab -> AppRenderer and it works, but only if I run the tool in the tool tab first.

To complete the scenario, we would need to pass down callTool from the App, and in AppsTab.handleSelectTool, where we set the app to be open if there is no form to fill first, call the tool. Also in AppsTab.handleOpenApp which handles the Open App button if there was a form to fill, call the tool. Probably there can be a common function that is called from both those places.

@infoxicator
Copy link
Contributor Author

Agree that using toolResult and the existing callTool function is better. Following the pattern with ToolsTab I see it can be done.

@cliffhall I have now implemented a flow with the following:

  1. If you call the tool in the tools tab and then switch to the apps tab, the app will reuse the result already present from the tools tab and doesn't make a new tool call

  2. if you go directly to the apps tab it works as before where you can make the tool call and render the app with paramteters

  3. if there are no. required paramteters the tool call happens automatically when opening the apps tab.

Hope that covers everything

give it a test and let me know

Copy link
Member

@cliffhall cliffhall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@infoxicator Code looks good, but I see one lint error. (Clearly we need lint to happen in our CI)
Image

Copy link
Member

@cliffhall cliffhall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 👍

@cliffhall cliffhall merged commit f1b4265 into modelcontextprotocol:main Feb 16, 2026
4 checks passed
@idosal idosal requested a review from Copilot February 16, 2026 22:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a bug where the Apps tab was missing tool result data, causing apps like the Budget Allocator Server example to not display correctly. The fix implements a complete Tools → Apps handoff mechanism that passes both tool input parameters and execution results to the AppRenderer.

Changes:

  • Added cross-tab prefill state management to pass tool call data (params + result) from Tools tab to Apps tab
  • Modified Apps tab to execute tools before opening app renderer, with loading states and race condition guards
  • Updated AppRenderer to accept and normalize tool results before passing to MCP UI renderer

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
client/src/App.tsx Adds PrefilledAppsToolCall state and logic to capture/clear tool results for app tools when executed from Tools tab
client/src/components/AppsTab.tsx Implements executeToolAndOpenApp flow with loading states, race condition guards via run IDs, and automatic prefilled app launching
client/src/components/AppRenderer.tsx Adds toolResult prop with normalization logic to handle both direct and wrapped CompatibilityCallToolResult formats
client/src/components/ToolsTab.tsx Changes callTool return type from Promise to Promise
client/src/components/__tests__/AppsTab.test.tsx Comprehensive tests for async app launching, prefilled calls, param snapshot preservation, error recovery, and race condition handling
client/src/components/__tests__/AppRenderer.test.tsx Tests for tool result prop passing, normalization of wrapped results, and bridge event delivery
client/src/__tests__/App.toolsAppsPrefill.test.tsx Integration test verifying end-to-end Tools → Apps prefill handoff

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +74 to +78
try {
return structuredClone(source);
} catch {
return { ...source };
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cloneToolParams function is duplicated between App.tsx and AppsTab.tsx with identical implementations. Consider extracting this utility function to a shared location like client/src/utils/paramUtils.ts to follow the DRY principle and maintain consistency.

Suggested change
try {
return structuredClone(source);
} catch {
return { ...source };
}
if (typeof structuredClone === "function") {
try {
return structuredClone(source);
} catch {
// Fall through to the shallow-clone fallback below.
}
}
// Fallback for environments without structuredClone or if it fails.
return { ...source };

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants