@json-layout/core 2.8.2 → 2.9.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/package.json +4 -2
- package/src/compile/index.js +3 -1
- package/src/compile/skeleton-node.js +46 -5
- package/src/compile/types.ts +1 -0
- package/src/compile/utils/resolve-refs.js +5 -8
- package/src/compile/utils/x-i18n.js +15 -6
- package/src/state/index.js +18 -1
- package/src/state/state-node.js +11 -2
- package/src/state/utils/urls.js +2 -0
- package/src/utils/json-pointer.js +29 -0
- package/src/webmcp/README.md +144 -0
- package/src/webmcp/index.js +88 -82
- package/src/webmcp/project.js +542 -111
- package/src/webmcp/resolve.js +37 -1
- package/src/webmcp/schema.js +169 -0
- package/src/webmcp/suggestions-store.js +121 -0
- package/src/webmcp/tools/describe-state.js +20 -64
- package/src/webmcp/tools/edit-array.js +51 -28
- package/src/webmcp/tools/fill-form-skill.js +17 -41
- package/src/webmcp/tools/get-data.js +66 -13
- package/src/webmcp/tools/get-field-suggestions.js +12 -23
- package/src/webmcp/tools/set-data.js +79 -28
- package/src/webmcp/tools/set-field-value.js +88 -39
- package/src/webmcp/variants-memo.js +53 -0
- package/types/compile/index.d.ts.map +1 -1
- package/types/compile/skeleton-node.d.ts +9 -2
- package/types/compile/skeleton-node.d.ts.map +1 -1
- package/types/compile/types.d.ts +1 -0
- package/types/compile/types.d.ts.map +1 -1
- package/types/compile/utils/resolve-refs.d.ts.map +1 -1
- package/types/compile/utils/x-i18n.d.ts +1 -1
- package/types/compile/utils/x-i18n.d.ts.map +1 -1
- package/types/state/index.d.ts +10 -0
- package/types/state/index.d.ts.map +1 -1
- package/types/state/state-node.d.ts.map +1 -1
- package/types/state/utils/urls.d.ts.map +1 -1
- package/types/utils/json-pointer.d.ts +22 -0
- package/types/utils/json-pointer.d.ts.map +1 -0
- package/types/webmcp/index.d.ts +23 -15
- package/types/webmcp/index.d.ts.map +1 -1
- package/types/webmcp/project.d.ts +178 -57
- package/types/webmcp/project.d.ts.map +1 -1
- package/types/webmcp/resolve.d.ts +7 -3
- package/types/webmcp/resolve.d.ts.map +1 -1
- package/types/webmcp/schema.d.ts +44 -0
- package/types/webmcp/schema.d.ts.map +1 -0
- package/types/webmcp/suggestions-store.d.ts +82 -0
- package/types/webmcp/suggestions-store.d.ts.map +1 -0
- package/types/webmcp/tools/describe-state.d.ts +5 -57
- package/types/webmcp/tools/describe-state.d.ts.map +1 -1
- package/types/webmcp/tools/edit-array.d.ts +8 -37
- package/types/webmcp/tools/edit-array.d.ts.map +1 -1
- package/types/webmcp/tools/fill-form-skill.d.ts +10 -13
- package/types/webmcp/tools/fill-form-skill.d.ts.map +1 -1
- package/types/webmcp/tools/get-data.d.ts +17 -15
- package/types/webmcp/tools/get-data.d.ts.map +1 -1
- package/types/webmcp/tools/get-field-suggestions.d.ts +4 -30
- package/types/webmcp/tools/get-field-suggestions.d.ts.map +1 -1
- package/types/webmcp/tools/set-data.d.ts +17 -34
- package/types/webmcp/tools/set-data.d.ts.map +1 -1
- package/types/webmcp/tools/set-field-value.d.ts +20 -57
- package/types/webmcp/tools/set-field-value.d.ts.map +1 -1
- package/types/webmcp/variants-memo.d.ts +42 -0
- package/types/webmcp/variants-memo.d.ts.map +1 -0
package/src/webmcp/index.js
CHANGED
|
@@ -12,7 +12,10 @@ import * as getData from './tools/get-data.js'
|
|
|
12
12
|
import * as getFieldSuggestions from './tools/get-field-suggestions.js'
|
|
13
13
|
import * as editArray from './tools/edit-array.js'
|
|
14
14
|
import * as fillFormSkill from './tools/fill-form-skill.js'
|
|
15
|
-
import { formatMutationResult, formatSuggestions } from './project.js'
|
|
15
|
+
import { formatMutationResult, formatSuggestions, projectSuggestions, abbreviateValue, formatVisibilityDiff, suggestionsBlocked, suggestionsSource } from './project.js'
|
|
16
|
+
import { resolveNode } from './resolve.js'
|
|
17
|
+
import { SuggestionsStore } from './suggestions-store.js'
|
|
18
|
+
import { VariantsMemo } from './variants-memo.js'
|
|
16
19
|
|
|
17
20
|
/** @typedef {import('@mcp-b/webmcp-types').ToolDescriptor} ToolDescriptor */
|
|
18
21
|
|
|
@@ -42,22 +45,10 @@ function parseIfJsonString (value) {
|
|
|
42
45
|
* @typedef {object} WebMCPOptions
|
|
43
46
|
* @property {string} [prefixName] - Prefix for all tool names
|
|
44
47
|
* @property {string} [dataTitle] - Title used in descriptions (default: 'form')
|
|
45
|
-
* @property {object} [schema] - The original JSON schema
|
|
46
48
|
* @property {boolean} [includeFillFormSkill] - Include the fillFormSkill tool (default: false)
|
|
47
49
|
* @property {boolean} [includeSubAgent] - Include a subagent_ tool wrapping all form tools (default: false)
|
|
48
50
|
*/
|
|
49
51
|
|
|
50
|
-
/**
|
|
51
|
-
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
52
|
-
* @returns {"small"|"medium"|"large"}
|
|
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
|
-
}
|
|
60
|
-
|
|
61
52
|
/**
|
|
62
53
|
* WebMCP class that provides MCP tool descriptors for a StatefulLayout instance
|
|
63
54
|
*/
|
|
@@ -82,32 +73,35 @@ export class WebMCP {
|
|
|
82
73
|
|
|
83
74
|
/**
|
|
84
75
|
* @readonly
|
|
85
|
-
* @type {
|
|
76
|
+
* @type {boolean}
|
|
86
77
|
*/
|
|
87
|
-
|
|
78
|
+
_includeFillFormSkill = false
|
|
88
79
|
|
|
89
80
|
/**
|
|
90
81
|
* @readonly
|
|
91
|
-
* @type {
|
|
82
|
+
* @type {boolean}
|
|
92
83
|
*/
|
|
93
|
-
|
|
84
|
+
_includeSubAgent = false
|
|
94
85
|
|
|
95
86
|
/**
|
|
96
|
-
* @
|
|
97
|
-
* @type {boolean}
|
|
87
|
+
* @type {string[]}
|
|
98
88
|
*/
|
|
99
|
-
|
|
89
|
+
_registeredTools = []
|
|
100
90
|
|
|
101
91
|
/**
|
|
92
|
+
* memory of the last suggestions per node path, used by setFieldValue's suggestionIndex
|
|
102
93
|
* @readonly
|
|
103
|
-
* @type {
|
|
94
|
+
* @type {SuggestionsStore}
|
|
104
95
|
*/
|
|
105
|
-
|
|
96
|
+
_suggestionsStore = new SuggestionsStore()
|
|
106
97
|
|
|
107
98
|
/**
|
|
108
|
-
*
|
|
99
|
+
* Variant lists already sent to the agent. Never cleared on a write: a schema's branches
|
|
100
|
+
* are a constant, so unlike memorized suggestions nothing about the data can invalidate
|
|
101
|
+
* them.
|
|
102
|
+
* @type {VariantsMemo}
|
|
109
103
|
*/
|
|
110
|
-
|
|
104
|
+
_variantsMemo = new VariantsMemo()
|
|
111
105
|
|
|
112
106
|
/**
|
|
113
107
|
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
@@ -117,10 +111,8 @@ export class WebMCP {
|
|
|
117
111
|
this._statefulLayout = statefulLayout
|
|
118
112
|
this._prefixName = options.prefixName || ''
|
|
119
113
|
this._dataTitle = options.dataTitle || 'form'
|
|
120
|
-
this._schema = options.schema || null
|
|
121
114
|
this._includeFillFormSkill = options.includeFillFormSkill || false
|
|
122
115
|
this._includeSubAgent = options.includeSubAgent || false
|
|
123
|
-
this._complexity = getComplexity(statefulLayout)
|
|
124
116
|
}
|
|
125
117
|
|
|
126
118
|
/**
|
|
@@ -136,17 +128,15 @@ export class WebMCP {
|
|
|
136
128
|
*/
|
|
137
129
|
getTools () {
|
|
138
130
|
const dataTitle = this._dataTitle
|
|
139
|
-
const complexity = this._complexity
|
|
140
131
|
|
|
141
132
|
/** @type {ToolDescriptor[]} */
|
|
142
133
|
const tools = []
|
|
143
134
|
|
|
144
135
|
if (this._includeFillFormSkill) {
|
|
145
|
-
const skill = fillFormSkill.generateSkill(dataTitle, this._prefixName
|
|
136
|
+
const skill = fillFormSkill.generateSkill(dataTitle, this._prefixName)
|
|
146
137
|
tools.push({
|
|
147
138
|
name: this._toolName('fillFormSkill'),
|
|
148
139
|
description: fillFormSkill.getDescription(dataTitle),
|
|
149
|
-
outputSchema: { type: 'string' },
|
|
150
140
|
execute: async (args) => {
|
|
151
141
|
try {
|
|
152
142
|
return {
|
|
@@ -166,15 +156,18 @@ export class WebMCP {
|
|
|
166
156
|
tools.push(
|
|
167
157
|
{
|
|
168
158
|
name: this._toolName('getData'),
|
|
169
|
-
description:
|
|
159
|
+
description: getData.getDescription(dataTitle),
|
|
170
160
|
inputSchema: getData.inputSchema,
|
|
171
|
-
outputSchema: getData.outputSchema,
|
|
172
161
|
execute: async (args) => {
|
|
173
162
|
try {
|
|
174
163
|
const result = getData.execute(this._statefulLayout, args || {})
|
|
164
|
+
// Returned whole, always. This tool's answer IS the data: an agent may hand it
|
|
165
|
+
// to an API, and a document with named placeholders where its values should be
|
|
166
|
+
// would be forwarded as those strings with nothing looking wrong. Volume on a
|
|
167
|
+
// large form is a question of when to call this at all, which the guide
|
|
168
|
+
// answers — not a licence for the tool to answer with something else.
|
|
175
169
|
return {
|
|
176
|
-
content: [{ type: 'text', text: JSON.stringify(result) }]
|
|
177
|
-
structuredContent: result
|
|
170
|
+
content: [{ type: 'text', text: JSON.stringify(result) }]
|
|
178
171
|
}
|
|
179
172
|
} catch (err) {
|
|
180
173
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -187,9 +180,8 @@ export class WebMCP {
|
|
|
187
180
|
},
|
|
188
181
|
{
|
|
189
182
|
name: this._toolName('setData'),
|
|
190
|
-
description: setData.getDescription(dataTitle
|
|
183
|
+
description: setData.getDescription(dataTitle),
|
|
191
184
|
inputSchema: setData.inputSchema,
|
|
192
|
-
outputSchema: setData.outputSchema,
|
|
193
185
|
execute: async (args) => {
|
|
194
186
|
try {
|
|
195
187
|
if (!args?.data) {
|
|
@@ -198,11 +190,22 @@ export class WebMCP {
|
|
|
198
190
|
args.data = parseIfJsonString(args.data)
|
|
199
191
|
const result = setData.execute(
|
|
200
192
|
this._statefulLayout,
|
|
201
|
-
/** @type {{ data: unknown }} */(args)
|
|
193
|
+
/** @type {{ data: unknown, merge?: boolean }} */(args)
|
|
202
194
|
)
|
|
195
|
+
// the whole data was replaced, what a memorized path designates may have changed
|
|
196
|
+
this._suggestionsStore.clear()
|
|
197
|
+
const warnings = []
|
|
198
|
+
if (result.removed.length) {
|
|
199
|
+
warnings.push(`removed ${result.removed.length} key(s) not present in the data you passed: ${result.removed.join(', ')} — pass merge=true to keep them`)
|
|
200
|
+
}
|
|
201
|
+
if (result.unknownKeys.length) {
|
|
202
|
+
warnings.push(`${result.unknownKeys.length} key(s) match no field of this form and were ignored by it: ${result.unknownKeys.join(', ')} — check for a typo with describeState`)
|
|
203
|
+
}
|
|
204
|
+
const visibilityInfo = result.visibility ? formatVisibilityDiff(result.visibility).replace(/^\n/, '') : ''
|
|
205
|
+
const stored = result.written.length ? `stored ${result.written.length} key(s): ${result.written.join(', ')}` : ''
|
|
206
|
+
const text = [formatMutationResult(result.valid, result.errors), ...(stored ? [stored] : []), ...(visibilityInfo ? [visibilityInfo] : []), ...warnings].join('\n')
|
|
203
207
|
return {
|
|
204
|
-
content: [{ type: 'text', text
|
|
205
|
-
structuredContent: result
|
|
208
|
+
content: [{ type: 'text', text }]
|
|
206
209
|
}
|
|
207
210
|
} catch (err) {
|
|
208
211
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -215,16 +218,13 @@ export class WebMCP {
|
|
|
215
218
|
},
|
|
216
219
|
{
|
|
217
220
|
name: this._toolName('describeState'),
|
|
218
|
-
description: describeState.getDescription(dataTitle
|
|
221
|
+
description: describeState.getDescription(dataTitle),
|
|
219
222
|
inputSchema: describeState.inputSchema,
|
|
220
|
-
outputSchema: describeState.outputSchema,
|
|
221
223
|
execute: async (args) => {
|
|
222
224
|
try {
|
|
223
|
-
const
|
|
224
|
-
const text = describeState.toMarkdown(this._statefulLayout, args || {})
|
|
225
|
+
const text = describeState.toMarkdown(this._statefulLayout, args || {}, this._variantsMemo)
|
|
225
226
|
return {
|
|
226
|
-
content: [{ type: 'text', text }]
|
|
227
|
-
structuredContent: result
|
|
227
|
+
content: [{ type: 'text', text }]
|
|
228
228
|
}
|
|
229
229
|
} catch (err) {
|
|
230
230
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -239,21 +239,34 @@ export class WebMCP {
|
|
|
239
239
|
name: this._toolName('setFieldValue'),
|
|
240
240
|
description: setFieldValue.getDescription(dataTitle),
|
|
241
241
|
inputSchema: setFieldValue.inputSchema,
|
|
242
|
-
outputSchema: setFieldValue.outputSchema,
|
|
243
242
|
execute: async (args) => {
|
|
244
243
|
try {
|
|
245
244
|
if (!args?.path) {
|
|
246
245
|
throw new Error('path is required')
|
|
247
246
|
}
|
|
248
|
-
args.value = parseIfJsonString(args.value)
|
|
247
|
+
if (args.value !== undefined) args.value = parseIfJsonString(args.value)
|
|
249
248
|
const result = setFieldValue.execute(
|
|
250
249
|
this._statefulLayout,
|
|
251
|
-
/** @type {{ path: string, value
|
|
250
|
+
/** @type {{ path: string, value?: unknown, suggestionIndex?: number }} */(args),
|
|
251
|
+
this._suggestionsStore,
|
|
252
|
+
this._variantsMemo
|
|
252
253
|
)
|
|
253
|
-
|
|
254
|
+
// A getItems expression can depend on another field, so this write may have
|
|
255
|
+
// changed the options of a field memorized under an unchanged path — but only
|
|
256
|
+
// of a field whose list actually depends on it. Comparing each memorized path's
|
|
257
|
+
// itemsCacheKey against the node's current one is how the state layer itself
|
|
258
|
+
// decides whether to re-fetch.
|
|
259
|
+
this._suggestionsStore.retainFresh((path, cacheKey) => {
|
|
260
|
+
const node = resolveNode(this._statefulLayout.stateTree.root, path)
|
|
261
|
+
return !!node && node.itemsCacheKey === cacheKey
|
|
262
|
+
})
|
|
263
|
+
let fieldInfo = `${result.field.path} (${result.field.type}) = ${abbreviateValue(result.field.data)}`
|
|
264
|
+
if (result.visibility) fieldInfo += formatVisibilityDiff(result.visibility)
|
|
265
|
+
if (result.activatedMarkdown) {
|
|
266
|
+
fieldInfo += `\nFields of the activated variant:\n${result.activatedMarkdown}`
|
|
267
|
+
}
|
|
254
268
|
return {
|
|
255
|
-
content: [{ type: 'text', text: formatMutationResult(result.valid, result.errors, fieldInfo) }]
|
|
256
|
-
structuredContent: result
|
|
269
|
+
content: [{ type: 'text', text: formatMutationResult(result.valid, result.errors, fieldInfo, result.otherErrors) }]
|
|
257
270
|
}
|
|
258
271
|
} catch (err) {
|
|
259
272
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -266,9 +279,8 @@ export class WebMCP {
|
|
|
266
279
|
},
|
|
267
280
|
{
|
|
268
281
|
name: this._toolName('getFieldSuggestions'),
|
|
269
|
-
description:
|
|
282
|
+
description: getFieldSuggestions.getDescription(dataTitle),
|
|
270
283
|
inputSchema: getFieldSuggestions.inputSchema,
|
|
271
|
-
outputSchema: getFieldSuggestions.outputSchema,
|
|
272
284
|
execute: async (args) => {
|
|
273
285
|
try {
|
|
274
286
|
if (!args?.path) {
|
|
@@ -276,11 +288,17 @@ export class WebMCP {
|
|
|
276
288
|
}
|
|
277
289
|
const result = await getFieldSuggestions.execute(
|
|
278
290
|
this._statefulLayout,
|
|
279
|
-
/** @type {{ path: string, query?: string }} */(args)
|
|
291
|
+
/** @type {{ path: string, query?: string }} */(args),
|
|
292
|
+
this._suggestionsStore
|
|
280
293
|
)
|
|
294
|
+
const suggestions = projectSuggestions(result.items, result.baseIndex)
|
|
295
|
+
// An empty answer has two causes the agent must tell apart: a query that
|
|
296
|
+
// matched nothing, and a list whose request could not be built because another
|
|
297
|
+
// field is still empty. Only the second is a reason to go somewhere else.
|
|
298
|
+
const node = resolveNode(this._statefulLayout.stateTree.root, /** @type {any} */(args).path)
|
|
299
|
+
const blockedOn = node && suggestionsBlocked(node) ? suggestionsSource(node) : undefined
|
|
281
300
|
return {
|
|
282
|
-
content: [{ type: 'text', text: formatSuggestions(
|
|
283
|
-
structuredContent: result
|
|
301
|
+
content: [{ type: 'text', text: formatSuggestions(suggestions, blockedOn) }]
|
|
284
302
|
}
|
|
285
303
|
} catch (err) {
|
|
286
304
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -295,7 +313,6 @@ export class WebMCP {
|
|
|
295
313
|
name: this._toolName('editArray'),
|
|
296
314
|
description: editArray.getDescription(dataTitle),
|
|
297
315
|
inputSchema: editArray.inputSchema,
|
|
298
|
-
outputSchema: editArray.outputSchema,
|
|
299
316
|
execute: async (args) => {
|
|
300
317
|
try {
|
|
301
318
|
if (!args?.path || !args?.action) {
|
|
@@ -306,14 +323,20 @@ export class WebMCP {
|
|
|
306
323
|
}
|
|
307
324
|
const result = editArray.execute(
|
|
308
325
|
this._statefulLayout,
|
|
309
|
-
/** @type {{ path: string, action: 'add'|'remove', index?: number, value?: unknown }} */(args)
|
|
326
|
+
/** @type {{ path: string, action: 'add'|'remove', index?: number, value?: unknown }} */(args),
|
|
327
|
+
this._variantsMemo
|
|
310
328
|
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
329
|
+
// adding or removing an item shifts the paths of the items after it, so the
|
|
330
|
+
// suggestions memorized for those paths now designate another item
|
|
331
|
+
this._suggestionsStore.clear()
|
|
332
|
+
let actionInfo = args.action === 'add'
|
|
333
|
+
? `added item at index ${result.index}, ${result.itemCount} total`
|
|
334
|
+
: `removed item at index ${result.index}, ${result.itemCount} remaining`
|
|
335
|
+
if (result.itemMarkdown) {
|
|
336
|
+
actionInfo += `\nFields of the new item (activated for edition):\n${result.itemMarkdown}`
|
|
337
|
+
}
|
|
314
338
|
return {
|
|
315
|
-
content: [{ type: 'text', text: formatMutationResult(result.valid, result.errors, actionInfo) }]
|
|
316
|
-
structuredContent: result
|
|
339
|
+
content: [{ type: 'text', text: formatMutationResult(result.valid, result.errors, actionInfo, result.otherErrors) }]
|
|
317
340
|
}
|
|
318
341
|
} catch (err) {
|
|
319
342
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -326,26 +349,9 @@ export class WebMCP {
|
|
|
326
349
|
}
|
|
327
350
|
)
|
|
328
351
|
|
|
329
|
-
if (this._schema) {
|
|
330
|
-
tools.push({
|
|
331
|
-
name: this._toolName('getSchema'),
|
|
332
|
-
description: `Get the JSON schema that governs the "${dataTitle}" form.`,
|
|
333
|
-
outputSchema: {
|
|
334
|
-
type: 'object',
|
|
335
|
-
description: 'The JSON schema definition'
|
|
336
|
-
},
|
|
337
|
-
execute: async (args) => {
|
|
338
|
-
return {
|
|
339
|
-
content: [{ type: 'text', text: JSON.stringify(this._schema) }],
|
|
340
|
-
structuredContent: this._schema
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
})
|
|
344
|
-
}
|
|
345
|
-
|
|
346
352
|
if (this._includeSubAgent) {
|
|
347
353
|
const toolNames = tools.map(t => t.name)
|
|
348
|
-
const prompt = fillFormSkill.generateSkill(dataTitle, this._prefixName
|
|
354
|
+
const prompt = fillFormSkill.generateSkill(dataTitle, this._prefixName)
|
|
349
355
|
tools.push({
|
|
350
356
|
name: `subagent_${this._toolName('form')}`,
|
|
351
357
|
description: `Delegate a form-filling task for "${dataTitle}" to a specialized sub-agent`,
|
|
@@ -357,9 +363,9 @@ export class WebMCP {
|
|
|
357
363
|
required: ['task']
|
|
358
364
|
},
|
|
359
365
|
execute: async () => {
|
|
366
|
+
const config = { prompt, tools: toolNames }
|
|
360
367
|
return {
|
|
361
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
362
|
-
structuredContent: { prompt, tools: toolNames }
|
|
368
|
+
content: [{ type: 'text', text: JSON.stringify(config) }]
|
|
363
369
|
}
|
|
364
370
|
}
|
|
365
371
|
})
|