@json-layout/core 2.5.1 → 2.6.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.
- package/package.json +2 -1
- package/src/compile/serialize.js +4 -0
- package/src/state/index.js +25 -1
- package/src/state/state-node.js +122 -73
- package/src/state/types.ts +5 -1
- package/src/state/utils/modified.js +32 -0
- package/src/webmcp/index.js +141 -22
- package/src/webmcp/project.js +210 -35
- package/src/webmcp/tools/describe-state.js +28 -3
- package/src/webmcp/tools/edit-array.js +97 -0
- package/src/webmcp/tools/fill-form-skill.js +2 -2
- package/src/webmcp/tools/set-data.js +8 -6
- package/src/webmcp/tools/set-field-value.js +19 -7
- package/types/compile/serialize.d.ts.map +1 -1
- package/types/state/index.d.ts +14 -1
- package/types/state/index.d.ts.map +1 -1
- package/types/state/state-node.d.ts.map +1 -1
- package/types/state/types.d.ts +5 -1
- package/types/state/types.d.ts.map +1 -1
- package/types/state/utils/modified.d.ts +17 -0
- package/types/state/utils/modified.d.ts.map +1 -0
- package/types/webmcp/index.d.ts +21 -9
- package/types/webmcp/index.d.ts.map +1 -1
- package/types/webmcp/project.d.ts +80 -28
- package/types/webmcp/project.d.ts.map +1 -1
- package/types/webmcp/tools/describe-state.d.ts +10 -1
- package/types/webmcp/tools/describe-state.d.ts.map +1 -1
- package/types/webmcp/tools/edit-array.d.ts +88 -0
- package/types/webmcp/tools/edit-array.d.ts.map +1 -0
- package/types/webmcp/tools/set-data.d.ts +11 -16
- package/types/webmcp/tools/set-data.d.ts.map +1 -1
- package/types/webmcp/tools/set-field-value.d.ts +35 -17
- package/types/webmcp/tools/set-field-value.d.ts.map +1 -1
package/src/webmcp/index.js
CHANGED
|
@@ -10,18 +10,53 @@ import * as setFieldValue from './tools/set-field-value.js'
|
|
|
10
10
|
import * as setData from './tools/set-data.js'
|
|
11
11
|
import * as getData from './tools/get-data.js'
|
|
12
12
|
import * as getFieldSuggestions from './tools/get-field-suggestions.js'
|
|
13
|
+
import * as editArray from './tools/edit-array.js'
|
|
13
14
|
import * as fillFormSkill from './tools/fill-form-skill.js'
|
|
15
|
+
import { formatMutationResult, formatSuggestions } from './project.js'
|
|
14
16
|
|
|
15
17
|
/** @typedef {import('@mcp-b/webmcp-types').ToolDescriptor} ToolDescriptor */
|
|
16
18
|
|
|
17
19
|
const log = debug('jl:webmcp')
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* If value is a JSON string representing an object or array, parse it.
|
|
23
|
+
* Otherwise return value unchanged.
|
|
24
|
+
* @param {unknown} value
|
|
25
|
+
* @returns {unknown}
|
|
26
|
+
*/
|
|
27
|
+
function parseIfJsonString (value) {
|
|
28
|
+
if (typeof value !== 'string') return value
|
|
29
|
+
const trimmed = value.trim()
|
|
30
|
+
if ((trimmed[0] === '{' && trimmed[trimmed.length - 1] === '}') ||
|
|
31
|
+
(trimmed[0] === '[' && trimmed[trimmed.length - 1] === ']')) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(trimmed)
|
|
34
|
+
} catch {
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
/**
|
|
20
42
|
* @typedef {object} WebMCPOptions
|
|
21
43
|
* @property {string} [prefixName] - Prefix for all tool names
|
|
22
44
|
* @property {string} [dataTitle] - Title used in descriptions (default: 'form')
|
|
23
45
|
* @property {object} [schema] - The original JSON schema
|
|
46
|
+
* @property {boolean} [includeFillFormSkill] - Include the fillFormSkill tool (default: false)
|
|
47
|
+
* @property {boolean} [includeSubAgent] - Include a subagent_ tool wrapping all form tools (default: false)
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
52
|
+
* @returns {"small"|"medium"|"large"}
|
|
24
53
|
*/
|
|
54
|
+
function getComplexity (statefulLayout) {
|
|
55
|
+
const nbNormalizedLayouts = Object.keys(statefulLayout.compiledLayout.normalizedLayouts).length
|
|
56
|
+
if (nbNormalizedLayouts > 50) return 'large'
|
|
57
|
+
if (nbNormalizedLayouts > 15) return 'medium'
|
|
58
|
+
return 'small'
|
|
59
|
+
}
|
|
25
60
|
|
|
26
61
|
/**
|
|
27
62
|
* WebMCP class that provides MCP tool descriptors for a StatefulLayout instance
|
|
@@ -47,9 +82,9 @@ export class WebMCP {
|
|
|
47
82
|
|
|
48
83
|
/**
|
|
49
84
|
* @readonly
|
|
50
|
-
* @type {
|
|
85
|
+
* @type {"small"|"medium"|"large"}
|
|
51
86
|
*/
|
|
52
|
-
|
|
87
|
+
_complexity
|
|
53
88
|
|
|
54
89
|
/**
|
|
55
90
|
* @readonly
|
|
@@ -57,6 +92,18 @@ export class WebMCP {
|
|
|
57
92
|
*/
|
|
58
93
|
_schema = null
|
|
59
94
|
|
|
95
|
+
/**
|
|
96
|
+
* @readonly
|
|
97
|
+
* @type {boolean}
|
|
98
|
+
*/
|
|
99
|
+
_includeFillFormSkill = false
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @readonly
|
|
103
|
+
* @type {boolean}
|
|
104
|
+
*/
|
|
105
|
+
_includeSubAgent = false
|
|
106
|
+
|
|
60
107
|
/**
|
|
61
108
|
* @type {string[]}
|
|
62
109
|
*/
|
|
@@ -71,7 +118,9 @@ export class WebMCP {
|
|
|
71
118
|
this._prefixName = options.prefixName || ''
|
|
72
119
|
this._dataTitle = options.dataTitle || 'form'
|
|
73
120
|
this._schema = options.schema || null
|
|
74
|
-
this.
|
|
121
|
+
this._includeFillFormSkill = options.includeFillFormSkill || false
|
|
122
|
+
this._includeSubAgent = options.includeSubAgent || false
|
|
123
|
+
this._complexity = getComplexity(statefulLayout)
|
|
75
124
|
}
|
|
76
125
|
|
|
77
126
|
/**
|
|
@@ -87,17 +136,21 @@ export class WebMCP {
|
|
|
87
136
|
*/
|
|
88
137
|
getTools () {
|
|
89
138
|
const dataTitle = this._dataTitle
|
|
139
|
+
const complexity = this._complexity
|
|
90
140
|
|
|
91
141
|
/** @type {ToolDescriptor[]} */
|
|
92
|
-
const tools = [
|
|
93
|
-
|
|
142
|
+
const tools = []
|
|
143
|
+
|
|
144
|
+
if (this._includeFillFormSkill) {
|
|
145
|
+
const skill = fillFormSkill.generateSkill(dataTitle, this._prefixName, !!this._schema, this._statefulLayout)
|
|
146
|
+
tools.push({
|
|
94
147
|
name: this._toolName('fillFormSkill'),
|
|
95
148
|
description: fillFormSkill.getDescription(dataTitle),
|
|
96
149
|
outputSchema: { type: 'string' },
|
|
97
150
|
execute: async (args) => {
|
|
98
151
|
try {
|
|
99
152
|
return {
|
|
100
|
-
content: [{ type: 'text', text:
|
|
153
|
+
content: [{ type: 'text', text: skill }]
|
|
101
154
|
}
|
|
102
155
|
} catch (err) {
|
|
103
156
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -107,17 +160,21 @@ export class WebMCP {
|
|
|
107
160
|
}
|
|
108
161
|
}
|
|
109
162
|
}
|
|
110
|
-
}
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
tools.push(
|
|
111
167
|
{
|
|
112
168
|
name: this._toolName('getData'),
|
|
113
|
-
description:
|
|
169
|
+
description: `Get current "${dataTitle}" data and validity status. Call this first to see what data already exists.`,
|
|
114
170
|
inputSchema: getData.inputSchema,
|
|
115
171
|
outputSchema: getData.outputSchema,
|
|
116
172
|
execute: async (args) => {
|
|
117
173
|
try {
|
|
118
174
|
const result = getData.execute(this._statefulLayout, args || {})
|
|
119
175
|
return {
|
|
120
|
-
content: [{ type: 'text', text: JSON.stringify(result
|
|
176
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
177
|
+
structuredContent: result
|
|
121
178
|
}
|
|
122
179
|
} catch (err) {
|
|
123
180
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -130,7 +187,7 @@ export class WebMCP {
|
|
|
130
187
|
},
|
|
131
188
|
{
|
|
132
189
|
name: this._toolName('setData'),
|
|
133
|
-
description: setData.getDescription(dataTitle),
|
|
190
|
+
description: setData.getDescription(dataTitle, complexity),
|
|
134
191
|
inputSchema: setData.inputSchema,
|
|
135
192
|
outputSchema: setData.outputSchema,
|
|
136
193
|
execute: async (args) => {
|
|
@@ -138,12 +195,14 @@ export class WebMCP {
|
|
|
138
195
|
if (!args?.data) {
|
|
139
196
|
throw new Error('data is required')
|
|
140
197
|
}
|
|
198
|
+
args.data = parseIfJsonString(args.data)
|
|
141
199
|
const result = setData.execute(
|
|
142
200
|
this._statefulLayout,
|
|
143
201
|
/** @type {{ data: unknown }} */(args)
|
|
144
202
|
)
|
|
145
203
|
return {
|
|
146
|
-
content: [{ type: 'text', text:
|
|
204
|
+
content: [{ type: 'text', text: formatMutationResult(result.valid, result.errors) }],
|
|
205
|
+
structuredContent: result
|
|
147
206
|
}
|
|
148
207
|
} catch (err) {
|
|
149
208
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -156,14 +215,16 @@ export class WebMCP {
|
|
|
156
215
|
},
|
|
157
216
|
{
|
|
158
217
|
name: this._toolName('describeState'),
|
|
159
|
-
description: describeState.getDescription(dataTitle),
|
|
218
|
+
description: describeState.getDescription(dataTitle, complexity),
|
|
160
219
|
inputSchema: describeState.inputSchema,
|
|
161
220
|
outputSchema: describeState.outputSchema,
|
|
162
221
|
execute: async (args) => {
|
|
163
222
|
try {
|
|
164
223
|
const result = describeState.execute(this._statefulLayout, args || {})
|
|
224
|
+
const text = describeState.toMarkdown(this._statefulLayout, args || {})
|
|
165
225
|
return {
|
|
166
|
-
content: [{ type: 'text', text
|
|
226
|
+
content: [{ type: 'text', text }],
|
|
227
|
+
structuredContent: result
|
|
167
228
|
}
|
|
168
229
|
} catch (err) {
|
|
169
230
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -184,12 +245,15 @@ export class WebMCP {
|
|
|
184
245
|
if (!args?.path) {
|
|
185
246
|
throw new Error('path is required')
|
|
186
247
|
}
|
|
248
|
+
args.value = parseIfJsonString(args.value)
|
|
187
249
|
const result = setFieldValue.execute(
|
|
188
250
|
this._statefulLayout,
|
|
189
251
|
/** @type {{ path: string, value: unknown }} */(args)
|
|
190
252
|
)
|
|
253
|
+
const fieldInfo = `${result.field.path} (${result.field.type}) = ${JSON.stringify(result.field.data)}`
|
|
191
254
|
return {
|
|
192
|
-
content: [{ type: 'text', text:
|
|
255
|
+
content: [{ type: 'text', text: formatMutationResult(result.valid, result.errors, fieldInfo) }],
|
|
256
|
+
structuredContent: result
|
|
193
257
|
}
|
|
194
258
|
} catch (err) {
|
|
195
259
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -202,7 +266,7 @@ export class WebMCP {
|
|
|
202
266
|
},
|
|
203
267
|
{
|
|
204
268
|
name: this._toolName('getFieldSuggestions'),
|
|
205
|
-
description:
|
|
269
|
+
description: `Get allowed values for a dropdown or autocomplete field in "${dataTitle}". Required when describeState shows "suggestions" for a field. Pass the returned value directly to setFieldValue or include it in setData.`,
|
|
206
270
|
inputSchema: getFieldSuggestions.inputSchema,
|
|
207
271
|
outputSchema: getFieldSuggestions.outputSchema,
|
|
208
272
|
execute: async (args) => {
|
|
@@ -215,7 +279,41 @@ export class WebMCP {
|
|
|
215
279
|
/** @type {{ path: string, query?: string }} */(args)
|
|
216
280
|
)
|
|
217
281
|
return {
|
|
218
|
-
content: [{ type: 'text', text:
|
|
282
|
+
content: [{ type: 'text', text: formatSuggestions(result.items) }],
|
|
283
|
+
structuredContent: result
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
289
|
+
isError: true
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: this._toolName('editArray'),
|
|
296
|
+
description: editArray.getDescription(dataTitle),
|
|
297
|
+
inputSchema: editArray.inputSchema,
|
|
298
|
+
outputSchema: editArray.outputSchema,
|
|
299
|
+
execute: async (args) => {
|
|
300
|
+
try {
|
|
301
|
+
if (!args?.path || !args?.action) {
|
|
302
|
+
throw new Error('path and action are required')
|
|
303
|
+
}
|
|
304
|
+
if (args.value !== undefined) {
|
|
305
|
+
args.value = parseIfJsonString(args.value)
|
|
306
|
+
}
|
|
307
|
+
const result = editArray.execute(
|
|
308
|
+
this._statefulLayout,
|
|
309
|
+
/** @type {{ path: string, action: 'add'|'remove', index?: number, value?: unknown }} */(args)
|
|
310
|
+
)
|
|
311
|
+
const actionInfo = args.action === 'add'
|
|
312
|
+
? `added item, ${result.itemCount} total`
|
|
313
|
+
: `removed item, ${result.itemCount} remaining`
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: 'text', text: formatMutationResult(result.valid, result.errors, actionInfo) }],
|
|
316
|
+
structuredContent: result
|
|
219
317
|
}
|
|
220
318
|
} catch (err) {
|
|
221
319
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -226,21 +324,42 @@ export class WebMCP {
|
|
|
226
324
|
}
|
|
227
325
|
}
|
|
228
326
|
}
|
|
229
|
-
|
|
327
|
+
)
|
|
230
328
|
|
|
231
329
|
if (this._schema) {
|
|
232
330
|
tools.push({
|
|
233
331
|
name: this._toolName('getSchema'),
|
|
234
|
-
description: `Get the
|
|
332
|
+
description: `Get the JSON schema that governs the "${dataTitle}" form.`,
|
|
235
333
|
outputSchema: {
|
|
236
334
|
type: 'object',
|
|
237
|
-
|
|
238
|
-
jsonSchema: {},
|
|
239
|
-
}
|
|
335
|
+
description: 'The JSON schema definition'
|
|
240
336
|
},
|
|
241
337
|
execute: async (args) => {
|
|
242
338
|
return {
|
|
243
|
-
|
|
339
|
+
content: [{ type: 'text', text: JSON.stringify(this._schema) }],
|
|
340
|
+
structuredContent: this._schema
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (this._includeSubAgent) {
|
|
347
|
+
const toolNames = tools.map(t => t.name)
|
|
348
|
+
const prompt = fillFormSkill.generateSkill(dataTitle, this._prefixName, !!this._schema, this._statefulLayout)
|
|
349
|
+
tools.push({
|
|
350
|
+
name: `subagent_${this._toolName('form')}`,
|
|
351
|
+
description: `Delegate a form-filling task for "${dataTitle}" to a specialized sub-agent`,
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: 'object',
|
|
354
|
+
properties: {
|
|
355
|
+
task: { type: 'string', description: 'The task to delegate to this sub-agent' }
|
|
356
|
+
},
|
|
357
|
+
required: ['task']
|
|
358
|
+
},
|
|
359
|
+
execute: async () => {
|
|
360
|
+
return {
|
|
361
|
+
content: [{ type: 'text', text: JSON.stringify({ prompt, tools: toolNames }) }],
|
|
362
|
+
structuredContent: { prompt, tools: toolNames }
|
|
244
363
|
}
|
|
245
364
|
}
|
|
246
365
|
})
|
package/src/webmcp/project.js
CHANGED
|
@@ -14,6 +14,28 @@ const constraintKeys = {
|
|
|
14
14
|
'number-combobox': ['separator']
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** @type {Record<string, string>} */
|
|
18
|
+
const compToType = {
|
|
19
|
+
'text-field': 'text',
|
|
20
|
+
'number-field': 'number',
|
|
21
|
+
textarea: 'textarea',
|
|
22
|
+
checkbox: 'boolean',
|
|
23
|
+
'date-picker': 'date',
|
|
24
|
+
'date-time-picker': 'datetime',
|
|
25
|
+
'time-picker': 'time',
|
|
26
|
+
select: 'select',
|
|
27
|
+
autocomplete: 'autocomplete',
|
|
28
|
+
combobox: 'combobox',
|
|
29
|
+
'number-combobox': 'number-combobox',
|
|
30
|
+
'one-of-select': 'variant-selector',
|
|
31
|
+
list: 'array',
|
|
32
|
+
section: 'section',
|
|
33
|
+
slider: 'slider',
|
|
34
|
+
'file-input': 'file',
|
|
35
|
+
slot: 'slot',
|
|
36
|
+
'composite-slot': 'composite-slot'
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
/**
|
|
18
40
|
* @param {string} comp
|
|
19
41
|
* @returns {string[]|undefined}
|
|
@@ -24,50 +46,35 @@ function getConstraintKeys (comp) {
|
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
/**
|
|
27
|
-
* @
|
|
28
|
-
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
29
|
-
* @returns {{
|
|
30
|
-
* key: string|number,
|
|
49
|
+
* @typedef {{
|
|
31
50
|
* path: string,
|
|
32
|
-
*
|
|
51
|
+
* type: string,
|
|
33
52
|
* data: unknown,
|
|
34
53
|
* title?: string,
|
|
35
54
|
* label?: string,
|
|
36
55
|
* help?: string,
|
|
37
56
|
* error?: string,
|
|
38
|
-
* childError?: boolean,
|
|
39
57
|
* required?: boolean,
|
|
40
58
|
* readOnly?: boolean,
|
|
59
|
+
* modified?: boolean,
|
|
41
60
|
* constraints?: Record<string, unknown>,
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
61
|
+
* variants?: Array<{index: number, title: string}>,
|
|
62
|
+
* selectedVariant?: number,
|
|
63
|
+
* children?: Array<ProjectedNode>,
|
|
64
|
+
* getSuggestions?: boolean
|
|
65
|
+
* }} ProjectedNode
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {import('../state/types.js').StateNode} node
|
|
70
|
+
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
71
|
+
* @returns {ProjectedNode}
|
|
46
72
|
*/
|
|
47
73
|
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
|
-
*/
|
|
74
|
+
/** @type {ProjectedNode} */
|
|
67
75
|
const out = {
|
|
68
|
-
key: node.key,
|
|
69
76
|
path: node.fullKey,
|
|
70
|
-
|
|
77
|
+
type: compToType[node.layout.comp] || node.layout.comp,
|
|
71
78
|
data: node.data
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -77,10 +84,10 @@ export function projectNode (node, statefulLayout) {
|
|
|
77
84
|
if (node.layout.help) out.help = node.layout.help
|
|
78
85
|
|
|
79
86
|
if (node.error) out.error = node.error
|
|
80
|
-
if (node.childError) out.childError = true
|
|
81
87
|
|
|
82
88
|
if (node.skeleton.required) out.required = true
|
|
83
89
|
if (node.options.readOnly) out.readOnly = true
|
|
90
|
+
if (node.modified) out.modified = true
|
|
84
91
|
if (isItemsLayout(node.layout, statefulLayout.compiledLayout.components)) out.getSuggestions = true
|
|
85
92
|
|
|
86
93
|
const keys = getConstraintKeys(node.layout.comp)
|
|
@@ -95,9 +102,12 @@ export function projectNode (node, statefulLayout) {
|
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
if (node.layout.comp === 'one-of-select' && Array.isArray(layout.oneOfItems)) {
|
|
98
|
-
out.
|
|
105
|
+
out.variants = layout.oneOfItems
|
|
99
106
|
.filter((item) => !item.header)
|
|
100
|
-
.map((item) => ({
|
|
107
|
+
.map((item) => ({ index: item.key, title: item.title }))
|
|
108
|
+
// find selected variant
|
|
109
|
+
const selected = layout.oneOfItems.find((item) => item.selected)
|
|
110
|
+
if (selected) out.selectedVariant = selected.key
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
if (node.children) {
|
|
@@ -109,10 +119,27 @@ export function projectNode (node, statefulLayout) {
|
|
|
109
119
|
return out
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Project a single field result for slim mutation responses
|
|
124
|
+
* @param {import('../state/types.js').StateNode} node
|
|
125
|
+
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
126
|
+
* @returns {{ path: string, type: string, data: unknown, error?: string }}
|
|
127
|
+
*/
|
|
128
|
+
export function projectFieldResult (node, statefulLayout) {
|
|
129
|
+
/** @type {{ path: string, type: string, data: unknown, error?: string }} */
|
|
130
|
+
const out = {
|
|
131
|
+
path: node.fullKey,
|
|
132
|
+
type: compToType[node.layout.comp] || node.layout.comp,
|
|
133
|
+
data: node.data
|
|
134
|
+
}
|
|
135
|
+
if (node.error) out.error = node.error
|
|
136
|
+
return out
|
|
137
|
+
}
|
|
138
|
+
|
|
112
139
|
/**
|
|
113
140
|
* @param {import('../state/types.js').StateTree} stateTree
|
|
114
141
|
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
115
|
-
* @returns {{ root:
|
|
142
|
+
* @returns {{ root: ProjectedNode, valid: boolean }}
|
|
116
143
|
*/
|
|
117
144
|
export function projectStateTree (stateTree, statefulLayout) {
|
|
118
145
|
return {
|
|
@@ -121,6 +148,154 @@ export function projectStateTree (stateTree, statefulLayout) {
|
|
|
121
148
|
}
|
|
122
149
|
}
|
|
123
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Format a projected node as a markdown line for LLM-readable output.
|
|
153
|
+
* @param {import('../state/types.js').StateNode} node
|
|
154
|
+
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
155
|
+
* @param {number} [depth]
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
export function projectNodeToMarkdown (node, statefulLayout, depth = 0) {
|
|
159
|
+
const indent = ' '.repeat(depth)
|
|
160
|
+
const type = compToType[node.layout.comp] || node.layout.comp
|
|
161
|
+
const layout = /** @type {Record<string, unknown>} */(node.layout)
|
|
162
|
+
|
|
163
|
+
// build metadata tags
|
|
164
|
+
const meta = [type]
|
|
165
|
+
if (node.skeleton.required) meta.push('required')
|
|
166
|
+
if (node.options.readOnly) meta.push('readOnly')
|
|
167
|
+
if (node.error) meta.push('error')
|
|
168
|
+
if (node.modified) meta.push('modified')
|
|
169
|
+
|
|
170
|
+
// constraints
|
|
171
|
+
const keys = getConstraintKeys(node.layout.comp)
|
|
172
|
+
if (keys) {
|
|
173
|
+
for (const k of keys) {
|
|
174
|
+
const v = layout[k]
|
|
175
|
+
if (v !== undefined && v !== null) meta.push(`${k}=${v}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// variants
|
|
180
|
+
if (node.layout.comp === 'one-of-select' && Array.isArray(layout.oneOfItems)) {
|
|
181
|
+
const selected = layout.oneOfItems.find((item) => item.selected)
|
|
182
|
+
if (selected) meta.push(`selected=${selected.key}`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isItemsLayout(node.layout, statefulLayout.compiledLayout.components)) meta.push('suggestions')
|
|
186
|
+
|
|
187
|
+
// array item count
|
|
188
|
+
if (node.layout.comp === 'list' && Array.isArray(node.data)) {
|
|
189
|
+
meta.push(`${node.data.length} items`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const path = node.fullKey || '/'
|
|
193
|
+
let line = `${indent}- ${path} (${meta.join(', ')})`
|
|
194
|
+
|
|
195
|
+
if (typeof layout.label === 'string') line += ` label="${layout.label}"`
|
|
196
|
+
else if (typeof layout.title === 'string') line += ` title="${layout.title}"`
|
|
197
|
+
|
|
198
|
+
// value for leaf nodes (no children or empty children)
|
|
199
|
+
if (!node.children || node.children.length === 0) {
|
|
200
|
+
line += ` value=${JSON.stringify(node.data)}`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (node.error) line += ` — ${node.error}`
|
|
204
|
+
|
|
205
|
+
const lines = [line]
|
|
206
|
+
|
|
207
|
+
// variants list
|
|
208
|
+
if (node.layout.comp === 'one-of-select' && Array.isArray(layout.oneOfItems)) {
|
|
209
|
+
const variants = layout.oneOfItems.filter((item) => !item.header)
|
|
210
|
+
for (const v of variants) {
|
|
211
|
+
lines.push(`${indent} - variant ${v.key}: ${v.title}`)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// recurse children
|
|
216
|
+
if (node.children) {
|
|
217
|
+
for (const child of node.children) {
|
|
218
|
+
if (child.layout.comp === 'none') continue
|
|
219
|
+
lines.push(projectNodeToMarkdown(child, statefulLayout, depth + 1))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return lines.join('\n')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Format a state tree as markdown for LLM-readable output.
|
|
228
|
+
* @param {import('../state/types.js').StateTree} stateTree
|
|
229
|
+
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
230
|
+
* @returns {string}
|
|
231
|
+
*/
|
|
232
|
+
export function projectStateTreeToMarkdown (stateTree, statefulLayout) {
|
|
233
|
+
const errors = collectErrors(stateTree.root)
|
|
234
|
+
const validLine = stateTree.valid
|
|
235
|
+
? 'valid: true, no errors'
|
|
236
|
+
: `valid: false, ${errors.length} error(s)`
|
|
237
|
+
|
|
238
|
+
const lines = [validLine, '']
|
|
239
|
+
|
|
240
|
+
if (!stateTree.valid && errors.length > 0) {
|
|
241
|
+
lines.push('Errors:')
|
|
242
|
+
for (const e of errors) {
|
|
243
|
+
lines.push(`- ${e.path}: ${e.message}`)
|
|
244
|
+
}
|
|
245
|
+
lines.push('')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lines.push('Fields:')
|
|
249
|
+
lines.push(projectNodeToMarkdown(stateTree.root, statefulLayout, 0))
|
|
250
|
+
|
|
251
|
+
return lines.join('\n')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Format a mutation result as concise text for LLM-readable output.
|
|
256
|
+
* @param {boolean} valid
|
|
257
|
+
* @param {Array<{path: string, message: string}>} errors
|
|
258
|
+
* @param {string} [prefix] - optional prefix line (e.g. field info)
|
|
259
|
+
* @returns {string}
|
|
260
|
+
*/
|
|
261
|
+
export function formatMutationResult (valid, errors, prefix) {
|
|
262
|
+
const lines = []
|
|
263
|
+
if (prefix) lines.push(prefix)
|
|
264
|
+
|
|
265
|
+
if (valid) {
|
|
266
|
+
lines.push('valid, no errors')
|
|
267
|
+
} else {
|
|
268
|
+
lines.push(`invalid, ${errors.length} error(s)`)
|
|
269
|
+
if (errors.length > 0) {
|
|
270
|
+
lines.push('Errors:')
|
|
271
|
+
for (const e of errors) {
|
|
272
|
+
lines.push(`- ${e.path}: ${e.message}`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return lines.join('\n')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Format field suggestions as markdown for LLM-readable output.
|
|
282
|
+
* @param {Array<{value: unknown, title: string, key?: string}>} items
|
|
283
|
+
* @returns {string}
|
|
284
|
+
*/
|
|
285
|
+
export function formatSuggestions (items) {
|
|
286
|
+
if (items.length === 0) return 'No suggestions available'
|
|
287
|
+
const lines = ['Suggestions (use the value with setFieldValue or setData):']
|
|
288
|
+
for (const item of items) {
|
|
289
|
+
const val = JSON.stringify(item.value)
|
|
290
|
+
if (item.key && item.key !== item.title) {
|
|
291
|
+
lines.push(`- value=${val} — ${item.title} (${item.key})`)
|
|
292
|
+
} else {
|
|
293
|
+
lines.push(`- value=${val} — ${item.title}`)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return lines.join('\n')
|
|
297
|
+
}
|
|
298
|
+
|
|
124
299
|
/**
|
|
125
300
|
* @param {import('../state/types.js').StateNode} node
|
|
126
301
|
* @returns {Array<{path: string, message: string}>}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @file describeState tool
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { projectStateTree, projectNode, collectErrors } from '../project.js'
|
|
5
|
+
import { projectStateTree, projectNode, collectErrors, projectNodeToMarkdown, projectStateTreeToMarkdown, formatMutationResult } from '../project.js'
|
|
6
6
|
import { resolveNode } from '../resolve.js'
|
|
7
7
|
|
|
8
8
|
export const inputSchema = {
|
|
@@ -40,10 +40,15 @@ export const outputSchema = {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* @param {string} dataTitle
|
|
43
|
+
* @param {"small"|"medium"|"large"} [complexity]
|
|
43
44
|
* @returns {string}
|
|
44
45
|
*/
|
|
45
|
-
export function getDescription (dataTitle) {
|
|
46
|
-
|
|
46
|
+
export function getDescription (dataTitle, complexity) {
|
|
47
|
+
let desc = `Describe the "${dataTitle}" form structure, field types, constraints, and current errors.`
|
|
48
|
+
if (complexity === 'large') {
|
|
49
|
+
desc += ' Use the "path" parameter to focus on a subtree — avoid calling without a path on large forms.'
|
|
50
|
+
}
|
|
51
|
+
return desc
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/**
|
|
@@ -72,3 +77,23 @@ export function execute (statefulLayout, args) {
|
|
|
72
77
|
errors
|
|
73
78
|
}
|
|
74
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {import('../../state/index.js').StatefulLayout} statefulLayout
|
|
83
|
+
* @param {{ path?: string }} args
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export function toMarkdown (statefulLayout, args) {
|
|
87
|
+
if (args.path) {
|
|
88
|
+
const node = resolveNode(statefulLayout.stateTree.root, args.path)
|
|
89
|
+
if (!node) {
|
|
90
|
+
throw new Error(`node not found at path: ${args.path}`)
|
|
91
|
+
}
|
|
92
|
+
const errors = collectErrors(node)
|
|
93
|
+
return formatMutationResult(statefulLayout.valid, errors,
|
|
94
|
+
projectNodeToMarkdown(node, statefulLayout)
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return projectStateTreeToMarkdown(statefulLayout.stateTree, statefulLayout)
|
|
99
|
+
}
|