Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d8092cf
Fix MCP Apps rendering issue and add comprehensive logging
github-actions[bot] Jan 26, 2026
689bc3f
Fix MCP Apps iframe rendering issue
github-actions[bot] Jan 26, 2026
cc7ab7f
Fix MCP Apps HTML extraction to match spec
github-actions[bot] Jan 26, 2026
eb11d46
Add comprehensive tests for MCP Apps support
github-actions[bot] Jan 26, 2026
5cc25db
Add MCP Apps support to Inspector
github-actions[bot] Jan 26, 2026
3b054b4
Prefer structuredContent over content field in tool responses
github-actions[bot] Jan 27, 2026
939f3fa
Revert "Prefer structuredContent over content field in tool responses"
cliffhall Jan 27, 2026
b5b5910
feat: integrate @mcp-ui/client for MCP application rendering
cliffhall Jan 29, 2026
9a16f13
Potential fix for code scanning alert no. 37: Client-side cross-site …
cliffhall Jan 29, 2026
b4000df
Update package-lock.json
cliffhall Jan 29, 2026
6f4c294
Potential fix for code scanning alert no. 38: Client-side cross-site …
cliffhall Jan 29, 2026
6ec23b8
Potential fix for code scanning alert no. 39: Client-side cross-site …
cliffhall Jan 29, 2026
700d24f
prettier
cliffhall Jan 29, 2026
7871e6e
Update client/src/components/AppsTab.tsx
cliffhall Jan 29, 2026
211ad3c
prettier
cliffhall Jan 29, 2026
e5dbe4c
In AppsTab.test.tsx, fetch button by aria-label
cliffhall Jan 29, 2026
7c4e9df
relative imports
cliffhall Jan 29, 2026
4a84f25
Remove sanitization logic from sandbox_proxy.html. It's breaking the …
cliffhall Jan 29, 2026
8b70df5
In sandbox_proxy.html,formatting
cliffhall Jan 29, 2026
d7c3ef3
In AppsTab.tsx, if an app has no inputSchema, just show it, otherwise…
cliffhall Jan 30, 2026
529ca11
In AppRenderer.tsx,
cliffhall Jan 30, 2026
853d789
In AppRenderer.test.tsx, and AppsTab.test.tsx
cliffhall Jan 30, 2026
b1a4435
Serving the sandbox_proxy.html from a separate port, using the proxy …
cliffhall Jan 30, 2026
00ee07b
In client.js
cliffhall Jan 30, 2026
d107fb4
In vite.config.ts
cliffhall Jan 30, 2026
35b7fff
Potential fix for code scanning alert no. 41: Missing rate limiting
cliffhall Jan 30, 2026
9fd260b
In AppRenderer.tsx
cliffhall Jan 30, 2026
cde8190
In sandbox_proxy.html
cliffhall Jan 30, 2026
47ed9e0
prettier
cliffhall Jan 30, 2026
1204b86
In AppRenderer.tsx
cliffhall Jan 31, 2026
f10a27a
Add logging message handler to AppRenderer
cliffhall Jan 31, 2026
3c076bd
In AppRenderer.tsx
cliffhall Feb 2, 2026
2677536
In AppRenderer.test.tsx
cliffhall Feb 2, 2026
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
7 changes: 7 additions & 0 deletions client/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ module.exports = {
tsconfig: "tsconfig.jest.json",
},
],
"^.+\\.m?js$": [
"ts-jest",
{
tsconfig: "tsconfig.jest.json",
},
],
},
extensionsToTreatAsEsm: [".ts", ".tsx"],
transformIgnorePatterns: ["node_modules/(?!(@modelcontextprotocol)/)"],
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
// Exclude directories and files that don't need to be tested
testPathIgnorePatterns: [
Expand Down
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"cleanup:e2e": "node e2e/global-teardown.js"
},
"dependencies": {
"@mcp-ui/client": "^6.0.0",
"@modelcontextprotocol/ext-apps": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.25.2",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.3",
Expand Down
89 changes: 75 additions & 14 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import {
AppWindow,
Bell,
Files,
FolderTree,
Expand All @@ -75,6 +76,7 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
import TasksTab from "./components/TasksTab";
import AppsTab from "./components/AppsTab";
import { InspectorConfig } from "./lib/configurationTypes";
import {
getMCPProxyAddress,
Expand Down Expand Up @@ -126,6 +128,9 @@ const App = () => {
const [resourceContentMap, setResourceContentMap] = useState<
Record<string, string>
>({});
const [fetchingResources, setFetchingResources] = useState<Set<string>>(
new Set(),
);
const [prompts, setPrompts] = useState<Prompt[]>([]);
const [promptContent, setPromptContent] = useState<string>("");
const [tools, setTools] = useState<Tool[]>([]);
Expand Down Expand Up @@ -308,11 +313,13 @@ const App = () => {
...(serverCapabilities?.prompts ? ["prompts"] : []),
...(serverCapabilities?.tools ? ["tools"] : []),
...(serverCapabilities?.tasks ? ["tasks"] : []),
"apps",
"ping",
"sampling",
"elicitations",
"roots",
"auth",
"metadata",
];

if (!validTabs.includes(originatingTab)) return;
Expand Down Expand Up @@ -440,11 +447,13 @@ const App = () => {
...(serverCapabilities?.prompts ? ["prompts"] : []),
...(serverCapabilities?.tools ? ["tools"] : []),
...(serverCapabilities?.tasks ? ["tasks"] : []),
"apps",
"ping",
"sampling",
"elicitations",
"roots",
"auth",
"metadata",
];

const isValidTab = validTabs.includes(hash);
Expand Down Expand Up @@ -473,6 +482,13 @@ const App = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mcpClient, activeTab]);

useEffect(() => {
if (mcpClient && activeTab === "apps" && serverCapabilities?.tools) {
void listTools();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mcpClient, activeTab, serverCapabilities?.tools]);

useEffect(() => {
localStorage.setItem("lastCommand", command);
}, [command]);
Expand Down Expand Up @@ -757,11 +773,13 @@ const App = () => {
...(serverCapabilities?.prompts ? ["prompts"] : []),
...(serverCapabilities?.tools ? ["tools"] : []),
...(serverCapabilities?.tasks ? ["tasks"] : []),
"apps",
"ping",
"sampling",
"elicitations",
"roots",
"auth",
"metadata",
];

if (validTabs.includes(originatingTab)) {
Expand Down Expand Up @@ -851,22 +869,48 @@ const App = () => {
};

const readResource = async (uri: string) => {
if (fetchingResources.has(uri) || resourceContentMap[uri]) {
return;
}

console.log("[App] Reading resource:", uri);
setFetchingResources((prev) => new Set(prev).add(uri));
lastToolCallOriginTabRef.current = currentTabRef.current;

const response = await sendMCPRequest(
{
method: "resources/read" as const,
params: { uri },
},
ReadResourceResultSchema,
"resources",
);
const content = JSON.stringify(response, null, 2);
setResourceContent(content);
setResourceContentMap((prev) => ({
...prev,
[uri]: content,
}));
try {
const response = await sendMCPRequest(
{
method: "resources/read" as const,
params: { uri },
},
ReadResourceResultSchema,
"resources",
);
console.log("[App] Resource read response:", {
uri,
responseLength: JSON.stringify(response).length,
hasContents: !!(response as { contents?: unknown[] }).contents,
});
const content = JSON.stringify(response, null, 2);
setResourceContent(content);
setResourceContentMap((prev) => ({
...prev,
[uri]: content,
}));
} catch (error) {
console.error(`[App] Failed to read resource ${uri}:`, error);
const errorString = (error as Error).message ?? String(error);
setResourceContentMap((prev) => ({
...prev,
[uri]: JSON.stringify({ error: errorString }),
}));
} finally {
setFetchingResources((prev) => {
const next = new Set(prev);
next.delete(uri);
return next;
});
}
};

const subscribeToResource = async (uri: string) => {
Expand Down Expand Up @@ -1308,6 +1352,10 @@ const App = () => {
<ListTodo className="w-4 h-4 mr-2" />
Tasks
</TabsTrigger>
<TabsTrigger value="apps">
<AppWindow className="w-4 h-4 mr-2" />
Apps
</TabsTrigger>
<TabsTrigger value="ping">
<Bell className="w-4 h-4 mr-2" />
Ping
Expand Down Expand Up @@ -1497,6 +1545,19 @@ const App = () => {
error={errors.tasks}
nextCursor={nextTaskCursor}
/>
<AppsTab
sandboxPath={`${getMCPProxyAddress(config)}/sandbox`}
tools={tools}
listTools={() => {
clearError("tools");
listTools();
}}
error={errors.tools}
mcpClient={mcpClient}
onNotification={(notification) => {
setNotifications((prev) => [...prev, notification]);
}}
/>
<ConsoleTab />
<PingTab
onPingClick={() => {
Expand Down
128 changes: 128 additions & 0 deletions client/src/components/AppRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { useMemo, useState } from "react";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
Tool,
ContentBlock,
ServerNotification,
LoggingMessageNotificationParams,
} from "@modelcontextprotocol/sdk/types.js";
import {
AppRenderer as McpUiAppRenderer,
type McpUiHostContext,
type RequestHandlerExtra,
} from "@mcp-ui/client";
import {
type McpUiMessageRequest,
type McpUiMessageResult,
} from "@modelcontextprotocol/ext-apps/app-bridge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import { useToast } from "@/lib/hooks/useToast";

interface AppRendererProps {
sandboxPath: string;
tool: Tool;
mcpClient: Client | null;
toolInput?: Record<string, unknown>;
onNotification?: (notification: ServerNotification) => void;
}

const AppRenderer = ({
sandboxPath,
tool,
mcpClient,
toolInput,
onNotification,
}: AppRendererProps) => {
const [error, setError] = useState<string | null>(null);
const { toast } = useToast();

const hostContext: McpUiHostContext = useMemo(
() => ({
theme: document.documentElement.classList.contains("dark")
? "dark"
: "light",
}),
[],
);

const handleOpenLink = async ({ url }: { url: string }) => {
let isError = true;
if (url.startsWith("https://") || url.startsWith("http://")) {
window.open(url, "_blank");
isError = false;
}
return { isError };
};

const handleMessage = async (
params: McpUiMessageRequest["params"],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_extra: RequestHandlerExtra,
): Promise<McpUiMessageResult> => {
const message = params.content
.filter((block): block is ContentBlock & { type: "text" } =>
Boolean(block.type === "text"),
)
.map((block) => block.text)
.join("\n");

if (message) {
toast({
description: message,
});
}

return {};
};

const handleLoggingMessage = (params: LoggingMessageNotificationParams) => {
if (onNotification) {
onNotification({
method: "notifications/message",
params,
} as ServerNotification);
}
};

if (!mcpClient) {
return (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>Waiting for MCP client...</AlertDescription>
</Alert>
);
}

return (
<div className="flex flex-col h-full">
{error && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}

<div
className="flex-1 border rounded overflow-hidden"
style={{ minHeight: "400px" }}
>
<McpUiAppRenderer
client={mcpClient}
onOpenLink={handleOpenLink}
onMessage={handleMessage}
onLoggingMessage={handleLoggingMessage}
toolName={tool.name}
hostContext={hostContext}
toolInput={toolInput}
sandbox={{
url: new URL(sandboxPath, window.location.origin),
}}
onError={(err) => setError(err.message)}
/>
</div>
</div>
);
};

export default AppRenderer;
Loading
Loading