@olonjs/core 1.0.93 → 1.0.94

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olonjs/core",
3
- "version": "1.0.93",
3
+ "version": "1.0.94",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/olonjs-core.umd.cjs",
@@ -11,10 +11,12 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/olonjs-core.js",
13
13
  "require": "./dist/olonjs-core.umd.cjs"
14
- }
14
+ },
15
+ "./package.json": "./package.json"
15
16
  },
16
17
  "files": [
17
- "dist"
18
+ "dist",
19
+ "src/lib/webmcp-contracts.mjs"
18
20
  ],
19
21
  "description": "The Sovereign Core Engine for OlonJS.",
20
22
  "author": "JsonPages Team",
@@ -0,0 +1,349 @@
1
+ import { z } from 'zod';
2
+
3
+ const WEBMCP_TOOL_REQUEST_TYPE = 'olonjs:webmcp:tool-call';
4
+ const WEBMCP_TOOL_RESULT_TYPE = 'olonjs:webmcp:tool-result';
5
+
6
+ function cloneJson(value) {
7
+ return value == null ? value : JSON.parse(JSON.stringify(value));
8
+ }
9
+
10
+ function getTypeName(schema) {
11
+ return schema?._def?.typeName;
12
+ }
13
+
14
+ function unwrapSchema(schema) {
15
+ let current = schema;
16
+ let isOptional = false;
17
+ let isNullable = false;
18
+ let defaultValue;
19
+
20
+ for (;;) {
21
+ const typeName = getTypeName(current);
22
+ if (typeName === z.ZodFirstPartyTypeKind.ZodOptional) {
23
+ isOptional = true;
24
+ current = current._def.innerType;
25
+ continue;
26
+ }
27
+ if (typeName === z.ZodFirstPartyTypeKind.ZodDefault) {
28
+ isOptional = true;
29
+ if (defaultValue === undefined) {
30
+ try {
31
+ defaultValue = current._def.defaultValue();
32
+ } catch {
33
+ defaultValue = undefined;
34
+ }
35
+ }
36
+ current = current._def.innerType;
37
+ continue;
38
+ }
39
+ if (typeName === z.ZodFirstPartyTypeKind.ZodNullable) {
40
+ isNullable = true;
41
+ current = current._def.innerType;
42
+ continue;
43
+ }
44
+ break;
45
+ }
46
+
47
+ return { schema: current, isOptional, isNullable, defaultValue };
48
+ }
49
+
50
+ function withSchemaMetadata(schema, jsonSchema, meta) {
51
+ const next = cloneJson(jsonSchema) ?? {};
52
+ if (schema?.description && next.description == null) {
53
+ next.description = schema.description;
54
+ }
55
+ if (meta.defaultValue !== undefined && next.default == null) {
56
+ next.default = meta.defaultValue;
57
+ }
58
+ if (meta.isNullable) {
59
+ return { anyOf: [next, { type: 'null' }] };
60
+ }
61
+ return next;
62
+ }
63
+
64
+ function unionToEnum(options) {
65
+ const values = [];
66
+ let primitiveType = null;
67
+
68
+ for (const option of options) {
69
+ const unwrapped = unwrapSchema(option).schema;
70
+ const typeName = getTypeName(unwrapped);
71
+
72
+ if (typeName === z.ZodFirstPartyTypeKind.ZodLiteral) {
73
+ values.push(unwrapped._def.value);
74
+ primitiveType = primitiveType ?? typeof unwrapped._def.value;
75
+ continue;
76
+ }
77
+
78
+ if (typeName === z.ZodFirstPartyTypeKind.ZodEnum) {
79
+ for (const value of unwrapped._def.values) values.push(value);
80
+ primitiveType = primitiveType ?? 'string';
81
+ continue;
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ if (values.length === 0) return null;
88
+
89
+ if (primitiveType === 'number') return { type: 'number', enum: values };
90
+ if (primitiveType === 'boolean') return { type: 'boolean', enum: values };
91
+ return { type: 'string', enum: values.map((value) => String(value)) };
92
+ }
93
+
94
+ function zodToJsonSchema(schema) {
95
+ const meta = unwrapSchema(schema);
96
+ const current = meta.schema;
97
+ const typeName = getTypeName(current);
98
+
99
+ switch (typeName) {
100
+ case z.ZodFirstPartyTypeKind.ZodObject: {
101
+ const shape = current._def.shape();
102
+ const properties = {};
103
+ const required = [];
104
+
105
+ for (const [key, childSchema] of Object.entries(shape)) {
106
+ const childMeta = unwrapSchema(childSchema);
107
+ properties[key] = zodToJsonSchema(childSchema);
108
+ if (!childMeta.isOptional) required.push(key);
109
+ }
110
+
111
+ const objectSchema = {
112
+ type: 'object',
113
+ properties,
114
+ additionalProperties: false,
115
+ ...(required.length > 0 ? { required } : {}),
116
+ };
117
+ return withSchemaMetadata(schema, objectSchema, meta);
118
+ }
119
+
120
+ case z.ZodFirstPartyTypeKind.ZodString:
121
+ return withSchemaMetadata(schema, { type: 'string' }, meta);
122
+
123
+ case z.ZodFirstPartyTypeKind.ZodBoolean:
124
+ return withSchemaMetadata(schema, { type: 'boolean' }, meta);
125
+
126
+ case z.ZodFirstPartyTypeKind.ZodNumber: {
127
+ const checks = Array.isArray(current._def.checks) ? current._def.checks : [];
128
+ const isInteger = checks.some((check) => check.kind === 'int');
129
+ return withSchemaMetadata(schema, { type: isInteger ? 'integer' : 'number' }, meta);
130
+ }
131
+
132
+ case z.ZodFirstPartyTypeKind.ZodArray:
133
+ return withSchemaMetadata(schema, { type: 'array', items: zodToJsonSchema(current._def.type) }, meta);
134
+
135
+ case z.ZodFirstPartyTypeKind.ZodEnum:
136
+ return withSchemaMetadata(schema, { type: 'string', enum: [...current._def.values] }, meta);
137
+
138
+ case z.ZodFirstPartyTypeKind.ZodLiteral: {
139
+ const literal = current._def.value;
140
+ const primitiveType = literal === null ? 'null' : typeof literal;
141
+ return withSchemaMetadata(schema, { const: literal, ...(primitiveType !== 'object' ? { type: primitiveType } : {}) }, meta);
142
+ }
143
+
144
+ case z.ZodFirstPartyTypeKind.ZodRecord:
145
+ return withSchemaMetadata(schema, { type: 'object', additionalProperties: zodToJsonSchema(current._def.valueType) }, meta);
146
+
147
+ case z.ZodFirstPartyTypeKind.ZodUnion: {
148
+ const enumSchema = unionToEnum(current._def.options);
149
+ if (enumSchema) return withSchemaMetadata(schema, enumSchema, meta);
150
+ return withSchemaMetadata(
151
+ schema,
152
+ { anyOf: current._def.options.map((option) => zodToJsonSchema(option)) },
153
+ meta
154
+ );
155
+ }
156
+
157
+ default:
158
+ return withSchemaMetadata(schema, {}, meta);
159
+ }
160
+ }
161
+
162
+ function buildMutationInputSchema(sectionType, sectionDataSchema) {
163
+ return {
164
+ type: 'object',
165
+ additionalProperties: false,
166
+ properties: {
167
+ slug: {
168
+ type: 'string',
169
+ description: 'Canonical page slug currently open in Studio.',
170
+ },
171
+ sectionId: {
172
+ type: 'string',
173
+ description: 'Concrete section instance id inside the current draft.',
174
+ },
175
+ scope: {
176
+ type: 'string',
177
+ enum: ['local', 'global'],
178
+ default: 'local',
179
+ },
180
+ data: sectionDataSchema,
181
+ itemPath: {
182
+ type: 'array',
183
+ description: 'Optional root-to-leaf selection path for targeted field mutation.',
184
+ items: {
185
+ type: 'object',
186
+ additionalProperties: false,
187
+ properties: {
188
+ fieldKey: { type: 'string' },
189
+ itemId: { type: 'string' },
190
+ },
191
+ required: ['fieldKey'],
192
+ },
193
+ },
194
+ value: {
195
+ description: 'Value written to the final field targeted by itemPath.',
196
+ },
197
+ },
198
+ required: ['sectionId'],
199
+ oneOf: [
200
+ { required: ['data'] },
201
+ { required: ['itemPath', 'value'] },
202
+ ],
203
+ };
204
+ }
205
+
206
+ function getSectionTypes(pageConfig) {
207
+ return Array.from(
208
+ new Set((Array.isArray(pageConfig?.sections) ? pageConfig.sections : []).map((section) => section?.type).filter(Boolean))
209
+ );
210
+ }
211
+
212
+ function inferSectionLabel(section) {
213
+ const data = section?.data && typeof section.data === 'object' ? section.data : {};
214
+ if (typeof data.title === 'string' && data.title.trim()) return data.title.trim();
215
+ if (typeof data.sectionTitle === 'string' && data.sectionTitle.trim()) return data.sectionTitle.trim();
216
+ if (typeof data.label === 'string' && data.label.trim()) return data.label.trim();
217
+ return section?.type ?? 'section';
218
+ }
219
+
220
+ function buildToolName(sectionType) {
221
+ return `update-${String(sectionType)}`;
222
+ }
223
+
224
+ export function buildPageContractHref(slug) {
225
+ return `/schemas/${slug}.schema.json`;
226
+ }
227
+
228
+ export function buildPageManifestHref(slug) {
229
+ return `/mcp-manifests/${slug}.json`;
230
+ }
231
+
232
+ function getPageSections(pageConfig, siteConfig) {
233
+ const pageSections = Array.isArray(pageConfig?.sections) ? pageConfig.sections : [];
234
+ const site = siteConfig && typeof siteConfig === 'object' ? siteConfig : {};
235
+ const globalSections = [];
236
+
237
+ if (site.header && pageConfig?.['global-header'] !== false) {
238
+ globalSections.push({ ...site.header, scope: 'global' });
239
+ }
240
+ if (site.footer) {
241
+ globalSections.push({ ...site.footer, scope: 'global' });
242
+ }
243
+
244
+ return [
245
+ ...globalSections,
246
+ ...pageSections.map((section) => ({ ...section, scope: 'local' })),
247
+ ];
248
+ }
249
+
250
+ export function buildPageContract({ slug, pageConfig, schemas, siteConfig }) {
251
+ const pageMeta = pageConfig?.meta && typeof pageConfig.meta === 'object' ? pageConfig.meta : {};
252
+ const title = typeof pageMeta.title === 'string' ? pageMeta.title : slug;
253
+ const description = typeof pageMeta.description === 'string' ? pageMeta.description : '';
254
+ const pageSections = getPageSections(pageConfig, siteConfig);
255
+ const sectionTypes = Array.from(new Set(pageSections.map((section) => section?.type).filter(Boolean)));
256
+
257
+ const sectionSchemas = Object.fromEntries(
258
+ sectionTypes
259
+ .filter((sectionType) => schemas?.[sectionType] != null)
260
+ .map((sectionType) => [sectionType, zodToJsonSchema(schemas[sectionType])])
261
+ );
262
+
263
+ const sectionInstances = pageSections.map((section) => ({
264
+ id: section.id,
265
+ type: section.type,
266
+ scope: section.scope === 'global' ? 'global' : 'local',
267
+ label: inferSectionLabel(section),
268
+ }));
269
+
270
+ const tools = sectionTypes
271
+ .filter((sectionType) => sectionSchemas[sectionType] != null)
272
+ .map((sectionType) => ({
273
+ name: buildToolName(sectionType),
274
+ sectionType,
275
+ description: `Update a ${sectionType} section in OlonJS Studio and persist immediately to file.`,
276
+ inputSchema: buildMutationInputSchema(sectionType, sectionSchemas[sectionType]),
277
+ }));
278
+
279
+ return {
280
+ version: '1.0.0',
281
+ kind: 'olonjs-page-contract',
282
+ slug,
283
+ title,
284
+ description,
285
+ manifestHref: buildPageManifestHref(slug),
286
+ systemPrompt: `You are operating the "${title}" page in OlonJS Studio. Use only the declared tools and keep mutations valid against the section schema.`,
287
+ sectionTypes,
288
+ sectionInstances,
289
+ sectionSchemas,
290
+ tools,
291
+ };
292
+ }
293
+
294
+ export function buildPageManifest({ slug, pageConfig, schemas, siteConfig }) {
295
+ const contract = buildPageContract({ slug, pageConfig, schemas, siteConfig });
296
+ return {
297
+ version: '1.0.0',
298
+ kind: 'olonjs-page-mcp-manifest',
299
+ generatedAt: new Date().toISOString(),
300
+ slug,
301
+ title: contract.title,
302
+ description: contract.description,
303
+ contractHref: buildPageContractHref(slug),
304
+ transport: {
305
+ kind: 'window-message',
306
+ requestType: WEBMCP_TOOL_REQUEST_TYPE,
307
+ resultType: WEBMCP_TOOL_RESULT_TYPE,
308
+ target: 'window',
309
+ },
310
+ capabilities: {
311
+ resources: [
312
+ {
313
+ uri: `olon://pages/${slug}`,
314
+ name: `${contract.title} Data`,
315
+ mimeType: 'application/json',
316
+ description: `Structured content for the ${slug} page.`,
317
+ },
318
+ ],
319
+ },
320
+ sectionTypes: contract.sectionTypes,
321
+ sectionInstances: contract.sectionInstances,
322
+ tools: contract.tools.map(({ name, sectionType, description }) => ({
323
+ name,
324
+ sectionType,
325
+ description,
326
+ })),
327
+ };
328
+ }
329
+
330
+ export function buildSiteManifest({ pages, schemas, siteConfig }) {
331
+ const pageEntries = Object.entries(pages ?? {}).sort(([a], [b]) => a.localeCompare(b));
332
+
333
+ return {
334
+ version: '1.0.0',
335
+ kind: 'olonjs-mcp-manifest-index',
336
+ generatedAt: new Date().toISOString(),
337
+ pages: pageEntries.map(([slug, pageConfig]) => {
338
+ const pageManifest = buildPageManifest({ slug, pageConfig, schemas, siteConfig });
339
+ return {
340
+ slug,
341
+ title: pageManifest.title,
342
+ description: pageManifest.description,
343
+ manifestHref: buildPageManifestHref(slug),
344
+ contractHref: buildPageContractHref(slug),
345
+ sectionTypes: pageManifest.sectionTypes,
346
+ };
347
+ }),
348
+ };
349
+ }