Skip to content

Commit 66db489

Browse files
committed
Add ListDatabases UI
1 parent 557c511 commit 66db489

File tree

10 files changed

+1350
-236
lines changed

10 files changed

+1350
-236
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"@types/express": "^5.0.3",
8787
"@types/node": "^24.5.2",
8888
"@types/proper-lockfile": "^4.1.4",
89+
"@types/react": "^18.0.0",
8990
"@types/semver": "^7.7.0",
9091
"@types/yargs-parser": "^21.0.3",
9192
"@typescript-eslint/parser": "^8.44.0",
@@ -115,6 +116,7 @@
115116
"vitest": "^3.2.4"
116117
},
117118
"dependencies": {
119+
"@leafygreen-ui/table": "^15.2.2",
118120
"@mcp-ui/server": "^5.13.1",
119121
"@modelcontextprotocol/sdk": "^1.24.2",
120122
"@mongodb-js/device-id": "^0.3.1",
@@ -133,6 +135,8 @@
133135
"node-machine-id": "1.1.12",
134136
"oauth4webapi": "^3.8.0",
135137
"openapi-fetch": "^0.15.0",
138+
"react": "^18.0.0",
139+
"react-dom": "^18.0.0",
136140
"ts-levenshtein": "^1.0.7",
137141
"voyage-ai-provider": "^2.0.0",
138142
"zod": "^3.25.76"

pnpm-lock.yaml

Lines changed: 1059 additions & 234 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools/mongodb/metadata/listDatabases.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type * as bson from "bson";
44
import type { OperationType } from "../../tool.js";
55
import { formatUntrustedData } from "../../tool.js";
66
import { createUIResource } from "@mcp-ui/server";
7-
import { html as htmlContent } from "../../../ui/sample-bundle.js";
7+
import { html as htmlContent } from "../../../ui/bundles/sample-bundle.js";
88

99
export class ListDatabasesTool extends MongoDBToolBase {
1010
public name = "list-databases";
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// /** @jsxImportSource @emotion/react */
2+
// import { Card } from "@leafygreen-ui/card";
3+
// import { H1 } from "@leafygreen-ui/typography";
4+
// import { z } from "zod";
5+
// import { useRenderData } from "../../hooks/useRenderData";
6+
// import * as styles from "./ListDataBases.styles";
7+
8+
// // Schema for a single database
9+
// const DatabaseInfoSchema = z.object({
10+
// name: z.string(),
11+
// size: z.number(),
12+
// });
13+
14+
// // Schema for list-databases tool render data
15+
// const ListDatabasesDataSchema = z.object({
16+
// databases: z.array(DatabaseInfoSchema),
17+
// totalCount: z.number(),
18+
// });
19+
20+
// type ListDatabasesRenderData = z.infer<typeof ListDatabasesDataSchema>;
21+
22+
// /**
23+
// * Format bytes to human-readable format
24+
// */
25+
// function formatBytes(bytes: number): string {
26+
// if (bytes === 0) return "0 Bytes";
27+
28+
// const k = 1024;
29+
// const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
30+
// const i = Math.floor(Math.log(bytes) / Math.log(k));
31+
32+
// return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
33+
// }
34+
35+
// export const ListDatabases = () => {
36+
// const { data, isLoading, error } = useRenderData<ListDatabasesRenderData>();
37+
38+
// if (isLoading) {
39+
// return (
40+
// <Card>
41+
// <div css={styles.cardContentStyles}>
42+
// <H1>Databases</H1>
43+
// <div style={{ padding: "20px" }}>Loading...</div>
44+
// </div>
45+
// </Card>
46+
// );
47+
// }
48+
49+
// if (error) {
50+
// return (
51+
// <Card>
52+
// <div css={styles.cardContentStyles}>
53+
// <H1>Databases</H1>
54+
// <div style={{ color: "red", padding: "20px" }}>Error: {error}</div>
55+
// </div>
56+
// </Card>
57+
// );
58+
// }
59+
60+
// if (!data || !data.databases || data.databases.length === 0) {
61+
// return (
62+
// <Card>
63+
// <div css={styles.cardContentStyles}>
64+
// <H1>Databases</H1>
65+
// <div style={{ padding: "20px" }}>No databases available</div>
66+
// </div>
67+
// </Card>
68+
// );
69+
// }
70+
71+
// // Validate props against schema
72+
// const validationResult = ListDatabasesDataSchema.safeParse(data);
73+
74+
// if (!validationResult.success) {
75+
// console.error("[ListDatabases] Validation error:", validationResult.error);
76+
// return (
77+
// <Card>
78+
// <div css={styles.cardContentStyles}>
79+
// <H1>Databases</H1>
80+
// <div style={{ color: "red", padding: "20px" }}>
81+
// Validation Error: {validationResult.error.issues.map((e) => e.message).join(", ")}
82+
// </div>
83+
// </div>
84+
// </Card>
85+
// );
86+
// }
87+
88+
// return (
89+
// <Card>
90+
// <div css={styles.cardContentStyles}>
91+
// <H1>Databases</H1>
92+
// <ul css={styles.listStyles}>
93+
// {data.databases.map((db, index) => (
94+
// <li key={index} css={styles.listItemStyles}>
95+
// <span css={styles.databaseNameStyles}>{db.name}</span>
96+
// <span css={styles.databaseSizeStyles}>{formatBytes(db.size)}</span>
97+
// </li>
98+
// ))}
99+
// </ul>
100+
// </div>
101+
// </Card>
102+
// );
103+
// };
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from "react";
2+
import { z } from "zod";
3+
import { useRenderData } from "../../hooks/index.js";
4+
// import * as styles from "./ListDatabases.styles";
5+
6+
// Schema for a single database
7+
const DatabaseInfoSchema = z.object({
8+
name: z.string(),
9+
size: z.number(),
10+
});
11+
12+
// Schema for list-databases tool render data
13+
const ListDatabasesDataSchema = z.object({
14+
databases: z.array(DatabaseInfoSchema),
15+
totalCount: z.number(),
16+
});
17+
18+
type ListDatabasesRenderData = z.infer<typeof ListDatabasesDataSchema>;
19+
20+
/**
21+
* Format bytes to human-readable format
22+
*/
23+
function formatBytes(bytes: number): string {
24+
if (bytes === 0) return "0 Bytes";
25+
26+
const k = 1024;
27+
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
28+
const i = Math.floor(Math.log(bytes) / Math.log(k));
29+
30+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
31+
}
32+
33+
export const ListDatabases = () => {
34+
const { data, isLoading, error } = useRenderData<ListDatabasesRenderData>();
35+
36+
if (isLoading) {
37+
return <div>Loading...</div>;
38+
}
39+
40+
if (error) {
41+
return <div>Error: {error}</div>;
42+
}
43+
44+
// Validate props against schema
45+
const validationResult = ListDatabasesDataSchema.safeParse(data);
46+
47+
// if (!validationResult.success) {
48+
// console.error("[ListDatabases] Validation error:", validationResult.error);
49+
// return (
50+
// <Card>
51+
// <div css={styles.cardContentStyles}>
52+
// <H1>Databases</H1>
53+
// <div style={{ color: "red", padding: "20px" }}>
54+
// Validation Error: {validationResult.error.issues.map((e) => e.message).join(", ")}
55+
// </div>
56+
// </div>
57+
// </Card>
58+
// );
59+
// }
60+
61+
// return (
62+
// <Card>
63+
// <div css={styles.cardContentStyles}>
64+
// <H1>Databases</H1>
65+
// <ul css={styles.listStyles}>
66+
// {data.databases.map((db, index) => (
67+
// <li key={index} css={styles.listItemStyles}>
68+
// <span css={styles.databaseNameStyles}>{db.name}</span>
69+
// <span css={styles.databaseSizeStyles}>{formatBytes(db.size)}</span>
70+
// </li>
71+
// ))}
72+
// </ul>
73+
// </div>
74+
// </Card>
75+
// );
76+
77+
return <div>ListDatabases</div>;
78+
};

src/ui/components/ListDatabases/index.ts

Whitespace-only changes.

src/ui/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { useRenderData } from "./useRenderData.js";

src/ui/hooks/useRenderData.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { useEffect, useState } from "react";
2+
3+
/**
4+
* Hook for receiving render data from parent window via postMessage
5+
* This is used by iframe-based UI components that receive data from the MCP client
6+
*
7+
* @template T - The type of data expected in the renderData payload
8+
* @returns An object containing:
9+
* - data: The received render data (or null if not yet received)
10+
* - isLoading: Whether data is still being loaded
11+
* - error: Error message if message validation failed
12+
*
13+
* @example
14+
* ```tsx
15+
* interface MyData {
16+
* items: string[];
17+
* }
18+
*
19+
* function MyComponent() {
20+
* const { data, isLoading, error } = useRenderData<MyData>();
21+
* // ...
22+
* }
23+
* ```
24+
*/
25+
export function useRenderData<T = unknown>() {
26+
const [data, setData] = useState<T | null>(null);
27+
const [isLoading, setIsLoading] = useState(true);
28+
const [error, setError] = useState<string | null>(null);
29+
30+
useEffect(() => {
31+
console.log("[useRenderData] Waiting for render data from parent...");
32+
33+
// Listen for render data from parent window
34+
const handleMessage = (event: MessageEvent) => {
35+
console.log("[useRenderData] Received message:", event.data);
36+
37+
// Note: Origin validation is intentionally NOT performed here
38+
// MCP-UI is designed to be universally embeddable from any MCP client
39+
// (desktop apps, browser extensions, web apps with variable origins)
40+
// For private/enterprise deployments requiring origin restrictions, see next.config.ts
41+
42+
// Security: Message type validation - only accept expected message types
43+
if (event.data?.type !== "ui-lifecycle-iframe-render-data") {
44+
// Silently ignore messages that aren't for us
45+
return;
46+
}
47+
48+
// Security: Payload structure validation
49+
if (!event.data.payload || typeof event.data.payload !== "object") {
50+
const errorMsg = "Invalid payload structure received";
51+
console.error(`[useRenderData] ${errorMsg}`);
52+
setError(errorMsg);
53+
setIsLoading(false);
54+
return;
55+
}
56+
57+
const renderData = event.data.payload.renderData;
58+
console.log("[useRenderData] Received render data:", renderData);
59+
60+
// Security: Validate data exists and is of expected type
61+
if (renderData === undefined || renderData === null) {
62+
console.warn("[useRenderData] Received null/undefined renderData");
63+
setIsLoading(false);
64+
// Not an error - parent may intentionally send null
65+
return;
66+
}
67+
68+
// Security: Basic type checking - ensure data matches expected structure
69+
if (typeof renderData !== "object") {
70+
const errorMsg = `Expected object but received ${typeof renderData}`;
71+
console.error(`[useRenderData] ${errorMsg}`);
72+
setError(errorMsg);
73+
setIsLoading(false);
74+
return;
75+
}
76+
77+
// Note: XSS prevention is handled by React's automatic escaping when rendering
78+
// If you need to render raw HTML (dangerouslySetInnerHTML), sanitize with DOMPurify
79+
// Note: Schema validation is handled by the components themselves
80+
setData(renderData as T);
81+
setIsLoading(false);
82+
setError(null);
83+
};
84+
85+
window.addEventListener("message", handleMessage);
86+
87+
// Notify parent we're ready to receive data
88+
window.parent.postMessage({ type: "ui-lifecycle-iframe-ready" }, "*");
89+
console.log("[useRenderData] Sent ui-lifecycle-iframe-ready to parent");
90+
91+
return () => {
92+
console.log("[useRenderData] Cleaning up message listener");
93+
window.removeEventListener("message", handleMessage);
94+
};
95+
}, []);
96+
97+
return {
98+
data,
99+
isLoading,
100+
error,
101+
};
102+
}

tsconfig.build.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
"noImplicitReturns": true,
1919
"declaration": true,
2020
"declarationMap": true,
21+
"jsx": "react-jsx",
2122
"paths": {
2223
"mongodb-connection-string-url": [
2324
"./node_modules/mongodb-connection-string-url/lib/index.d.ts"
2425
],
2526
"ts-levenshtein": ["./node_modules/ts-levenshtein/dist/index.d.mts"]
2627
}
2728
},
28-
"include": ["src/**/*.ts"]
29+
"include": ["src/**/*.ts", "src/**/*.tsx"]
2930
}

0 commit comments

Comments
 (0)