@leanmcp/ui 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # @leanmcp/ui
2
+
3
+ **MCP-Native UI SDK for React** - Build rich, interactive MCP Apps with first-class tool integration.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @leanmcp/ui
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```tsx
14
+ import { AppProvider, ToolButton } from '@leanmcp/ui';
15
+ import '@leanmcp/ui/styles.css';
16
+
17
+ function MyApp() {
18
+ return (
19
+ <AppProvider appInfo={{ name: 'MyApp', version: '1.0.0' }}>
20
+ <ToolButton tool="refresh-data" resultDisplay="toast">
21
+ Refresh
22
+ </ToolButton>
23
+ </AppProvider>
24
+ );
25
+ }
26
+ ```
27
+
28
+ ## Components
29
+
30
+ ### MCP-Native Components
31
+
32
+ | Component | Description |
33
+ |-----------|-------------|
34
+ | `ToolButton` | Button with tool execution, confirmation, result display |
35
+ | `ToolSelect` | Select with tool-based options and selection callbacks |
36
+ | `ToolInput` | Input with debounced search and autocomplete |
37
+ | `ToolForm` | Form with multiple field types (text, select, checkbox, slider) |
38
+ | `ToolDataGrid` | Table with server-side pagination, sorting, row actions |
39
+ | `ResourceView` | Display MCP server resources with auto-refresh |
40
+ | `StreamingContent` | Render streaming/partial tool data |
41
+
42
+ ### Utility Components
43
+
44
+ | Component | Description |
45
+ |-----------|-------------|
46
+ | `RequireConnection` | Guard wrapper for MCP connection state |
47
+ | `ToolErrorBoundary` | Error boundary with retry for tool errors |
48
+ | `ToolProvider` | Scoped configuration context |
49
+
50
+ ## Hooks
51
+
52
+ | Hook | Description |
53
+ |------|-------------|
54
+ | `useTool` | Call tools with retry, abort, transformation |
55
+ | `useToolStream` | Handle streaming tool input |
56
+ | `useResource` | Read MCP resources with auto-refresh |
57
+ | `useMessage` | Send messages to host chat |
58
+ | `useHostContext` | Access host theme and viewport |
59
+
60
+ ## Examples
61
+
62
+ ### ToolButton with Confirmation
63
+
64
+ ```tsx
65
+ <ToolButton
66
+ tool="delete-item"
67
+ args={{ id: item.id }}
68
+ confirm={{
69
+ title: 'Delete Item?',
70
+ description: 'This cannot be undone.'
71
+ }}
72
+ variant="destructive"
73
+ >
74
+ Delete
75
+ </ToolButton>
76
+ ```
77
+
78
+ ### ToolSelect with Dynamic Options
79
+
80
+ ```tsx
81
+ <ToolSelect
82
+ optionsTool="list-categories"
83
+ transformOptions={(r) => r.categories.map(c => ({
84
+ value: c.id,
85
+ label: c.name
86
+ }))}
87
+ onSelectTool="set-category"
88
+ argName="categoryId"
89
+ />
90
+ ```
91
+
92
+ ### ToolDataGrid
93
+
94
+ ```tsx
95
+ <ToolDataGrid
96
+ dataTool="list-users"
97
+ columns={[
98
+ { key: 'name', header: 'Name', sortable: true },
99
+ { key: 'email', header: 'Email' },
100
+ { key: 'status', header: 'Status' }
101
+ ]}
102
+ transformData={(r) => ({ rows: r.users, total: r.total })}
103
+ rowActions={[
104
+ { label: 'Edit', tool: 'edit-user' }
105
+ ]}
106
+ pagination
107
+ />
108
+ ```
109
+
110
+ ### ToolForm
111
+
112
+ ```tsx
113
+ <ToolForm
114
+ toolName="create-item"
115
+ fields={[
116
+ { name: 'title', label: 'Title', required: true },
117
+ { name: 'priority', label: 'Priority', type: 'select',
118
+ options: [
119
+ { value: 'low', label: 'Low' },
120
+ { value: 'high', label: 'High' }
121
+ ]
122
+ },
123
+ { name: 'notify', label: 'Send notifications', type: 'switch' }
124
+ ]}
125
+ showSuccessToast
126
+ />
127
+ ```
128
+
129
+ ## Theming
130
+
131
+ The SDK uses CSS variables compatible with MCP host theming. Import the styles:
132
+
133
+ ```tsx
134
+ import '@leanmcp/ui/styles.css';
135
+ ```
136
+
137
+ The styles automatically adapt to the host's theme (light/dark).
138
+
139
+ ## Testing
140
+
141
+ Use `MockAppProvider` for unit testing:
142
+
143
+ ```tsx
144
+ import { MockAppProvider } from '@leanmcp/ui/testing';
145
+
146
+ test('renders tool result', () => {
147
+ render(
148
+ <MockAppProvider
149
+ toolResult={{ data: 'test' }}
150
+ callTool={async () => ({ content: [{ type: 'text', text: '{}' }] })}
151
+ >
152
+ <MyComponent />
153
+ </MockAppProvider>
154
+ );
155
+ });
156
+ ```
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ require('reflect-metadata');
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var UI_APP_COMPONENT_KEY = "ui:app:component";
8
+ var UI_APP_URI_KEY = "ui:app:uri";
9
+ var UI_APP_OPTIONS_KEY = "ui:app:options";
10
+ function UIApp(options) {
11
+ return (target, propertyKey, descriptor) => {
12
+ const methodName = String(propertyKey);
13
+ const className = target.constructor.name.toLowerCase().replace("service", "");
14
+ const uri = options.uri ?? `ui://${className}/${methodName}`;
15
+ Reflect.defineMetadata(UI_APP_COMPONENT_KEY, options.component, descriptor.value);
16
+ Reflect.defineMetadata(UI_APP_URI_KEY, uri, descriptor.value);
17
+ Reflect.defineMetadata(UI_APP_OPTIONS_KEY, options, descriptor.value);
18
+ const existingMeta = Reflect.getMetadata("tool:meta", descriptor.value) || {};
19
+ Reflect.defineMetadata("tool:meta", {
20
+ ...existingMeta,
21
+ "ui/resourceUri": uri
22
+ }, descriptor.value);
23
+ return descriptor;
24
+ };
25
+ }
26
+ __name(UIApp, "UIApp");
27
+ function getUIAppMetadata(target) {
28
+ const component = Reflect.getMetadata(UI_APP_COMPONENT_KEY, target);
29
+ if (!component) return void 0;
30
+ return {
31
+ component,
32
+ uri: Reflect.getMetadata(UI_APP_URI_KEY, target),
33
+ ...Reflect.getMetadata(UI_APP_OPTIONS_KEY, target)
34
+ };
35
+ }
36
+ __name(getUIAppMetadata, "getUIAppMetadata");
37
+ function getUIAppUri(target) {
38
+ return Reflect.getMetadata(UI_APP_URI_KEY, target);
39
+ }
40
+ __name(getUIAppUri, "getUIAppUri");
41
+
42
+ exports.UIApp = UIApp;
43
+ exports.UI_APP_COMPONENT_KEY = UI_APP_COMPONENT_KEY;
44
+ exports.UI_APP_OPTIONS_KEY = UI_APP_OPTIONS_KEY;
45
+ exports.UI_APP_URI_KEY = UI_APP_URI_KEY;
46
+ exports.__name = __name;
47
+ exports.getUIAppMetadata = getUIAppMetadata;
48
+ exports.getUIAppUri = getUIAppUri;
49
+ //# sourceMappingURL=chunk-3PV26V5F.js.map
50
+ //# sourceMappingURL=chunk-3PV26V5F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/decorator/UIApp.ts"],"names":["UI_APP_COMPONENT_KEY","UI_APP_URI_KEY","UI_APP_OPTIONS_KEY","UIApp","options","target","propertyKey","descriptor","methodName","String","className","name","toLowerCase","replace","uri","Reflect","defineMetadata","component","value","existingMeta","getMetadata","getUIAppMetadata","undefined","getUIAppUri"],"mappings":";;;;;;AAcO,IAAMA,oBAAAA,GAAuB;AAC7B,IAAMC,cAAAA,GAAiB;AACvB,IAAMC,kBAAAA,GAAqB;AA0C3B,SAASC,MAAMC,OAAAA,EAAqB;AACvC,EAAA,OAAO,CAACC,MAAAA,EAAgBC,WAAAA,EAA8BC,UAAAA,KAAAA;AAClD,IAAA,MAAMC,UAAAA,GAAaC,OAAOH,WAAAA,CAAAA;AAE1B,IAAA,MAAMI,SAAAA,GAAYL,OAAO,WAAA,CAAYM,IAAAA,CAAKC,aAAW,CAAGC,OAAAA,CAAQ,WAAW,EAAA,CAAA;AAG3E,IAAA,MAAMC,MAAMV,OAAAA,CAAQU,GAAAA,IAAO,CAAA,KAAA,EAAQJ,SAAAA,IAAaF,UAAAA,CAAAA,CAAAA;AAGhDO,IAAAA,OAAAA,CAAQC,cAAAA,CAAehB,oBAAAA,EAAsBI,OAAAA,CAAQa,SAAAA,EAAWV,WAAWW,KAAK,CAAA;AAChFH,IAAAA,OAAAA,CAAQC,cAAAA,CAAef,cAAAA,EAAgBa,GAAAA,EAAKP,UAAAA,CAAWW,KAAK,CAAA;AAC5DH,IAAAA,OAAAA,CAAQC,cAAAA,CAAed,kBAAAA,EAAoBE,OAAAA,EAASG,UAAAA,CAAWW,KAAK,CAAA;AAIpE,IAAA,MAAMC,eAAeJ,OAAAA,CAAQK,WAAAA,CAAY,aAAab,UAAAA,CAAWW,KAAK,KAAK,EAAC;AAC5EH,IAAAA,OAAAA,CAAQC,eAAe,WAAA,EAAa;MAChC,GAAGG,YAAAA;MACH,gBAAA,EAAkBL;AACtB,KAAA,EAAGP,WAAWW,KAAK,CAAA;AAEnB,IAAA,OAAOX,UAAAA;AACX,EAAA,CAAA;AACJ;AAxBgBJ,MAAAA,CAAAA,KAAAA,EAAAA,OAAAA,CAAAA;AA6BT,SAASkB,iBAAiBhB,MAAAA,EAAgB;AAC7C,EAAA,MAAMY,SAAAA,GAAYF,OAAAA,CAAQK,WAAAA,CAAYpB,oBAAAA,EAAsBK,MAAAA,CAAAA;AAC5D,EAAA,IAAI,CAACY,WAAW,OAAOK,MAAAA;AAEvB,EAAA,OAAO;AACHL,IAAAA,SAAAA;IACAH,GAAAA,EAAKC,OAAAA,CAAQK,WAAAA,CAAYnB,cAAAA,EAAgBI,MAAAA,CAAAA;IACzC,GAAGU,OAAAA,CAAQK,WAAAA,CAAYlB,kBAAAA,EAAoBG,MAAAA;AAC/C,GAAA;AACJ;AATgBgB,MAAAA,CAAAA,gBAAAA,EAAAA,kBAAAA,CAAAA;AAcT,SAASE,YAAYlB,MAAAA,EAAgB;AACxC,EAAA,OAAOU,OAAAA,CAAQK,WAAAA,CAAYnB,cAAAA,EAAgBI,MAAAA,CAAAA;AAC/C;AAFgBkB,MAAAA,CAAAA,WAAAA,EAAAA,aAAAA,CAAAA","file":"chunk-3PV26V5F.js","sourcesContent":["/**\r\n * @UIApp Decorator\r\n * \r\n * Links an MCP tool to a React UI component.\r\n * When applied to a tool method, it:\r\n * 1. Adds _meta.ui/resourceUri to the tool definition\r\n * 2. Auto-registers a resource that renders the component to HTML\r\n * \r\n * This decorator is designed to work with @leanmcp/core decorators.\r\n */\r\nimport 'reflect-metadata';\r\nimport type React from 'react';\r\n\r\n// Metadata keys\r\nexport const UI_APP_COMPONENT_KEY = 'ui:app:component';\r\nexport const UI_APP_URI_KEY = 'ui:app:uri';\r\nexport const UI_APP_OPTIONS_KEY = 'ui:app:options';\r\n\r\n/**\r\n * Options for @UIApp decorator\r\n */\r\nexport interface UIAppOptions {\r\n /** \r\n * React component or path to component file (relative to service file).\r\n * - Use path string (e.g., './WeatherCard') for CLI build - avoids importing browser code in server\r\n * - Use component reference for direct SSR rendering\r\n */\r\n component: React.ComponentType<any> | string;\r\n /** Custom resource URI (auto-generated if not provided) */\r\n uri?: string;\r\n /** HTML document title */\r\n title?: string;\r\n /** Additional CSS styles */\r\n styles?: string;\r\n}\r\n\r\n\r\n/**\r\n * Decorator that links a tool to a UI component.\r\n * \r\n * When the tool is called, the host will fetch the UI resource\r\n * which returns the component rendered as HTML.\r\n * \r\n * @example\r\n * ```typescript\r\n * import { Tool } from '@leanmcp/core';\r\n * import { UIApp } from '@leanmcp/ui';\r\n * import { WeatherCard } from './WeatherCard';\r\n * \r\n * class WeatherService {\r\n * @Tool({ description: 'Get weather for a city' })\r\n * @UIApp({ component: WeatherCard })\r\n * async getWeather(args: { city: string }) {\r\n * return { city: args.city, temp: 22 };\r\n * }\r\n * }\r\n * ```\r\n */\r\nexport function UIApp(options: UIAppOptions): MethodDecorator {\r\n return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {\r\n const methodName = String(propertyKey);\r\n // Use same pattern as @Resource in @leanmcp/core: className without 'service' suffix\r\n const className = target.constructor.name.toLowerCase().replace('service', '');\r\n\r\n // Generate URI using ui:// scheme (required by ext-apps hosts)\r\n const uri = options.uri ?? `ui://${className}/${methodName}`;\r\n\r\n // Store metadata on the method\r\n Reflect.defineMetadata(UI_APP_COMPONENT_KEY, options.component, descriptor.value!);\r\n Reflect.defineMetadata(UI_APP_URI_KEY, uri, descriptor.value!);\r\n Reflect.defineMetadata(UI_APP_OPTIONS_KEY, options, descriptor.value!);\r\n\r\n // Also store the resource URI for the @Tool decorator to pick up\r\n // The @Tool decorator in @leanmcp/core should check for this\r\n const existingMeta = Reflect.getMetadata('tool:meta', descriptor.value) || {};\r\n Reflect.defineMetadata('tool:meta', {\r\n ...existingMeta,\r\n 'ui/resourceUri': uri,\r\n }, descriptor.value!);\r\n\r\n return descriptor;\r\n };\r\n}\r\n\r\n/**\r\n * Helper to get UIApp metadata from a method\r\n */\r\nexport function getUIAppMetadata(target: Function): UIAppOptions | undefined {\r\n const component = Reflect.getMetadata(UI_APP_COMPONENT_KEY, target);\r\n if (!component) return undefined;\r\n\r\n return {\r\n component,\r\n uri: Reflect.getMetadata(UI_APP_URI_KEY, target),\r\n ...Reflect.getMetadata(UI_APP_OPTIONS_KEY, target),\r\n };\r\n}\r\n\r\n/**\r\n * Helper to get the resource URI from a method\r\n */\r\nexport function getUIAppUri(target: Function): string | undefined {\r\n return Reflect.getMetadata(UI_APP_URI_KEY, target);\r\n}\r\n"]}
@@ -0,0 +1,42 @@
1
+ import 'reflect-metadata';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
+ var UI_APP_COMPONENT_KEY = "ui:app:component";
6
+ var UI_APP_URI_KEY = "ui:app:uri";
7
+ var UI_APP_OPTIONS_KEY = "ui:app:options";
8
+ function UIApp(options) {
9
+ return (target, propertyKey, descriptor) => {
10
+ const methodName = String(propertyKey);
11
+ const className = target.constructor.name.toLowerCase().replace("service", "");
12
+ const uri = options.uri ?? `ui://${className}/${methodName}`;
13
+ Reflect.defineMetadata(UI_APP_COMPONENT_KEY, options.component, descriptor.value);
14
+ Reflect.defineMetadata(UI_APP_URI_KEY, uri, descriptor.value);
15
+ Reflect.defineMetadata(UI_APP_OPTIONS_KEY, options, descriptor.value);
16
+ const existingMeta = Reflect.getMetadata("tool:meta", descriptor.value) || {};
17
+ Reflect.defineMetadata("tool:meta", {
18
+ ...existingMeta,
19
+ "ui/resourceUri": uri
20
+ }, descriptor.value);
21
+ return descriptor;
22
+ };
23
+ }
24
+ __name(UIApp, "UIApp");
25
+ function getUIAppMetadata(target) {
26
+ const component = Reflect.getMetadata(UI_APP_COMPONENT_KEY, target);
27
+ if (!component) return void 0;
28
+ return {
29
+ component,
30
+ uri: Reflect.getMetadata(UI_APP_URI_KEY, target),
31
+ ...Reflect.getMetadata(UI_APP_OPTIONS_KEY, target)
32
+ };
33
+ }
34
+ __name(getUIAppMetadata, "getUIAppMetadata");
35
+ function getUIAppUri(target) {
36
+ return Reflect.getMetadata(UI_APP_URI_KEY, target);
37
+ }
38
+ __name(getUIAppUri, "getUIAppUri");
39
+
40
+ export { UIApp, UI_APP_COMPONENT_KEY, UI_APP_OPTIONS_KEY, UI_APP_URI_KEY, __name, getUIAppMetadata, getUIAppUri };
41
+ //# sourceMappingURL=chunk-WORZ46KI.mjs.map
42
+ //# sourceMappingURL=chunk-WORZ46KI.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/decorator/UIApp.ts"],"names":["UI_APP_COMPONENT_KEY","UI_APP_URI_KEY","UI_APP_OPTIONS_KEY","UIApp","options","target","propertyKey","descriptor","methodName","String","className","name","toLowerCase","replace","uri","Reflect","defineMetadata","component","value","existingMeta","getMetadata","getUIAppMetadata","undefined","getUIAppUri"],"mappings":";;;;AAcO,IAAMA,oBAAAA,GAAuB;AAC7B,IAAMC,cAAAA,GAAiB;AACvB,IAAMC,kBAAAA,GAAqB;AA0C3B,SAASC,MAAMC,OAAAA,EAAqB;AACvC,EAAA,OAAO,CAACC,MAAAA,EAAgBC,WAAAA,EAA8BC,UAAAA,KAAAA;AAClD,IAAA,MAAMC,UAAAA,GAAaC,OAAOH,WAAAA,CAAAA;AAE1B,IAAA,MAAMI,SAAAA,GAAYL,OAAO,WAAA,CAAYM,IAAAA,CAAKC,aAAW,CAAGC,OAAAA,CAAQ,WAAW,EAAA,CAAA;AAG3E,IAAA,MAAMC,MAAMV,OAAAA,CAAQU,GAAAA,IAAO,CAAA,KAAA,EAAQJ,SAAAA,IAAaF,UAAAA,CAAAA,CAAAA;AAGhDO,IAAAA,OAAAA,CAAQC,cAAAA,CAAehB,oBAAAA,EAAsBI,OAAAA,CAAQa,SAAAA,EAAWV,WAAWW,KAAK,CAAA;AAChFH,IAAAA,OAAAA,CAAQC,cAAAA,CAAef,cAAAA,EAAgBa,GAAAA,EAAKP,UAAAA,CAAWW,KAAK,CAAA;AAC5DH,IAAAA,OAAAA,CAAQC,cAAAA,CAAed,kBAAAA,EAAoBE,OAAAA,EAASG,UAAAA,CAAWW,KAAK,CAAA;AAIpE,IAAA,MAAMC,eAAeJ,OAAAA,CAAQK,WAAAA,CAAY,aAAab,UAAAA,CAAWW,KAAK,KAAK,EAAC;AAC5EH,IAAAA,OAAAA,CAAQC,eAAe,WAAA,EAAa;MAChC,GAAGG,YAAAA;MACH,gBAAA,EAAkBL;AACtB,KAAA,EAAGP,WAAWW,KAAK,CAAA;AAEnB,IAAA,OAAOX,UAAAA;AACX,EAAA,CAAA;AACJ;AAxBgBJ,MAAAA,CAAAA,KAAAA,EAAAA,OAAAA,CAAAA;AA6BT,SAASkB,iBAAiBhB,MAAAA,EAAgB;AAC7C,EAAA,MAAMY,SAAAA,GAAYF,OAAAA,CAAQK,WAAAA,CAAYpB,oBAAAA,EAAsBK,MAAAA,CAAAA;AAC5D,EAAA,IAAI,CAACY,WAAW,OAAOK,MAAAA;AAEvB,EAAA,OAAO;AACHL,IAAAA,SAAAA;IACAH,GAAAA,EAAKC,OAAAA,CAAQK,WAAAA,CAAYnB,cAAAA,EAAgBI,MAAAA,CAAAA;IACzC,GAAGU,OAAAA,CAAQK,WAAAA,CAAYlB,kBAAAA,EAAoBG,MAAAA;AAC/C,GAAA;AACJ;AATgBgB,MAAAA,CAAAA,gBAAAA,EAAAA,kBAAAA,CAAAA;AAcT,SAASE,YAAYlB,MAAAA,EAAgB;AACxC,EAAA,OAAOU,OAAAA,CAAQK,WAAAA,CAAYnB,cAAAA,EAAgBI,MAAAA,CAAAA;AAC/C;AAFgBkB,MAAAA,CAAAA,WAAAA,EAAAA,aAAAA,CAAAA","file":"chunk-WORZ46KI.mjs","sourcesContent":["/**\r\n * @UIApp Decorator\r\n * \r\n * Links an MCP tool to a React UI component.\r\n * When applied to a tool method, it:\r\n * 1. Adds _meta.ui/resourceUri to the tool definition\r\n * 2. Auto-registers a resource that renders the component to HTML\r\n * \r\n * This decorator is designed to work with @leanmcp/core decorators.\r\n */\r\nimport 'reflect-metadata';\r\nimport type React from 'react';\r\n\r\n// Metadata keys\r\nexport const UI_APP_COMPONENT_KEY = 'ui:app:component';\r\nexport const UI_APP_URI_KEY = 'ui:app:uri';\r\nexport const UI_APP_OPTIONS_KEY = 'ui:app:options';\r\n\r\n/**\r\n * Options for @UIApp decorator\r\n */\r\nexport interface UIAppOptions {\r\n /** \r\n * React component or path to component file (relative to service file).\r\n * - Use path string (e.g., './WeatherCard') for CLI build - avoids importing browser code in server\r\n * - Use component reference for direct SSR rendering\r\n */\r\n component: React.ComponentType<any> | string;\r\n /** Custom resource URI (auto-generated if not provided) */\r\n uri?: string;\r\n /** HTML document title */\r\n title?: string;\r\n /** Additional CSS styles */\r\n styles?: string;\r\n}\r\n\r\n\r\n/**\r\n * Decorator that links a tool to a UI component.\r\n * \r\n * When the tool is called, the host will fetch the UI resource\r\n * which returns the component rendered as HTML.\r\n * \r\n * @example\r\n * ```typescript\r\n * import { Tool } from '@leanmcp/core';\r\n * import { UIApp } from '@leanmcp/ui';\r\n * import { WeatherCard } from './WeatherCard';\r\n * \r\n * class WeatherService {\r\n * @Tool({ description: 'Get weather for a city' })\r\n * @UIApp({ component: WeatherCard })\r\n * async getWeather(args: { city: string }) {\r\n * return { city: args.city, temp: 22 };\r\n * }\r\n * }\r\n * ```\r\n */\r\nexport function UIApp(options: UIAppOptions): MethodDecorator {\r\n return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {\r\n const methodName = String(propertyKey);\r\n // Use same pattern as @Resource in @leanmcp/core: className without 'service' suffix\r\n const className = target.constructor.name.toLowerCase().replace('service', '');\r\n\r\n // Generate URI using ui:// scheme (required by ext-apps hosts)\r\n const uri = options.uri ?? `ui://${className}/${methodName}`;\r\n\r\n // Store metadata on the method\r\n Reflect.defineMetadata(UI_APP_COMPONENT_KEY, options.component, descriptor.value!);\r\n Reflect.defineMetadata(UI_APP_URI_KEY, uri, descriptor.value!);\r\n Reflect.defineMetadata(UI_APP_OPTIONS_KEY, options, descriptor.value!);\r\n\r\n // Also store the resource URI for the @Tool decorator to pick up\r\n // The @Tool decorator in @leanmcp/core should check for this\r\n const existingMeta = Reflect.getMetadata('tool:meta', descriptor.value) || {};\r\n Reflect.defineMetadata('tool:meta', {\r\n ...existingMeta,\r\n 'ui/resourceUri': uri,\r\n }, descriptor.value!);\r\n\r\n return descriptor;\r\n };\r\n}\r\n\r\n/**\r\n * Helper to get UIApp metadata from a method\r\n */\r\nexport function getUIAppMetadata(target: Function): UIAppOptions | undefined {\r\n const component = Reflect.getMetadata(UI_APP_COMPONENT_KEY, target);\r\n if (!component) return undefined;\r\n\r\n return {\r\n component,\r\n uri: Reflect.getMetadata(UI_APP_URI_KEY, target),\r\n ...Reflect.getMetadata(UI_APP_OPTIONS_KEY, target),\r\n };\r\n}\r\n\r\n/**\r\n * Helper to get the resource URI from a method\r\n */\r\nexport function getUIAppUri(target: Function): string | undefined {\r\n return Reflect.getMetadata(UI_APP_URI_KEY, target);\r\n}\r\n"]}