@json-layout/core 2.4.0 → 2.5.0

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.
Files changed (41) hide show
  1. package/package.json +8 -1
  2. package/src/i18n/de.js +0 -19
  3. package/src/i18n/en.js +0 -19
  4. package/src/i18n/fr.js +0 -19
  5. package/src/i18n/nl.js +0 -19
  6. package/src/i18n/types.ts +0 -19
  7. package/src/state/state-tree.js +10 -1
  8. package/src/webmcp/index.js +283 -0
  9. package/src/webmcp/project.js +148 -0
  10. package/src/webmcp/resolve.js +25 -0
  11. package/src/webmcp/tools/describe-state.js +74 -0
  12. package/src/webmcp/tools/fill-form-skill.js +67 -0
  13. package/src/webmcp/tools/get-data.js +36 -0
  14. package/src/webmcp/tools/get-field-suggestions.js +84 -0
  15. package/src/webmcp/tools/set-data.js +56 -0
  16. package/src/webmcp/tools/set-field-value.js +71 -0
  17. package/types/i18n/de.d.ts +0 -19
  18. package/types/i18n/en.d.ts +0 -19
  19. package/types/i18n/fr.d.ts +0 -19
  20. package/types/i18n/nl.d.ts +0 -19
  21. package/types/i18n/types.d.ts +0 -19
  22. package/types/i18n/types.d.ts.map +1 -1
  23. package/types/state/state-tree.d.ts.map +1 -1
  24. package/types/webmcp/index.d.ts +78 -0
  25. package/types/webmcp/index.d.ts.map +1 -0
  26. package/types/webmcp/project.d.ts +59 -0
  27. package/types/webmcp/project.d.ts.map +1 -0
  28. package/types/webmcp/resolve.d.ts +11 -0
  29. package/types/webmcp/resolve.d.ts.map +1 -0
  30. package/types/webmcp/tools/describe-state.d.ts +70 -0
  31. package/types/webmcp/tools/describe-state.d.ts.map +1 -0
  32. package/types/webmcp/tools/fill-form-skill.d.ts +24 -0
  33. package/types/webmcp/tools/fill-form-skill.d.ts.map +1 -0
  34. package/types/webmcp/tools/get-data.d.ts +31 -0
  35. package/types/webmcp/tools/get-data.d.ts.map +1 -0
  36. package/types/webmcp/tools/get-field-suggestions.d.ts +66 -0
  37. package/types/webmcp/tools/get-field-suggestions.d.ts.map +1 -0
  38. package/types/webmcp/tools/set-data.d.ts +65 -0
  39. package/types/webmcp/tools/set-data.d.ts.map +1 -0
  40. package/types/webmcp/tools/set-field-value.d.ts +73 -0
  41. package/types/webmcp/tools/set-field-value.d.ts.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@json-layout/core",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Compilation and state management utilities for JSON Layout.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -45,6 +45,12 @@
45
45
  "types": "./types/utils/doc-options.d.ts",
46
46
  "default": "./src/utils/doc-options.js"
47
47
  }
48
+ },
49
+ "./webmcp": {
50
+ "import": {
51
+ "types": "./types/webmcp/index.d.ts",
52
+ "default": "./src/webmcp/index.js"
53
+ }
48
54
  }
49
55
  },
50
56
  "files": [
@@ -89,6 +95,7 @@
89
95
  "marked": "^15.0.7"
90
96
  },
91
97
  "devDependencies": {
98
+ "@mcp-b/webmcp-types": "^2.2.0",
92
99
  "@types/debug": "^4.1.7",
93
100
  "@types/node-fetch": "^2.6.5",
94
101
  "json-schema-to-typescript": "^14.0.0",
package/src/i18n/de.js CHANGED
@@ -14,25 +14,6 @@ export default {
14
14
  up: 'Nach oben verschieben',
15
15
  down: 'Nach unten verschieben',
16
16
  showHelp: 'Hilfemeldung anzeigen',
17
- mdeLink1: '[Link-Titel',
18
- mdeLink2: '](Link-URL)',
19
- mdeImg1: '![](',
20
- mdeImg2: 'Bild-URL)',
21
- mdeTable1: '',
22
- mdeTable2: '\n\n| Spalte 1 | Spalte 2 | Spalte 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n',
23
- bold: 'Fett',
24
- italic: 'Kursiv',
25
- heading: 'Titel',
26
- quote: 'Zitat',
27
- unorderedList: 'Ungeordnete Liste',
28
- orderedList: 'Geordnete Liste',
29
- createLink: 'Link erstellen',
30
- insertImage: 'Bild einfügen',
31
- createTable: 'Tabelle erstellen',
32
- preview: 'Vorschau',
33
- mdeGuide: 'Syntax-Dokumentation',
34
- undo: 'Rückgängig',
35
- redo: 'Wiederholen',
36
17
  default: 'standard: ',
37
18
  name: 'name: ',
38
19
  examples: 'Beispiele: ',
package/src/i18n/en.js CHANGED
@@ -14,25 +14,6 @@ export default {
14
14
  up: 'Move up',
15
15
  down: 'Move down',
16
16
  showHelp: 'Show a help message',
17
- mdeLink1: '[Link title',
18
- mdeLink2: '](link url)',
19
- mdeImg1: '![](',
20
- mdeImg2: 'image url)',
21
- mdeTable1: '',
22
- mdeTable2: '\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n',
23
- bold: 'Bold',
24
- italic: 'Italic',
25
- heading: 'Title',
26
- quote: 'Quote',
27
- unorderedList: 'Unordered list',
28
- orderedList: 'Ordered list',
29
- createLink: 'Create a link',
30
- insertImage: 'Insert an image',
31
- createTable: 'Create a table',
32
- preview: 'Aperçu du rendu',
33
- mdeGuide: 'Documentation de la syntaxe',
34
- undo: 'Undo',
35
- redo: 'Redo',
36
17
  default: 'default: ',
37
18
  name: 'name: ',
38
19
  examples: 'Examples: ',
package/src/i18n/fr.js CHANGED
@@ -14,25 +14,6 @@ export default {
14
14
  up: 'Décaler vers le haut',
15
15
  down: 'Décaler vers le bas',
16
16
  showHelp: 'Afficher un message d\'aide',
17
- mdeLink1: '[titre du lien',
18
- mdeLink2: '](adresse du lien)',
19
- mdeImg1: '![](',
20
- mdeImg2: 'adresse de l\'image)',
21
- mdeTable1: '',
22
- mdeTable2: '\n\n| Colonne 1 | Colonne 2 | Colonne 3 |\n| -------- | -------- | -------- |\n| Texte | Texte | Texte |\n\n',
23
- bold: 'Gras',
24
- italic: 'Italique',
25
- heading: 'Titre',
26
- quote: 'Citation',
27
- unorderedList: 'Liste à puce',
28
- orderedList: 'Liste numérotée',
29
- createLink: 'Créer un lien',
30
- insertImage: 'Insérer une image',
31
- createTable: 'Créer un tableau',
32
- preview: 'Preview',
33
- mdeGuide: 'Syntax documentation',
34
- undo: 'Défaire',
35
- redo: 'Refaire',
36
17
  default: 'défaut : ',
37
18
  name: 'nom : ',
38
19
  examples: 'Exemples : ',
package/src/i18n/nl.js CHANGED
@@ -14,25 +14,6 @@ export default {
14
14
  up: 'Omhoog verplaatsen',
15
15
  down: 'Omlaag verplaatsen',
16
16
  showHelp: 'Toon helpbericht',
17
- mdeLink1: '[Link titel',
18
- mdeLink2: '](link url)',
19
- mdeImg1: '![](',
20
- mdeImg2: 'afbeelding url)',
21
- mdeTable1: '',
22
- mdeTable2: '\n\n| Kolom 1 | Kolom 2 | Kolom 3 |\n| -------- | -------- | -------- |\n| Tekst | Tekst | Tekst |\n\n',
23
- bold: 'Vet',
24
- italic: 'Cursief',
25
- heading: 'Titel',
26
- quote: 'Citaat',
27
- unorderedList: 'Ongeordende lijst',
28
- orderedList: 'Geordende lijst',
29
- createLink: 'Maak een koppeling',
30
- insertImage: 'Afbeelding invoegen',
31
- createTable: 'Tabel aanmaken',
32
- preview: 'Voorbeeld',
33
- mdeGuide: 'Documentatie over syntaxis',
34
- undo: 'Ongedaan maken',
35
- redo: 'Opnieuw',
36
17
  default: 'standaard: ',
37
18
  name: 'naam: ',
38
19
  examples: 'Voorbeelden: ',
package/src/i18n/types.ts CHANGED
@@ -18,25 +18,6 @@ export interface StateOptionsMessages {
18
18
  up: string
19
19
  down: string
20
20
  showHelp: string
21
- mdeLink1: string
22
- mdeLink2: string
23
- mdeImg1: string
24
- mdeImg2: string
25
- mdeTable1: string
26
- mdeTable2: string
27
- bold: string
28
- italic: string
29
- heading: string
30
- quote: string
31
- unorderedList: string
32
- orderedList: string
33
- createLink: string
34
- insertImage: string
35
- createTable: string
36
- preview: string
37
- mdeGuide: string
38
- undo: string
39
- redo: string
40
21
  keyboardDate: string
41
22
  keyboardDateTime: string
42
23
  }
@@ -51,7 +51,16 @@ export function createStateTree (
51
51
  ) {
52
52
  logStateTree('createStateTree', skeleton.root)
53
53
  const validate = compiledLayout.validates[skeleton.refPointer]
54
- const valid = validate(data)
54
+ let valid
55
+ if (compiledLayout.options?.ajvOptions?.coerceTypes) {
56
+ const serializedBefore = JSON.stringify(data)
57
+ valid = validate(data)
58
+ if (JSON.stringify(data) !== serializedBefore) {
59
+ data = { .../** @type {Record<string, unknown>} */(data) }
60
+ }
61
+ } else {
62
+ valid = validate(data)
63
+ }
55
64
  if (validate.errors) {
56
65
  logValidation(`${skeleton.root} new state tree initial validation errors`, validate.errors, validationState)
57
66
  for (const error of validate.errors) {
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @file WebMCP integration for JSON Layout StatefulLayout
3
+ * @description Provides MCP tool descriptors that work with browser's navigator.modelContext
4
+ */
5
+
6
+ import debug from 'debug'
7
+
8
+ import * as describeState from './tools/describe-state.js'
9
+ import * as setFieldValue from './tools/set-field-value.js'
10
+ import * as setData from './tools/set-data.js'
11
+ import * as getData from './tools/get-data.js'
12
+ import * as getFieldSuggestions from './tools/get-field-suggestions.js'
13
+ import * as fillFormSkill from './tools/fill-form-skill.js'
14
+
15
+ /** @typedef {import('@mcp-b/webmcp-types').ToolDescriptor} ToolDescriptor */
16
+
17
+ const log = debug('jl:webmcp')
18
+
19
+ /**
20
+ * @typedef {object} WebMCPOptions
21
+ * @property {string} [prefixName] - Prefix for all tool names
22
+ * @property {string} [dataTitle] - Title used in descriptions (default: 'form')
23
+ * @property {object} [schema] - The original JSON schema
24
+ */
25
+
26
+ /**
27
+ * WebMCP class that provides MCP tool descriptors for a StatefulLayout instance
28
+ */
29
+ export class WebMCP {
30
+ /**
31
+ * @readonly
32
+ * @type {import('../state/index.js').StatefulLayout}
33
+ */
34
+ _statefulLayout
35
+
36
+ /**
37
+ * @readonly
38
+ * @type {string}
39
+ */
40
+ _prefixName
41
+
42
+ /**
43
+ * @readonly
44
+ * @type {string}
45
+ */
46
+ _dataTitle
47
+
48
+ /**
49
+ * @readonly
50
+ * @type {string}
51
+ */
52
+ _skill
53
+
54
+ /**
55
+ * @readonly
56
+ * @type {object | null}
57
+ */
58
+ _schema = null
59
+
60
+ /**
61
+ * @type {string[]}
62
+ */
63
+ _registeredTools = []
64
+
65
+ /**
66
+ * @param {import('../state/index.js').StatefulLayout} statefulLayout
67
+ * @param {WebMCPOptions} [options]
68
+ */
69
+ constructor (statefulLayout, options = {}) {
70
+ this._statefulLayout = statefulLayout
71
+ this._prefixName = options.prefixName || ''
72
+ this._dataTitle = options.dataTitle || 'form'
73
+ this._schema = options.schema || null
74
+ this._skill = fillFormSkill.generateSkill(this._dataTitle, this._prefixName, !!this._schema, this._statefulLayout)
75
+ }
76
+
77
+ /**
78
+ * @param {string} name
79
+ * @returns {string}
80
+ */
81
+ _toolName (name) {
82
+ return this._prefixName + name
83
+ }
84
+
85
+ /**
86
+ * @returns {ToolDescriptor[]}
87
+ */
88
+ getTools () {
89
+ const dataTitle = this._dataTitle
90
+
91
+ /** @type {ToolDescriptor[]} */
92
+ const tools = [
93
+ {
94
+ name: this._toolName('fillFormSkill'),
95
+ description: fillFormSkill.getDescription(dataTitle),
96
+ outputSchema: { type: 'string' },
97
+ execute: async (args) => {
98
+ try {
99
+ return {
100
+ content: [{ type: 'text', text: this._skill }]
101
+ }
102
+ } catch (err) {
103
+ const message = err instanceof Error ? err.message : String(err)
104
+ return {
105
+ content: [{ type: 'text', text: `Error: ${message}` }],
106
+ isError: true
107
+ }
108
+ }
109
+ }
110
+ },
111
+ {
112
+ name: this._toolName('getData'),
113
+ description: getData.getDescription(dataTitle),
114
+ inputSchema: getData.inputSchema,
115
+ outputSchema: getData.outputSchema,
116
+ execute: async (args) => {
117
+ try {
118
+ const result = getData.execute(this._statefulLayout, args || {})
119
+ return {
120
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
121
+ }
122
+ } catch (err) {
123
+ const message = err instanceof Error ? err.message : String(err)
124
+ return {
125
+ content: [{ type: 'text', text: `Error: ${message}` }],
126
+ isError: true
127
+ }
128
+ }
129
+ }
130
+ },
131
+ {
132
+ name: this._toolName('setData'),
133
+ description: setData.getDescription(dataTitle),
134
+ inputSchema: setData.inputSchema,
135
+ outputSchema: setData.outputSchema,
136
+ execute: async (args) => {
137
+ try {
138
+ if (!args?.data) {
139
+ throw new Error('data is required')
140
+ }
141
+ const result = setData.execute(
142
+ this._statefulLayout,
143
+ /** @type {{ data: unknown }} */(args)
144
+ )
145
+ return {
146
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
147
+ }
148
+ } catch (err) {
149
+ const message = err instanceof Error ? err.message : String(err)
150
+ return {
151
+ content: [{ type: 'text', text: `Error: ${message}` }],
152
+ isError: true
153
+ }
154
+ }
155
+ }
156
+ },
157
+ {
158
+ name: this._toolName('describeState'),
159
+ description: describeState.getDescription(dataTitle),
160
+ inputSchema: describeState.inputSchema,
161
+ outputSchema: describeState.outputSchema,
162
+ execute: async (args) => {
163
+ try {
164
+ const result = describeState.execute(this._statefulLayout, args || {})
165
+ return {
166
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
167
+ }
168
+ } catch (err) {
169
+ const message = err instanceof Error ? err.message : String(err)
170
+ return {
171
+ content: [{ type: 'text', text: `Error: ${message}` }],
172
+ isError: true
173
+ }
174
+ }
175
+ }
176
+ },
177
+ {
178
+ name: this._toolName('setFieldValue'),
179
+ description: setFieldValue.getDescription(dataTitle),
180
+ inputSchema: setFieldValue.inputSchema,
181
+ outputSchema: setFieldValue.outputSchema,
182
+ execute: async (args) => {
183
+ try {
184
+ if (!args?.path) {
185
+ throw new Error('path is required')
186
+ }
187
+ const result = setFieldValue.execute(
188
+ this._statefulLayout,
189
+ /** @type {{ path: string, value: unknown }} */(args)
190
+ )
191
+ return {
192
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
193
+ }
194
+ } catch (err) {
195
+ const message = err instanceof Error ? err.message : String(err)
196
+ return {
197
+ content: [{ type: 'text', text: `Error: ${message}` }],
198
+ isError: true
199
+ }
200
+ }
201
+ }
202
+ },
203
+ {
204
+ name: this._toolName('getFieldSuggestions'),
205
+ description: getFieldSuggestions.getDescription(dataTitle),
206
+ inputSchema: getFieldSuggestions.inputSchema,
207
+ outputSchema: getFieldSuggestions.outputSchema,
208
+ execute: async (args) => {
209
+ try {
210
+ if (!args?.path) {
211
+ throw new Error('path is required')
212
+ }
213
+ const result = await getFieldSuggestions.execute(
214
+ this._statefulLayout,
215
+ /** @type {{ path: string, query?: string }} */(args)
216
+ )
217
+ return {
218
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
219
+ }
220
+ } catch (err) {
221
+ const message = err instanceof Error ? err.message : String(err)
222
+ return {
223
+ content: [{ type: 'text', text: `Error: ${message}` }],
224
+ isError: true
225
+ }
226
+ }
227
+ }
228
+ }
229
+ ]
230
+
231
+ if (this._schema) {
232
+ tools.push({
233
+ name: this._toolName('getSchema'),
234
+ description: `Get the the JSON schema that governs "${dataTitle}" form.`,
235
+ outputSchema: {
236
+ type: 'object',
237
+ properties: {
238
+ jsonSchema: {},
239
+ }
240
+ },
241
+ execute: async (args) => {
242
+ return {
243
+ jsonSchema: this._schema
244
+ }
245
+ }
246
+ })
247
+ }
248
+
249
+ return tools
250
+ }
251
+
252
+ /**
253
+ * @returns {Promise<void>}
254
+ */
255
+ async registerTools () {
256
+ if (typeof navigator === 'undefined' || !navigator.modelContext) {
257
+ throw new Error('navigator.modelContext is not available')
258
+ }
259
+
260
+ const tools = this.getTools()
261
+ for (const tool of tools) {
262
+ log('registering tool:', tool.name)
263
+ // @ts-ignore - complex generic types from webmcp-types
264
+ await navigator.modelContext.registerTool(tool)
265
+ this._registeredTools.push(tool.name)
266
+ }
267
+ }
268
+
269
+ /**
270
+ * @returns {Promise<void>}
271
+ */
272
+ async unregisterTools () {
273
+ if (typeof navigator === 'undefined' || !navigator.modelContext) {
274
+ throw new Error('navigator.modelContext is not available')
275
+ }
276
+
277
+ for (const name of this._registeredTools) {
278
+ log('unregistering tool:', name)
279
+ await navigator.modelContext.unregisterTool(name)
280
+ }
281
+ this._registeredTools = []
282
+ }
283
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @file Projection functions for webmcp tools
3
+ */
4
+
5
+ import { isItemsLayout } from '@json-layout/vocabulary'
6
+
7
+ const constraintKeys = {
8
+ 'number-field': ['min', 'max', 'step', 'precision'],
9
+ slider: ['min', 'max', 'step'],
10
+ 'date-picker': ['min', 'max', 'format'],
11
+ 'date-time-picker': ['min', 'max'],
12
+ 'time-picker': ['min', 'max'],
13
+ combobox: ['separator'],
14
+ 'number-combobox': ['separator']
15
+ }
16
+
17
+ /**
18
+ * @param {string} comp
19
+ * @returns {string[]|undefined}
20
+ */
21
+ function getConstraintKeys (comp) {
22
+ // @ts-ignore - complex union type not fully represented
23
+ return constraintKeys[comp]
24
+ }
25
+
26
+ /**
27
+ * @param {import('../state/types.js').StateNode} node
28
+ * @param {import('../state/index.js').StatefulLayout} statefulLayout
29
+ * @returns {{
30
+ * key: string|number,
31
+ * path: string,
32
+ * comp: string,
33
+ * data: unknown,
34
+ * title?: string,
35
+ * label?: string,
36
+ * help?: string,
37
+ * error?: string,
38
+ * childError?: boolean,
39
+ * required?: boolean,
40
+ * readOnly?: boolean,
41
+ * constraints?: Record<string, unknown>,
42
+ * oneOfItems?: Array<{key: number, title: string}>,
43
+ * children?: Array<any>
44
+ * getSuffections?: boolean
45
+ * }}
46
+ */
47
+ export function projectNode (node, statefulLayout) {
48
+ /**
49
+ * @type {{
50
+ * key: string|number,
51
+ * path: string,
52
+ * comp: string,
53
+ * data: unknown,
54
+ * title?: string,
55
+ * label?: string,
56
+ * help?: string,
57
+ * error?: string,
58
+ * childError?: boolean,
59
+ * required?: boolean,
60
+ * readOnly?: boolean,
61
+ * constraints?: Record<string, unknown>,
62
+ * oneOfItems?: Array<{key: number, title: string}>,
63
+ * children?: Array<any>
64
+ * getSuggestions?: boolean
65
+ * }}
66
+ */
67
+ const out = {
68
+ key: node.key,
69
+ path: node.fullKey,
70
+ comp: node.layout.comp,
71
+ data: node.data
72
+ }
73
+
74
+ const layout = /** @type {Record<string, unknown>} */(node.layout)
75
+ if (typeof layout.title === 'string') out.title = layout.title
76
+ if (typeof layout.label === 'string') out.label = layout.label
77
+ if (node.layout.help) out.help = node.layout.help
78
+
79
+ if (node.error) out.error = node.error
80
+ if (node.childError) out.childError = true
81
+
82
+ if (node.skeleton.required) out.required = true
83
+ if (node.options.readOnly) out.readOnly = true
84
+ if (isItemsLayout(node.layout, statefulLayout.compiledLayout.components)) out.getSuggestions = true
85
+
86
+ const keys = getConstraintKeys(node.layout.comp)
87
+ if (keys) {
88
+ /** @type {Record<string, unknown>} */
89
+ const constraints = {}
90
+ for (const k of keys) {
91
+ const v = layout[k]
92
+ if (v !== undefined && v !== null) constraints[k] = v
93
+ }
94
+ if (Object.keys(constraints).length > 0) out.constraints = constraints
95
+ }
96
+
97
+ if (node.layout.comp === 'one-of-select' && Array.isArray(layout.oneOfItems)) {
98
+ out.oneOfItems = layout.oneOfItems
99
+ .filter((item) => !item.header)
100
+ .map((item) => ({ key: item.key, title: item.title }))
101
+ }
102
+
103
+ if (node.children) {
104
+ out.children = node.children
105
+ .filter((c) => c.layout.comp !== 'none')
106
+ .map(node => projectNode(node, statefulLayout))
107
+ }
108
+
109
+ return out
110
+ }
111
+
112
+ /**
113
+ * @param {import('../state/types.js').StateTree} stateTree
114
+ * @param {import('../state/index.js').StatefulLayout} statefulLayout
115
+ * @returns {{ root: ReturnType<typeof projectNode>, valid: boolean }}
116
+ */
117
+ export function projectStateTree (stateTree, statefulLayout) {
118
+ return {
119
+ root: projectNode(stateTree.root, statefulLayout),
120
+ valid: stateTree.valid
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @param {import('../state/types.js').StateNode} node
126
+ * @returns {Array<{path: string, message: string}>}
127
+ */
128
+ export function collectErrors (node) {
129
+ /** @type {Array<{path: string, message: string}>} */
130
+ const errors = []
131
+ collectErrorsRecurse(node, errors)
132
+ return errors
133
+ }
134
+
135
+ /**
136
+ * @param {import('../state/types.js').StateNode} node
137
+ * @param {Array<{path: string, message: string}>} errors
138
+ */
139
+ function collectErrorsRecurse (node, errors) {
140
+ if (node.error) {
141
+ errors.push({ path: node.fullKey, message: node.error })
142
+ }
143
+ if (node.children) {
144
+ for (const child of node.children) {
145
+ collectErrorsRecurse(child, errors)
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @file Node resolution for webmcp tools
3
+ */
4
+
5
+ /**
6
+ * Navigate from a root StateNode to a descendant node by path.
7
+ * @param {import('../state/types.js').StateNode} root
8
+ * @param {string} path
9
+ * @returns {import('../state/types.js').StateNode|undefined}
10
+ */
11
+ export function resolveNode (root, path) {
12
+ if (!path || path === '/') return root
13
+
14
+ const segments = path.replace(/^\//, '').split('/')
15
+ /** @type {import('../state/types.js').StateNode|undefined} */
16
+ let current = root
17
+
18
+ for (const segment of segments) {
19
+ if (!current?.children) return undefined
20
+ const key = /^\d+$/.test(segment) ? parseInt(segment, 10) : segment
21
+ current = current.children.find((c) => c.key === key)
22
+ }
23
+
24
+ return current
25
+ }