Skip to content

Commit 254bc8a

Browse files
committed
review feedback 1
1 parent 8479dc2 commit 254bc8a

File tree

3 files changed

+42
-78
lines changed

3 files changed

+42
-78
lines changed

src/tools/tool.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ export abstract class ToolBase {
482482
});
483483

484484
const result = await this.execute(args, { signal });
485-
const finalResult = this.appendUIResource(result);
485+
const finalResult = await this.appendUIResource(result);
486486

487487
this.emitToolEvent(args, { startTime, result: finalResult });
488488

@@ -713,45 +713,24 @@ export abstract class ToolBase {
713713
return metadata;
714714
}
715715

716-
/**
717-
* Get the UI HTML string for this tool from the registry.
718-
* Returns the registered UI HTML, or undefined if no UI exists for this tool.
719-
*/
720-
protected getUI(): string | undefined {
721-
return this.uiRegistry?.get(this.name);
722-
}
723-
724716
/**
725717
* Appends a UIResource to the tool result.
726718
*
727719
* @param result - The result from the tool's `execute()` method
728720
* @returns The result with UIResource appended if conditions are met, otherwise unchanged
729721
*/
730-
private appendUIResource(result: CallToolResult): CallToolResult {
722+
private async appendUIResource(result: CallToolResult): Promise<CallToolResult> {
731723
if (!this.isFeatureEnabled("mcpUI")) {
732724
return result;
733725
}
734726

735-
const uiHtml = this.getUI();
727+
const uiHtml = await this.uiRegistry?.get(this.name);
736728
if (!uiHtml || !result.structuredContent) {
737729
return result;
738730
}
739731

740-
if (this.outputSchema) {
741-
const schema = z.object(this.outputSchema);
742-
const validation = schema.safeParse(result.structuredContent);
743-
if (!validation.success) {
744-
this.session.logger.warning({
745-
id: LogId.toolExecute,
746-
context: `tool - ${this.name}`,
747-
message: `structuredContent failed validation against outputSchema, skipping UI resource: ${validation.error.message}`,
748-
});
749-
return result;
750-
}
751-
}
752-
753732
const uiResource = createUIResource({
754-
uri: `ui://${this.name}/${Date.now()}`,
733+
uri: `ui://${this.name}`,
755734
content: {
756735
type: "rawHtml",
757736
htmlString: uiHtml,

src/ui/registry/registry.ts

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import { uiHtml } from "../generated/uiHtml.js";
2-
31
/**
42
* UI Registry that manages bundled UI HTML strings for tools.
5-
*
6-
* The default UIs are embedded at build time via the generated uiHtml module.
7-
* Custom UIs can be provided at runtime to override or extend the defaults.
83
*/
94
export class UIRegistry {
105
private customUIs: Map<string, string> = new Map();
6+
private cache: Map<string, string> = new Map();
117

128
constructor(options?: { customUIs?: Record<string, string> }) {
139
if (options?.customUIs) {
@@ -18,30 +14,25 @@ export class UIRegistry {
1814
}
1915

2016
/**
21-
* Get the UI HTML string for a tool.
22-
* @param toolName The name of the tool (kebab-case, e.g., "list-databases")
23-
* @returns The HTML string, or undefined if no UI exists for this tool
17+
* Gets the UI HTML string for a tool, or null if none exists.
2418
*/
25-
get(toolName: string): string | undefined {
26-
return this.customUIs.get(toolName) ?? uiHtml[toolName];
27-
}
19+
async get(toolName: string): Promise<string | null> {
20+
const customUI = this.customUIs.get(toolName);
21+
if (customUI !== undefined) {
22+
return customUI;
23+
}
2824

29-
/**
30-
* Check if a UI exists for a tool.
31-
* @param toolName The name of the tool
32-
* @returns True if a UI exists (custom or built-in)
33-
*/
34-
has(toolName: string): boolean {
35-
return this.customUIs.has(toolName) || toolName in uiHtml;
36-
}
25+
const cached = this.cache.get(toolName);
26+
if (cached !== undefined) {
27+
return cached;
28+
}
3729

38-
/**
39-
* Get all available tool names that have UIs.
40-
* @returns Array of tool names
41-
*/
42-
getAvailableTools(): string[] {
43-
const builtIn = Object.keys(uiHtml);
44-
const custom = Array.from(this.customUIs.keys());
45-
return [...new Set([...builtIn, ...custom])];
30+
try {
31+
const module = (await import(`../generated/tools/${toolName}.js`)) as { default: string };
32+
this.cache.set(toolName, module.default);
33+
return module.default;
34+
} catch {
35+
return null;
36+
}
4637
}
4738
}

vite.ui.config.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const componentsDir = resolve(__dirname, "src/ui/components");
1111
const entriesDir = resolve(__dirname, "node_modules/.cache/mongodb-mcp-server/ui-entries");
1212
const templatePath = resolve(__dirname, "src/ui/build/template.html");
1313
const mountPath = resolve(__dirname, "src/ui/build/mount.tsx");
14-
const generatedModulePath = resolve(__dirname, "src/ui/generated/uiHtml.ts");
14+
const generatedDir = resolve(__dirname, "src/ui/generated");
1515
const uiDistPath = resolve(__dirname, "dist/ui");
1616

1717
// Unique component names from uiMap - only these will be built
@@ -56,11 +56,9 @@ function generateHtmlEntries(): Plugin {
5656
}
5757

5858
/**
59-
* Vite plugin that generates the uiHtml.ts module after the build completes.
60-
* This embeds all built HTML strings into a TypeScript module so they can be
61-
* imported at runtime.
59+
* Vite plugin that generates per-tool UI modules after the build completes.
6260
*
63-
* Uses the uiMap from src/ui/registry/uiMap.ts to map tool names to component HTML files.
61+
* Uses the uiMap to map tool names to component HTML files.
6462
*/
6563
function generateUIModule(): Plugin {
6664
return {
@@ -71,9 +69,16 @@ function generateUIModule(): Plugin {
7169
return;
7270
}
7371

74-
const entries: Record<string, string> = {};
72+
const toolsDir = join(generatedDir, "tools");
73+
if (!existsSync(generatedDir)) {
74+
mkdirSync(generatedDir, { recursive: true });
75+
}
76+
if (!existsSync(toolsDir)) {
77+
mkdirSync(toolsDir, { recursive: true });
78+
}
79+
80+
const generatedTools: string[] = [];
7581

76-
// Use uiMap to determine which tools get which UI
7782
for (const [toolName, componentName] of Object.entries(uiMap)) {
7883
const htmlFile = join(uiDistPath, `${componentName}.html`);
7984
if (!existsSync(htmlFile)) {
@@ -83,32 +88,21 @@ function generateUIModule(): Plugin {
8388
continue;
8489
}
8590
const html = readFileSync(htmlFile, "utf-8");
86-
entries[toolName] = html;
87-
}
8891

89-
const moduleContent = `/**
92+
const toolModuleContent = `/**
9093
* AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
91-
*
92-
* This file is generated by the UI build process (vite build --config vite.ui.config.ts).
93-
* It contains the bundled HTML strings for each UI component, keyed by tool name.
94-
*
95-
* To add a new UI:
96-
* 1. Create a component in src/ui/components/YourComponent/
97-
* 2. Add the mapping in src/ui/registry/uiMap.ts
98-
* 3. Run \`pnpm build:ui\` to regenerate this file
94+
* Generated by: vite build --config vite.ui.config.ts
95+
* Tool: ${toolName}
96+
* Component: ${componentName}
9997
*/
100-
export const uiHtml: Record<string, string> = ${JSON.stringify(entries, null, 4)};
98+
export default ${JSON.stringify(html)};
10199
`;
102-
103-
// Ensure the generated directory exists
104-
const generatedDir = dirname(generatedModulePath);
105-
if (!existsSync(generatedDir)) {
106-
mkdirSync(generatedDir, { recursive: true });
100+
writeFileSync(join(toolsDir, `${toolName}.ts`), toolModuleContent);
101+
generatedTools.push(toolName);
107102
}
108103

109-
writeFileSync(generatedModulePath, moduleContent);
110104
console.log(
111-
`[generate-ui-module] Generated uiHtml.ts with ${Object.keys(entries).length} UI(s): ${Object.keys(entries).join(", ")}`
105+
`[generate-ui-module] Generated ${generatedTools.length} lazy UI module(s): ${generatedTools.join(", ")}`
112106
);
113107
},
114108
};

0 commit comments

Comments
 (0)