@webmcp-auto-ui/core 0.5.0 → 2.5.4

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/src/utils.ts CHANGED
@@ -75,6 +75,86 @@ export function signalCompletion(completionEventName: string, detail?: unknown):
75
75
  // Item 19 — sanitizeSchema (for AI SDKs)
76
76
  // ---------------------------------------------------------------------------
77
77
 
78
+ export interface SchemaPatch {
79
+ path: string; // e.g. "root", "properties.params", "properties.data.items"
80
+ type: 'additionalProperties'; // what was patched
81
+ value: false;
82
+ }
83
+
84
+ /**
85
+ * Sanitize schema AND report patches applied for strict tool use.
86
+ * Use this when you need visibility into what was changed.
87
+ */
88
+ export function sanitizeSchemaWithReport(schema: JsonSchema): { schema: JsonSchema; patches: SchemaPatch[] } {
89
+ if (typeof schema === 'boolean') return { schema, patches: [] };
90
+ const patches: SchemaPatch[] = [];
91
+ const result = sanitizeSchemaObjectWithReport({ ...schema } as JsonSchemaObject, new WeakSet(), 'root', patches);
92
+ return { schema: result, patches };
93
+ }
94
+
95
+ function sanitizeSchemaObjectWithReport(obj: JsonSchemaObject, seen: WeakSet<object>, path: string, patches: SchemaPatch[]): JsonSchemaObject {
96
+ if (seen.has(obj)) return obj;
97
+ seen.add(obj);
98
+
99
+ const result = { ...obj };
100
+
101
+ // Remove composition keywords that AI SDKs can't handle
102
+ delete result.oneOf;
103
+ delete result.anyOf;
104
+ delete result.allOf;
105
+ delete result.not;
106
+
107
+ // Remove conditional keywords
108
+ delete result.if;
109
+ delete result.then;
110
+ delete result.else;
111
+
112
+ // Remove $ref (needs dereferencing first)
113
+ delete result.$ref;
114
+
115
+ // Recursively sanitize nested schemas
116
+ if (result.properties) {
117
+ const sanitizedProps: Record<string, JsonSchema> = {};
118
+ for (const [key, value] of Object.entries(result.properties)) {
119
+ sanitizedProps[key] = typeof value === 'boolean'
120
+ ? value
121
+ : sanitizeSchemaObjectWithReport({ ...value } as JsonSchemaObject, seen, `${path}.properties.${key}`, patches);
122
+ }
123
+ result.properties = sanitizedProps;
124
+ }
125
+
126
+ if (result.items) {
127
+ if (Array.isArray(result.items)) {
128
+ result.items = result.items.map((item, i) =>
129
+ typeof item === 'boolean'
130
+ ? item
131
+ : sanitizeSchemaObjectWithReport({ ...item } as JsonSchemaObject, seen, `${path}.items[${i}]`, patches)
132
+ );
133
+ } else if (typeof result.items !== 'boolean') {
134
+ result.items = sanitizeSchemaObjectWithReport({ ...result.items } as JsonSchemaObject, seen, `${path}.items`, patches);
135
+ }
136
+ }
137
+
138
+ // Strict tool use: ensure additionalProperties is set on any object type
139
+ if ((result.type === 'object' || result.properties) && !('additionalProperties' in result)) {
140
+ result.additionalProperties = false;
141
+ patches.push({ path, type: 'additionalProperties', value: false });
142
+ }
143
+
144
+ if (result.additionalProperties && typeof result.additionalProperties === 'object') {
145
+ result.additionalProperties = sanitizeSchemaObjectWithReport(
146
+ { ...result.additionalProperties } as JsonSchemaObject,
147
+ seen,
148
+ `${path}.additionalProperties`,
149
+ patches
150
+ );
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+
157
+
78
158
  /**
79
159
  * Strip JSON Schema keywords that cause errors in AI SDKs
80
160
  * (e.g., Vercel AI SDK rejects oneOf/anyOf on non-STRING types).
@@ -129,6 +209,12 @@ function sanitizeSchemaObject(obj: JsonSchemaObject, seen: WeakSet<object>): Jso
129
209
  }
130
210
  }
131
211
 
212
+ // Strict tool use: ensure additionalProperties is set on any object type
213
+ // Anthropic requires this on ALL objects, even without properties
214
+ if ((result.type === 'object' || result.properties) && !('additionalProperties' in result)) {
215
+ result.additionalProperties = false;
216
+ }
217
+
132
218
  if (result.additionalProperties && typeof result.additionalProperties === 'object') {
133
219
  result.additionalProperties = sanitizeSchemaObject(
134
220
  { ...result.additionalProperties } as JsonSchemaObject,
@@ -139,6 +225,83 @@ function sanitizeSchemaObject(obj: JsonSchemaObject, seen: WeakSet<object>): Jso
139
225
  return result;
140
226
  }
141
227
 
228
+ // ---------------------------------------------------------------------------
229
+ // flattenSchema — flatten nested object properties for small LLMs
230
+ // ---------------------------------------------------------------------------
231
+
232
+ /**
233
+ * Flatten nested object schemas into flat key__subkey properties.
234
+ * Returns { schema, pathMap } where pathMap maps flat keys to nested paths.
235
+ * Only flattens properties of type "object" with their own properties.
236
+ */
237
+ export function flattenSchema(schema: JsonSchema): { schema: JsonSchema; pathMap: Record<string, string[]> } {
238
+ if (typeof schema === 'boolean') return { schema, pathMap: {} };
239
+ const obj = schema as JsonSchemaObject;
240
+ if (!obj.properties) return { schema, pathMap: {} };
241
+
242
+ const flatProps: Record<string, JsonSchema> = {};
243
+ const pathMap: Record<string, string[]> = {};
244
+ const required = new Set(obj.required ?? []);
245
+ const flatRequired: string[] = [];
246
+
247
+ for (const [key, prop] of Object.entries(obj.properties)) {
248
+ if (typeof prop !== 'boolean' && prop.type === 'object' && prop.properties) {
249
+ // Flatten nested object
250
+ const nestedRequired = new Set((prop as JsonSchemaObject).required ?? []);
251
+ for (const [subKey, subProp] of Object.entries(prop.properties!)) {
252
+ const flatKey = `${key}__${subKey}`;
253
+ flatProps[flatKey] = subProp;
254
+ pathMap[flatKey] = [key, subKey];
255
+ if (required.has(key) && nestedRequired.has(subKey)) {
256
+ flatRequired.push(flatKey);
257
+ }
258
+ }
259
+ } else {
260
+ // Keep as-is
261
+ flatProps[key] = prop;
262
+ pathMap[key] = [key];
263
+ if (required.has(key)) flatRequired.push(key);
264
+ }
265
+ }
266
+
267
+ const result: JsonSchemaObject = {
268
+ ...obj,
269
+ properties: flatProps,
270
+ };
271
+ if (flatRequired.length > 0) result.required = flatRequired;
272
+ else delete result.required;
273
+
274
+ return { schema: result, pathMap };
275
+ }
276
+
277
+ /**
278
+ * Unflatten params using a pathMap from flattenSchema.
279
+ * Converts { a__b: 1, a__c: 2, d: 3 } back to { a: { b: 1, c: 2 }, d: 3 }
280
+ */
281
+ export function unflattenParams(
282
+ params: Record<string, unknown>,
283
+ pathMap: Record<string, string[]>
284
+ ): Record<string, unknown> {
285
+ const result: Record<string, unknown> = {};
286
+ for (const [flatKey, value] of Object.entries(params)) {
287
+ const path = pathMap[flatKey] ?? flatKey.split('__');
288
+ if (path.length === 1) {
289
+ result[path[0]] = value;
290
+ } else {
291
+ // Build nested structure
292
+ let current = result;
293
+ for (let i = 0; i < path.length - 1; i++) {
294
+ if (!(path[i] in current) || typeof current[path[i]] !== 'object') {
295
+ current[path[i]] = {};
296
+ }
297
+ current = current[path[i]] as Record<string, unknown>;
298
+ }
299
+ current[path[path.length - 1]] = value;
300
+ }
301
+ }
302
+ return result;
303
+ }
304
+
142
305
  // ---------------------------------------------------------------------------
143
306
  // createToolGroup — ergonomic helper for SPA route-based tool registration
144
307
  // ---------------------------------------------------------------------------
@@ -0,0 +1,13 @@
1
+ import type { JsonSchema } from './types.js';
2
+ export interface ValidationError {
3
+ path: string;
4
+ message: string;
5
+ keyword: string;
6
+ expected?: unknown;
7
+ actual?: unknown;
8
+ }
9
+ export interface ValidationResult {
10
+ valid: boolean;
11
+ errors: ValidationError[];
12
+ }
13
+ export declare function validateJsonSchema(value: unknown, schema: JsonSchema, path?: string): ValidationResult;
@@ -0,0 +1,3 @@
1
+ import type { ToolExecuteResult } from './types.js';
2
+ export declare function textResult(text: string): ToolExecuteResult;
3
+ export declare function jsonResult(data: unknown): ToolExecuteResult;
@@ -1,11 +1,14 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // @webmcp-auto-ui/core — WebMCP helpers
3
- // Thin layer on top of polyfill.ts: skill registry + result builders.
3
+ // Result builders for tool execute callbacks.
4
4
  // Zero additional dependencies. SSR-safe.
5
+ //
6
+ // NOTE: The skill registry that used to live here (SkillDef, registerSkill,
7
+ // unregisterSkill, etc.) has been removed. The canonical skill type and
8
+ // registry now live in @webmcp-auto-ui/sdk (packages/sdk/src/skills/registry.ts).
5
9
  // ---------------------------------------------------------------------------
6
10
 
7
- import { executeToolInternal } from './polyfill.js';
8
- import type { ModelContextTool, ToolExecuteCallback, ToolExecuteResult } from './types.js';
11
+ import type { ToolExecuteResult } from './types.js';
9
12
 
10
13
  // ---------------------------------------------------------------------------
11
14
  // Result builders
@@ -18,63 +21,3 @@ export function textResult(text: string): ToolExecuteResult {
18
21
  export function jsonResult(data: unknown): ToolExecuteResult {
19
22
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
20
23
  }
21
-
22
- // ---------------------------------------------------------------------------
23
- // Skill registry
24
- // ---------------------------------------------------------------------------
25
-
26
- export interface SkillDef {
27
- id: string;
28
- name: string;
29
- description: string;
30
- component: string;
31
- presentation?: string;
32
- version?: string;
33
- tags?: string[];
34
- }
35
-
36
- const _skills = new Map<string, SkillDef>();
37
-
38
- function mcNav(): {
39
- registerTool: (t: ModelContextTool & { execute: ToolExecuteCallback }) => void;
40
- unregisterTool: (name: string) => void;
41
- } | null {
42
- if (typeof navigator === 'undefined') return null;
43
- return (navigator as unknown as Record<string, unknown>).modelContext as ReturnType<typeof mcNav> ?? null;
44
- }
45
-
46
- export function registerSkill(skill: SkillDef): void {
47
- _skills.set(skill.id, skill);
48
- const mc = mcNav();
49
- if (!mc) return;
50
- const toolName = `skill__${skill.id}`;
51
- try { mc.unregisterTool(toolName); } catch { /* not yet registered */ }
52
- mc.registerTool({
53
- name: toolName,
54
- description: `[Skill] ${skill.name} — ${skill.description}. Component: ${skill.component}.${skill.presentation ? ` Hints: ${skill.presentation}` : ''}`,
55
- inputSchema: { type: 'object', properties: {} },
56
- execute: () => jsonResult(skill),
57
- annotations: { readOnlyHint: true },
58
- });
59
- }
60
-
61
- export function unregisterSkill(id: string): void {
62
- _skills.delete(id);
63
- const mc = mcNav();
64
- if (!mc) return;
65
- try { mc.unregisterTool(`skill__${id}`); } catch { /* already gone */ }
66
- }
67
-
68
- export function getSkill(id: string): SkillDef | undefined {
69
- return _skills.get(id);
70
- }
71
-
72
- export function listSkills(): SkillDef[] {
73
- return Array.from(_skills.values());
74
- }
75
-
76
- export function clearSkills(): void {
77
- for (const id of Array.from(_skills.keys())) unregisterSkill(id);
78
- }
79
-
80
- export { executeToolInternal };
@@ -0,0 +1,53 @@
1
+ export interface WebMcpServerOptions {
2
+ description: string;
3
+ }
4
+ export interface WebMcpToolDef {
5
+ name: string;
6
+ description: string;
7
+ inputSchema: Record<string, unknown>;
8
+ execute: (params: Record<string, unknown>) => Promise<unknown>;
9
+ }
10
+ /** A vanilla renderer: receives a container + data, optionally returns a cleanup function. */
11
+ export type WidgetRenderer = ((container: HTMLElement, data: Record<string, unknown>) => void | (() => void)) | unknown;
12
+ export interface WidgetEntry {
13
+ name: string;
14
+ description: string;
15
+ inputSchema: Record<string, unknown>;
16
+ recipe: string;
17
+ renderer: WidgetRenderer;
18
+ group?: string;
19
+ /** True when the renderer is a plain function (not a framework component). */
20
+ vanilla: boolean;
21
+ }
22
+ export interface WebMcpServer {
23
+ readonly name: string;
24
+ readonly description: string;
25
+ registerWidget(recipeMarkdown: string, renderer: WidgetRenderer): void;
26
+ addTool(tool: WebMcpToolDef): void;
27
+ layer(): {
28
+ protocol: 'webmcp';
29
+ serverName: string;
30
+ description: string;
31
+ tools: WebMcpToolDef[];
32
+ };
33
+ getWidget(name: string): WidgetEntry | undefined;
34
+ listWidgets(): WidgetEntry[];
35
+ }
36
+ export interface ParsedFrontmatter {
37
+ frontmatter: Record<string, unknown>;
38
+ body: string;
39
+ }
40
+ /**
41
+ * Parse a markdown file with YAML frontmatter (--- delimited).
42
+ * Supports: scalars, nested objects (indentation), arrays (- item), inline values.
43
+ * No external YAML dependency.
44
+ */
45
+ export declare function parseFrontmatter(markdown: string): ParsedFrontmatter;
46
+ /**
47
+ * Mount a widget into a DOM container by searching registered servers.
48
+ * If the renderer is a function (vanilla renderer), it is called directly.
49
+ * Returns an optional cleanup function.
50
+ * Falls back to a text placeholder if no server provides the widget.
51
+ */
52
+ export declare function mountWidget(container: HTMLElement, type: string, data: Record<string, unknown>, servers: WebMcpServer[]): (() => void) | void;
53
+ export declare function createWebMcpServer(name: string, options: WebMcpServerOptions): WebMcpServer;