Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const restaurantListUI = defineUI({
description: "Displays restaurant search results",
html: "./dist/widget.html",
prefersBorder: true,
autoResize: true, // Enable automatic size notifications (default: true, MCP Apps only)
});

const app = createApp({
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/types/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ export interface UIDef {
*/
prefersBorder?: boolean;

/**
* Enable automatic size change notifications.
*
* When enabled, the UI automatically reports its size changes to the host
* using a ResizeObserver on document.body and document.documentElement.
* The host can then resize the UI container accordingly.
*
* MCP Apps only - ignored in ChatGPT.
*
* @default true
*/
autoResize?: boolean;
Comment on lines +119 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify the @default documentation for autoResize.

The JSDoc states @default true, but the test on line 154 of packages/core/tests/unit/ui-resources.test.ts shows that when autoResize is not specified, it remains undefined, with the comment stating "The default is applied at the client-side adapter level."

This creates ambiguity about whether:

  1. The property defaults to true at this type level, or
  2. The property remains undefined when unspecified, and the client-side adapter applies a default of true

Consider revising the JSDoc to clarify this behavior more accurately.

📝 Suggested JSDoc revision
  /**
   * Enable automatic size change notifications.
   *
   * When enabled, the UI automatically reports its size changes to the host
   * using a ResizeObserver on document.body and document.documentElement.
   * The host can then resize the UI container accordingly.
   *
   * MCP Apps only - ignored in ChatGPT.
   *
-  * @default true
+  * @default true (applied by client-side adapter when undefined)
   */
  autoResize?: boolean;

Or alternatively, if the server-side definition truly doesn't have a default:

  /**
   * Enable automatic size change notifications.
   *
   * When enabled, the UI automatically reports its size changes to the host
   * using a ResizeObserver on document.body and document.documentElement.
   * The host can then resize the UI container accordingly.
   *
   * MCP Apps only - ignored in ChatGPT.
   *
-  * @default true
+  * When undefined, the client-side adapter applies a default of true.
   */
  autoResize?: boolean;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Enable automatic size change notifications.
*
* When enabled, the UI automatically reports its size changes to the host
* using a ResizeObserver on document.body and document.documentElement.
* The host can then resize the UI container accordingly.
*
* MCP Apps only - ignored in ChatGPT.
*
* @default true
*/
autoResize?: boolean;
/**
* Enable automatic size change notifications.
*
* When enabled, the UI automatically reports its size changes to the host
* using a ResizeObserver on document.body and document.documentElement.
* The host can then resize the UI container accordingly.
*
* MCP Apps only - ignored in ChatGPT.
*
* @default true (applied by client-side adapter when undefined)
*/
autoResize?: boolean;
🤖 Prompt for AI Agents
In @packages/core/src/types/ui.ts around lines 119 - 130, The JSDoc for the
autoResize property is misleading: it currently says "@default true" but the
type does not apply a runtime default (the client-side adapter applies true when
the property is undefined). Update the comment for autoResize to state that the
type itself does not set a default and that the client-side adapter will treat
an omitted/undefined autoResize as true (or, if you prefer, remove the @default
tag and add a sentence clarifying that the default behavior is enforced by the
client adapter).


/**
* Dedicated domain for widget isolation.
* Advanced feature for security-sensitive applications.
Expand Down
52 changes: 52 additions & 0 deletions packages/core/tests/unit/ui-resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,55 @@ describe("UI Resource Registration", () => {
expect(ui.prefersBorder).toBe(true);
});

it("should support UI resources with autoResize", () => {
const autoResizeTool = defineTool({
description: "Auto-resize tool",
input: z.object({}),
output: z.object({}),
handler: async () => ({}),
ui: defineUI({
html: "<div>Auto Resize</div>",
autoResize: false,
}),
});

const app = createApp({
name: "test-app",
version: "1.0.0",
tools: {
autoResize: autoResizeTool,
},
});

const ui = app.tools.autoResize.ui as { autoResize: boolean };
expect(ui.autoResize).toBe(false);
});

it("should default autoResize to true when not specified", () => {
const defaultTool = defineTool({
description: "Default resize tool",
input: z.object({}),
output: z.object({}),
handler: async () => ({}),
ui: defineUI({
html: "<div>Default</div>",
}),
});

const app = createApp({
name: "test-app",
version: "1.0.0",
tools: {
default: defaultTool,
},
});

const ui = app.tools.default.ui as { autoResize?: boolean };
// autoResize is optional, so it may be undefined when not specified
// The default is applied at the client-side adapter level
expect(ui.autoResize).toBeUndefined();
});

it("should support UI resources with name and description", () => {
const dashboardTool = defineTool({
description: "Dashboard tool",
Expand Down Expand Up @@ -566,6 +615,7 @@ describe("UI Resource Registration", () => {
description: "A widget with all features",
widgetDescription: "Interactive widget for full features",
prefersBorder: true,
autoResize: false,
domain: "widget.example.com",
csp: {
connectDomains: ["https://api.example.com"],
Expand All @@ -590,6 +640,7 @@ describe("UI Resource Registration", () => {
description: string;
widgetDescription: string;
prefersBorder: boolean;
autoResize: boolean;
domain: string;
csp: {
connectDomains: string[];
Expand All @@ -604,6 +655,7 @@ describe("UI Resource Registration", () => {
expect(ui.description).toBe("A widget with all features");
expect(ui.widgetDescription).toBe("Interactive widget for full features");
expect(ui.prefersBorder).toBe(true);
expect(ui.autoResize).toBe(false);
expect(ui.domain).toBe("widget.example.com");
expect(ui.csp.connectDomains).toEqual(["https://api.example.com"]);
});
Expand Down
13 changes: 13 additions & 0 deletions packages/ui-react-builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ const app = createApp({
component: GreetingWidget,
name: "Greeting Widget",
prefersBorder: true,
// Optional: Disable automatic size notifications (default: true)
// autoResize: false,
}),
handler: async ({ name }) => ({
message: `Hello, ${name}!`,
Expand Down Expand Up @@ -189,6 +191,17 @@ If you need patterns not supported by auto-discovery, use `defineUI({ html: "...
| `defineReactUI` | Define a UI using a React component |
| `isReactUIDef` | Type guard to check if a value is a ReactUIDef |

#### `defineReactUI` Options

| Option | Type | Default | Description |
| --------------- | --------------- | ---------- | ------------------------------------------------------------------------------------------------------------ |
| `component` | `ComponentType` | (required) | React component to render |
| `name` | `string` | (required) | Display name for the UI |
| `description` | `string` | - | Description of the UI widget |
| `prefersBorder` | `boolean` | - | Hint to the host whether a border should be drawn |
| `autoResize` | `boolean` | `true` | Enable automatic size change notifications. Only supported in MCP Apps (Claude Desktop); ignored in ChatGPT. |
| `csp` | `CSPConfig` | - | Content Security Policy configuration (ChatGPT only) |

### Types

| Type | Description |
Expand Down
9 changes: 9 additions & 0 deletions packages/ui-react-builder/src/ast-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface ParsedReactUI {
importPath: string;
/** UI name from the defineReactUI call */
name: string;
/** Whether auto-resize is enabled (undefined means default/true) */
autoResize?: boolean;
}

/**
Expand Down Expand Up @@ -88,6 +90,7 @@ function parseDefineReactUICall(

let componentName: string | null = null;
let uiName: string | null = null;
let autoResize: boolean | undefined = undefined;

for (const prop of arg.properties) {
if (prop.type !== AST_NODE_TYPES.Property) continue;
Expand All @@ -106,6 +109,11 @@ function parseDefineReactUICall(
if (prop.value.type === AST_NODE_TYPES.Literal && typeof prop.value.value === "string") {
uiName = prop.value.value;
}
} else if (keyName === "autoResize") {
// Extract autoResize boolean
if (prop.value.type === AST_NODE_TYPES.Literal && typeof prop.value.value === "boolean") {
autoResize = prop.value.value;
}
}
}

Expand All @@ -122,6 +130,7 @@ function parseDefineReactUICall(
componentName,
importPath,
name: uiName,
autoResize,
};
}

Expand Down
7 changes: 6 additions & 1 deletion packages/ui-react-builder/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,16 @@ async function compileComponent(
const component = def.__component;
const componentName = component.name || "Component";
const defaultProps = def.__defaultProps;
const autoResize = def.__autoResize;

// Create the entry point using function serialization
// Note: The Vite plugin uses file paths for proper import resolution
// This build function falls back to function serialization (limited - doesn't capture imports)
const entryPoint = generateEntryPoint(`__COMPONENT_PLACEHOLDER__`, defaultProps);
const entryPoint = generateEntryPoint({
componentPath: `__COMPONENT_PLACEHOLDER__`,
defaultProps,
autoResize,
});

// Configure plugins
const plugins: esbuild.Plugin[] = [
Expand Down
3 changes: 2 additions & 1 deletion packages/ui-react-builder/src/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function toKebabCase(name: string): string {
* ```
*/
export function defineReactUI(definition: ReactUIInput): ReactUIDef {
const { component, defaultProps, outDir, ...rest } = definition;
const { component, defaultProps, outDir, autoResize, ...rest } = definition;

// Generate the output path from component name
const componentName = component.name ?? "component";
Expand All @@ -86,6 +86,7 @@ export function defineReactUI(definition: ReactUIInput): ReactUIDef {
__reactUI: true,
__component: component,
__defaultProps: defaultProps,
__autoResize: autoResize ?? true,
};
}

Expand Down
13 changes: 11 additions & 2 deletions packages/ui-react-builder/src/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export interface EntryPointOptions {
* Default props to pass to the component.
*/
defaultProps?: Record<string, unknown>;

/**
* Whether to enable automatic size change notifications.
* When undefined, uses the default (true).
*/
autoResize?: boolean;
}

/**
Expand Down Expand Up @@ -152,7 +158,7 @@ export function generateEntryPoint(
? { componentPath: componentPathOrOptions, defaultProps }
: componentPathOrOptions;

const { componentPath, componentExport = "default", defaultProps: props } = options;
const { componentPath, componentExport = "default", defaultProps: props, autoResize } = options;
const propsJson = props ? JSON.stringify(props) : "{}";

// Generate appropriate import statement based on export type
Expand All @@ -161,6 +167,9 @@ export function generateEntryPoint(
? `import Component from "${componentPath}";`
: `import { ${componentExport} as Component } from "${componentPath}";`;

// Generate AppsProvider props
const providerProps = autoResize === undefined ? "" : ` autoResize={${autoResize}}`;

return `
import React from "react";
import { createRoot } from "react-dom/client";
Expand All @@ -172,7 +181,7 @@ if (rootElement) {
const root = createRoot(rootElement);
root.render(
<React.StrictMode>
<AppsProvider>
<AppsProvider${providerProps}>
<Component {...${propsJson}} />
</AppsProvider>
</React.StrictMode>
Expand Down
17 changes: 17 additions & 0 deletions packages/ui-react-builder/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ export interface ReactUIInput extends Omit<UIDef, "html"> {
* @example "./dist/ui"
*/
outDir?: string;

/**
* Enable automatic size change notifications (MCP adapter only).
*
* When enabled, the UI automatically reports its size changes to the host
* using a ResizeObserver on document.body and document.documentElement.
* The host can then resize the UI container accordingly.
*
* @default true
*/
autoResize?: boolean;
}

// =============================================================================
Expand Down Expand Up @@ -91,6 +102,12 @@ export interface ReactUIDef extends UIDef {
* @internal
*/
__defaultProps?: Record<string, unknown>;

/**
* Whether to enable automatic size change notifications.
* @internal
*/
__autoResize?: boolean;
}

// =============================================================================
Expand Down
8 changes: 7 additions & 1 deletion packages/ui-react-builder/src/vite-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ interface DiscoveredUI {
name: string;
/** Generated key for output file */
key: string;
/** Whether auto-resize is enabled (undefined means default/true) */
autoResize?: boolean;
}

/**
Expand Down Expand Up @@ -319,6 +321,7 @@ async function discoverReactUIs(
componentPath,
name: ui.name,
key,
autoResize: ui.autoResize,
});
}

Expand Down Expand Up @@ -358,6 +361,9 @@ async function buildDiscoveredUIs(
for (const ui of discovered) {
const importPath = toEsbuildImportSpecifier(ui.componentPath);

// Generate AppsProvider props based on autoResize setting
const providerProps = ui.autoResize === undefined ? "" : ` autoResize={${ui.autoResize}}`;

// Generate entry point code
const entryCode = `
import React from "react";
Expand All @@ -370,7 +376,7 @@ if (rootElement) {
const root = createRoot(rootElement);
root.render(
<React.StrictMode>
<AppsProvider>
<AppsProvider${providerProps}>
<Component />
</AppsProvider>
</React.StrictMode>
Expand Down
58 changes: 58 additions & 0 deletions packages/ui-react-builder/tests/unit/ast-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,5 +803,63 @@ defineReactUI({

await expect(parseReactUIDefinitions(content)).rejects.toThrow();
});

it("should extract autoResize property from defineReactUI", async () => {
const content = `
import { defineReactUI } from "@mcp-apps-kit/ui-react-builder";
import { Widget } from "./Widget";

defineReactUI({
component: Widget,
name: "Widget with autoResize",
autoResize: false,
});
`;

const results = await parseReactUIDefinitions(content);

expect(results).toHaveLength(1);
expect(results[0]).toEqual({
componentName: "Widget",
importPath: "./Widget",
name: "Widget with autoResize",
autoResize: false,
});
});

it("should extract autoResize: true property", async () => {
const content = `
import { defineReactUI } from "@mcp-apps-kit/ui-react-builder";
import { Widget } from "./Widget";

defineReactUI({
component: Widget,
name: "Widget",
autoResize: true,
});
`;

const results = await parseReactUIDefinitions(content);

expect(results).toHaveLength(1);
expect(results[0].autoResize).toBe(true);
});

it("should handle defineReactUI without autoResize property", async () => {
const content = `
import { defineReactUI } from "@mcp-apps-kit/ui-react-builder";
import { Widget } from "./Widget";

defineReactUI({
component: Widget,
name: "Widget without autoResize",
});
`;

const results = await parseReactUIDefinitions(content);

expect(results).toHaveLength(1);
expect(results[0].autoResize).toBeUndefined();
});
});
});
Loading