Skip to content

Commit b5b5910

Browse files
committed
feat: integrate @mcp-ui/client for MCP application rendering
This commit implements the integration of `@mcp-ui/client` to handle the rendering of Model Context Protocol (MCP) applications within the inspector. It includes refactoring the rendering logic, improving resource fetching in the main application state, and setting up a secure sandbox environment. Changes per file: - client/package.json: - Added `@mcp-ui/client` as a dependency. - client/src/components/AppRenderer.tsx: - Refactored to use `McpUiAppRenderer` from `@mcp-ui/client`. - Implemented HTML parsing logic for MCP resource responses. - Configured host context (theme) and sandbox URL for the renderer. - Replaced iframe-based manual rendering with the official component. - client/src/App.tsx: - Added `fetchingResources` state to prevent duplicate concurrent resource requests. - Enhanced `readResource` with better error handling and state tracking. - Optimized resource content mapping to better support application state. - client/src/components/AppsTab.tsx: - Updated to use `getToolUiResourceUri` utility from `@modelcontextprotocol/ext-apps` instead of manual metadata property access. - Simplified tool filtering and selection logic. - client/public/sandbox_proxy.html: - Added a sandbox proxy page to facilitate secure communication between the inspector and the rendered MCP applications. - client/bin/client.js: - Added server rewrites to ensure `sandbox_proxy.html` is served correctly. - Implemented specific `Cache-Control` headers for `sandbox_proxy.html` to prevent stale cached versions. - client/vite.config.ts: - Explicitly configured `publicDir: "public"` to ensure the sandbox proxy is included in the build output. - client/src/components/__tests__/AppRenderer.test.tsx: - Updated tests to mock the new `@mcp-ui/client` component. - Adjusted assertions to verify correct props are passed to the renderer. - Enhanced mock MCP client to include required methods like `getServerCapabilities`.
1 parent 939f3fa commit b5b5910

8 files changed

Lines changed: 225 additions & 487 deletions

File tree

client/bin/client.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ const distPath = join(__dirname, "../dist");
1212
const server = http.createServer((request, response) => {
1313
const handlerOptions = {
1414
public: distPath,
15-
rewrites: [{ source: "/**", destination: "/index.html" }],
15+
cleanUrls: false,
16+
rewrites: [
17+
{ source: "/sandbox_proxy.html", destination: "/sandbox_proxy.html" },
18+
{ source: "/index.html", destination: "/index.html" },
19+
{ source: "/**", destination: "/index.html" },
20+
],
1621
headers: [
1722
{
1823
// Ensure index.html is never cached
@@ -24,6 +29,16 @@ const server = http.createServer((request, response) => {
2429
},
2530
],
2631
},
32+
{
33+
// Ensure sandbox_proxy.html is never cached
34+
source: "sandbox_proxy.html",
35+
headers: [
36+
{
37+
key: "Cache-Control",
38+
value: "no-cache, no-store, max-age=0",
39+
},
40+
],
41+
},
2742
{
2843
// Allow long-term caching for hashed assets
2944
source: "assets/**",

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"cleanup:e2e": "node e2e/global-teardown.js"
2626
},
2727
"dependencies": {
28+
"@mcp-ui/client": "^6.0.0",
2829
"@modelcontextprotocol/ext-apps": "^1.0.0",
2930
"@modelcontextprotocol/sdk": "^1.25.2",
3031
"@radix-ui/react-checkbox": "^1.1.4",

client/public/sandbox_proxy.html

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>MCP App Sandbox Proxy</title>
6+
<style>
7+
body,
8+
html {
9+
margin: 0;
10+
padding: 0;
11+
width: 100%;
12+
height: 100%;
13+
overflow: hidden;
14+
}
15+
</style>
16+
</head>
17+
<body>
18+
<div id="root"></div>
19+
<script>
20+
(function () {
21+
const SANDBOX_PROXY_READY_METHOD =
22+
"ui/notifications/sandbox-proxy-ready";
23+
const SANDBOX_RESOURCE_READY_METHOD =
24+
"ui/notifications/sandbox-resource-ready";
25+
26+
// Notify host that we are ready to receive the app resource
27+
window.parent.postMessage(
28+
{
29+
jsonrpc: "2.0",
30+
method: SANDBOX_PROXY_READY_METHOD,
31+
params: {},
32+
},
33+
"*",
34+
);
35+
36+
// Listen for the app resource (HTML) from the host
37+
window.addEventListener("message", (event) => {
38+
const message = event.data;
39+
if (message && message.method === SANDBOX_RESOURCE_READY_METHOD) {
40+
const { html } = message.params;
41+
if (html) {
42+
// Write the HTML to the document
43+
document.open();
44+
document.write(html);
45+
document.close();
46+
}
47+
}
48+
});
49+
})();
50+
</script>
51+
</body>
52+
</html>

client/src/App.tsx

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ const App = () => {
128128
const [resourceContentMap, setResourceContentMap] = useState<
129129
Record<string, string>
130130
>({});
131+
const [fetchingResources, setFetchingResources] = useState<Set<string>>(
132+
new Set(),
133+
);
131134
const [prompts, setPrompts] = useState<Prompt[]>([]);
132135
const [promptContent, setPromptContent] = useState<string>("");
133136
const [tools, setTools] = useState<Tool[]>([]);
@@ -866,36 +869,48 @@ const App = () => {
866869
};
867870

868871
const readResource = async (uri: string) => {
872+
if (fetchingResources.has(uri) || resourceContentMap[uri]) {
873+
return;
874+
}
875+
869876
console.log("[App] Reading resource:", uri);
877+
setFetchingResources((prev) => new Set(prev).add(uri));
870878
lastToolCallOriginTabRef.current = currentTabRef.current;
871879

872-
const response = await sendMCPRequest(
873-
{
874-
method: "resources/read" as const,
875-
params: { uri },
876-
},
877-
ReadResourceResultSchema,
878-
"resources",
879-
);
880-
console.log("[App] Resource read response:", {
881-
uri,
882-
responseLength: JSON.stringify(response).length,
883-
hasContents: !!(response as { contents?: unknown[] }).contents,
884-
});
885-
const content = JSON.stringify(response, null, 2);
886-
setResourceContent(content);
887-
setResourceContentMap((prev) => {
888-
const updated = {
880+
try {
881+
const response = await sendMCPRequest(
882+
{
883+
method: "resources/read" as const,
884+
params: { uri },
885+
},
886+
ReadResourceResultSchema,
887+
"resources",
888+
);
889+
console.log("[App] Resource read response:", {
890+
uri,
891+
responseLength: JSON.stringify(response).length,
892+
hasContents: !!(response as { contents?: unknown[] }).contents,
893+
});
894+
const content = JSON.stringify(response, null, 2);
895+
setResourceContent(content);
896+
setResourceContentMap((prev) => ({
889897
...prev,
890898
[uri]: content,
891-
};
892-
console.log("[App] Updated resourceContentMap:", {
893-
uri,
894-
contentLength: content.length,
895-
mapKeys: Object.keys(updated),
899+
}));
900+
} catch (error) {
901+
console.error(`[App] Failed to read resource ${uri}:`, error);
902+
const errorString = (error as Error).message ?? String(error);
903+
setResourceContentMap((prev) => ({
904+
...prev,
905+
[uri]: JSON.stringify({ error: errorString }),
906+
}));
907+
} finally {
908+
setFetchingResources((prev) => {
909+
const next = new Set(prev);
910+
next.delete(uri);
911+
return next;
896912
});
897-
return updated;
898-
});
913+
}
899914
};
900915

901916
const subscribeToResource = async (uri: string) => {

0 commit comments

Comments
 (0)