Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion client/src/components/AppRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
Tool,
ContentBlock,
CallToolResult,
CallToolResultSchema,
ServerNotification,
LoggingMessageNotificationParams,
} from "@modelcontextprotocol/sdk/types.js";
Expand Down Expand Up @@ -35,6 +37,8 @@ const AppRenderer = ({
onNotification,
}: AppRendererProps) => {
const [error, setError] = useState<string | null>(null);
const [toolResult, setToolResult] = useState<CallToolResult | undefined>();
const latestRunIdRef = useRef(0);
const { toast } = useToast();

const hostContext: McpUiHostContext = useMemo(
Expand Down Expand Up @@ -85,6 +89,67 @@ const AppRenderer = ({
}
};

useEffect(() => {
if (!mcpClient) {
setToolResult(undefined);
return;
}

const runId = ++latestRunIdRef.current;
const abortController = new AbortController();
setToolResult(undefined);

const runTool = async () => {
try {
const result = await mcpClient.request(
Copy link
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.

{
method: "tools/call",
params: {
name: tool.name,
arguments: toolInput ?? {},
},
},
CallToolResultSchema,
{ signal: abortController.signal },
);

if (
abortController.signal.aborted ||
runId !== latestRunIdRef.current
) {
return;
}

setToolResult(result);
} catch (runError) {
if (
abortController.signal.aborted ||
runId !== latestRunIdRef.current
) {
return;
}

const message =
runError instanceof Error ? runError.message : String(runError);
setToolResult({
content: [
{
type: "text",
text: message,
},
],
isError: true,
});
}
};

void runTool();

return () => {
abortController.abort();
};
}, [mcpClient, tool.name, toolInput]);

if (!mcpClient) {
return (
<Alert>
Expand Down Expand Up @@ -115,6 +180,7 @@ const AppRenderer = ({
toolName={tool.name}
hostContext={hostContext}
toolInput={toolInput}
toolResult={toolResult}
sandbox={{
url: new URL(sandboxPath, window.location.origin),
}}
Expand Down
Loading