pi-mcp-extension 1.11.1 → 1.11.2
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 +1 -1
- package/src/schema-convert.ts +164 -48
package/package.json
CHANGED
package/src/schema-convert.ts
CHANGED
|
@@ -111,6 +111,67 @@ function hasArraySchemaKeywords(schema: Record<string, unknown>): boolean {
|
|
|
111
111
|
return schema["items"] !== undefined || Array.isArray(schema["prefixItems"]);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** Whether `ref` is already on the active resolution path. */
|
|
115
|
+
function isRefSeenOnPath(
|
|
116
|
+
ref: string,
|
|
117
|
+
ctx: ConversionContext,
|
|
118
|
+
extraRefs: ReadonlySet<string>,
|
|
119
|
+
): boolean {
|
|
120
|
+
return ctx.seenRefs.has(ref) || extraRefs.has(ref);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether a `$ref` on an `allOf` member resolves to an object-like schema. */
|
|
124
|
+
function isRefToObjectSchema(
|
|
125
|
+
ref: unknown,
|
|
126
|
+
ctx: ConversionContext,
|
|
127
|
+
seenRefs: Set<string>,
|
|
128
|
+
): boolean {
|
|
129
|
+
if (typeof ref !== "string" || isRefSeenOnPath(ref, ctx, seenRefs)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
seenRefs.add(ref);
|
|
133
|
+
const resolved = resolveRefTarget(ref, ctx.rootDefs, ctx.effectiveRoot);
|
|
134
|
+
return resolved ? isObjectLikeSubSchema(resolved, ctx, seenRefs) : false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Whether a sub-schema describes an object directly, through a `$ref`, or through a nested `allOf`. */
|
|
138
|
+
function isObjectLikeSubSchema(
|
|
139
|
+
sub: Record<string, unknown>,
|
|
140
|
+
ctx: ConversionContext,
|
|
141
|
+
seenRefs: Set<string>,
|
|
142
|
+
): boolean {
|
|
143
|
+
return (
|
|
144
|
+
sub["type"] === "object" ||
|
|
145
|
+
hasObjectSchemaKeywords(sub) ||
|
|
146
|
+
hasAllOfObjectSubSchema(sub, ctx, seenRefs) ||
|
|
147
|
+
isRefToObjectSchema(sub["$ref"], ctx, seenRefs)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Whether `allOf` composes object sub-schemas / `$ref`s to object definitions.
|
|
153
|
+
* WHY: JSON Schema generators (Pydantic, FastAPI, OpenAPI 3.0) omit `type: "object"`
|
|
154
|
+
* on the `allOf` wrapper. Without object inference the schema becomes a bare
|
|
155
|
+
* `Type.Intersect`, which has no `properties` — Pi's `normalizeOptionalNulls` then
|
|
156
|
+
* skips it (coercing optional `null`s into `"null"` / `0` / `false`) and Anthropic's
|
|
157
|
+
* adapter sends an empty `input_schema`.
|
|
158
|
+
*/
|
|
159
|
+
function hasAllOfObjectSubSchema(
|
|
160
|
+
schema: Record<string, unknown>,
|
|
161
|
+
ctx: ConversionContext,
|
|
162
|
+
seenRefs: Set<string> = new Set<string>(),
|
|
163
|
+
): boolean {
|
|
164
|
+
const allOf = schema["allOf"];
|
|
165
|
+
if (!Array.isArray(allOf)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return allOf.some((sub) => isPlainObject(sub) && isObjectLikeSubSchema(sub, ctx, seenRefs));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function hasObjectSchemaShape(schema: Record<string, unknown>, ctx: ConversionContext): boolean {
|
|
172
|
+
return hasObjectSchemaKeywords(schema) || hasAllOfObjectSubSchema(schema, ctx);
|
|
173
|
+
}
|
|
174
|
+
|
|
114
175
|
interface ConversionContext {
|
|
115
176
|
rootDefs: Record<string, Record<string, unknown>>;
|
|
116
177
|
effectiveRoot: Record<string, unknown>;
|
|
@@ -347,23 +408,34 @@ function convertArraySchema(
|
|
|
347
408
|
);
|
|
348
409
|
}
|
|
349
410
|
|
|
350
|
-
/**
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
411
|
+
/** A property schema paired with the `$ref` path that was dereferenced to reach it. */
|
|
412
|
+
interface MergedProperty {
|
|
413
|
+
schema: Record<string, unknown>;
|
|
414
|
+
seenRefs: ReadonlySet<string>;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Accumulates flattened `allOf` contributions plus the refs already walked. */
|
|
418
|
+
interface AllOfMerge {
|
|
419
|
+
properties: Record<string, MergedProperty>;
|
|
420
|
+
required: Set<string>;
|
|
421
|
+
visitedRefs: Set<string>;
|
|
357
422
|
}
|
|
358
423
|
|
|
359
424
|
/** Merge a sub-schema's properties without overwriting properties declared by the caller. */
|
|
360
|
-
function mergeSubSchemaProperties(
|
|
425
|
+
function mergeSubSchemaProperties(
|
|
426
|
+
props: unknown,
|
|
427
|
+
mergedProperties: Record<string, MergedProperty>,
|
|
428
|
+
seenRefs: ReadonlySet<string>,
|
|
429
|
+
): void {
|
|
361
430
|
if (!isPlainObject(props)) {
|
|
362
431
|
return;
|
|
363
432
|
}
|
|
364
433
|
for (const [key, value] of Object.entries(props)) {
|
|
365
|
-
if (!(key in
|
|
366
|
-
|
|
434
|
+
if (!(key in mergedProperties)) {
|
|
435
|
+
mergedProperties[key] = {
|
|
436
|
+
schema: value as Record<string, unknown>,
|
|
437
|
+
seenRefs,
|
|
438
|
+
};
|
|
367
439
|
}
|
|
368
440
|
}
|
|
369
441
|
}
|
|
@@ -380,18 +452,51 @@ function mergeSubSchemaRequired(req: unknown, required: Set<string>): void {
|
|
|
380
452
|
}
|
|
381
453
|
}
|
|
382
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Follow an `allOf` member's `$ref` if not yet visited, recording `ref` on the active
|
|
457
|
+
* resolution path for any properties inherited from the referenced definition.
|
|
458
|
+
* WHY: generators chain definitions (`A: { $ref: B }`) and emit recursive fields wrapped
|
|
459
|
+
* in `allOf: [{ $ref: "#/$defs/Node" }]`; carrying `nextActiveRefs` onto inherited
|
|
460
|
+
* properties ensures `convertJsonSchemaToTypebox` breaks `$ref` cycles on inlined fields.
|
|
461
|
+
*/
|
|
462
|
+
function mergeAllOfRef(
|
|
463
|
+
ref: unknown,
|
|
464
|
+
merge: AllOfMerge,
|
|
465
|
+
ctx: ConversionContext,
|
|
466
|
+
activeRefs: ReadonlySet<string>,
|
|
467
|
+
): void {
|
|
468
|
+
if (typeof ref !== "string" || isRefSeenOnPath(ref, ctx, merge.visitedRefs)) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
merge.visitedRefs.add(ref);
|
|
472
|
+
const resolved = resolveRefTarget(ref, ctx.rootDefs, ctx.effectiveRoot);
|
|
473
|
+
if (!resolved) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const nextActiveRefs = new Set(activeRefs);
|
|
477
|
+
nextActiveRefs.add(ref);
|
|
478
|
+
mergeAllOfSubSchema(resolved, merge, ctx, nextActiveRefs);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Merge one `allOf` member (and every `$ref` or nested `allOf` beneath it) into the accumulator. */
|
|
383
482
|
function mergeAllOfSubSchema(
|
|
384
483
|
sub: unknown,
|
|
385
|
-
|
|
386
|
-
required: Set<string>,
|
|
484
|
+
merge: AllOfMerge,
|
|
387
485
|
ctx: ConversionContext,
|
|
486
|
+
activeRefs: ReadonlySet<string> = ctx.seenRefs,
|
|
388
487
|
): void {
|
|
389
488
|
if (!isPlainObject(sub)) {
|
|
390
489
|
return;
|
|
391
490
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
491
|
+
mergeSubSchemaProperties(sub["properties"], merge.properties, activeRefs);
|
|
492
|
+
mergeSubSchemaRequired(sub["required"], merge.required);
|
|
493
|
+
mergeAllOfRef(sub["$ref"], merge, ctx, activeRefs);
|
|
494
|
+
const nested = sub["allOf"];
|
|
495
|
+
if (Array.isArray(nested)) {
|
|
496
|
+
for (const nestedSub of nested) {
|
|
497
|
+
mergeAllOfSubSchema(nestedSub, merge, ctx, activeRefs);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
395
500
|
}
|
|
396
501
|
|
|
397
502
|
function convertObjectSchema(
|
|
@@ -399,14 +504,16 @@ function convertObjectSchema(
|
|
|
399
504
|
opts: Record<string, unknown>,
|
|
400
505
|
ctx: ConversionContext,
|
|
401
506
|
): TSchema {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
:
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
507
|
+
const merge: AllOfMerge = {
|
|
508
|
+
properties: {},
|
|
509
|
+
required: new Set<string>(
|
|
510
|
+
Array.isArray(schema["required"])
|
|
511
|
+
? (schema["required"] as unknown[]).filter((r): r is string => typeof r === "string")
|
|
512
|
+
: [],
|
|
513
|
+
),
|
|
514
|
+
visitedRefs: new Set<string>(),
|
|
515
|
+
};
|
|
516
|
+
mergeSubSchemaProperties(schema["properties"], merge.properties, ctx.seenRefs);
|
|
410
517
|
const additionalProperties = schema["additionalProperties"];
|
|
411
518
|
|
|
412
519
|
let allOfSchemas: TSchema[] | undefined;
|
|
@@ -420,19 +527,19 @@ function convertObjectSchema(
|
|
|
420
527
|
),
|
|
421
528
|
);
|
|
422
529
|
for (const sub of schema["allOf"] as unknown[]) {
|
|
423
|
-
mergeAllOfSubSchema(sub,
|
|
530
|
+
mergeAllOfSubSchema(sub, merge, ctx);
|
|
424
531
|
}
|
|
425
532
|
}
|
|
426
533
|
|
|
427
534
|
const props: Record<string, TSchema> = {};
|
|
428
|
-
for (const [key,
|
|
535
|
+
for (const [key, entry] of Object.entries(merge.properties)) {
|
|
429
536
|
const converted = convertJsonSchemaToTypebox(
|
|
430
|
-
|
|
537
|
+
entry.schema,
|
|
431
538
|
ctx.rootDefs,
|
|
432
539
|
ctx.effectiveRoot,
|
|
433
|
-
|
|
540
|
+
entry.seenRefs,
|
|
434
541
|
);
|
|
435
|
-
props[key] = required.has(key) ? converted : Type.Optional(converted);
|
|
542
|
+
props[key] = merge.required.has(key) ? converted : Type.Optional(converted);
|
|
436
543
|
}
|
|
437
544
|
|
|
438
545
|
const objOpts: Record<string, unknown> = { ...opts };
|
|
@@ -447,8 +554,8 @@ function convertObjectSchema(
|
|
|
447
554
|
);
|
|
448
555
|
}
|
|
449
556
|
|
|
450
|
-
if (required.size > 0) {
|
|
451
|
-
objOpts["required"] = Array.from(required);
|
|
557
|
+
if (merge.required.size > 0) {
|
|
558
|
+
objOpts["required"] = Array.from(merge.required);
|
|
452
559
|
}
|
|
453
560
|
|
|
454
561
|
if (Array.isArray(schema["oneOf"])) {
|
|
@@ -511,6 +618,23 @@ function convertTypedSchema(
|
|
|
511
618
|
return Type.Any(opts);
|
|
512
619
|
}
|
|
513
620
|
|
|
621
|
+
function inferSchemaType(
|
|
622
|
+
schema: Record<string, unknown>,
|
|
623
|
+
ctx: ConversionContext,
|
|
624
|
+
): string | undefined {
|
|
625
|
+
const rawType = schema["type"];
|
|
626
|
+
if (typeof rawType === "string") {
|
|
627
|
+
return rawType;
|
|
628
|
+
}
|
|
629
|
+
if (hasObjectSchemaShape(schema, ctx)) {
|
|
630
|
+
return "object";
|
|
631
|
+
}
|
|
632
|
+
if (hasArraySchemaKeywords(schema)) {
|
|
633
|
+
return "array";
|
|
634
|
+
}
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
514
638
|
function resolveNonTypedSchema(
|
|
515
639
|
schema: Record<string, unknown>,
|
|
516
640
|
opts: Record<string, unknown>,
|
|
@@ -524,14 +648,6 @@ function resolveNonTypedSchema(
|
|
|
524
648
|
if (Array.isArray(rawType)) {
|
|
525
649
|
return convertMultiTypeSchema(schema, rawType, opts, ctx);
|
|
526
650
|
}
|
|
527
|
-
if (
|
|
528
|
-
!hasObjectSchemaKeywords(schema) &&
|
|
529
|
-
!hasArraySchemaKeywords(schema) &&
|
|
530
|
-
rawType !== "object" &&
|
|
531
|
-
rawType !== "array"
|
|
532
|
-
) {
|
|
533
|
-
return convertCombinatorSchema(schema, rawType, opts, ctx) ?? null;
|
|
534
|
-
}
|
|
535
651
|
return null;
|
|
536
652
|
}
|
|
537
653
|
|
|
@@ -544,7 +660,9 @@ function resolveNonTypedSchema(
|
|
|
544
660
|
* - Nullable and multi-type arrays ("type": ["string", "null"]) with Type.Null() ordered first
|
|
545
661
|
* - Cycle-safe $ref (#, #/$defs/, #/definitions/, and arbitrary RFC 6901 JSON Pointers)
|
|
546
662
|
* - oneOf / anyOf → TypeBox Union
|
|
547
|
-
* - allOf → TypeBox Intersect
|
|
663
|
+
* - allOf → TypeBox Intersect; when it composes object sub-schemas or `$ref`s to
|
|
664
|
+
* object definitions, an object schema with merged `properties` / `required`
|
|
665
|
+
* (recursing through nested `allOf` and chained `$ref`s) that keeps the members
|
|
548
666
|
* Falls back to Type.Any() for unresolvable $ref, recursive cycles, or missing type.
|
|
549
667
|
*/
|
|
550
668
|
export function convertJsonSchemaToTypebox(
|
|
@@ -575,15 +693,13 @@ export function convertJsonSchemaToTypebox(
|
|
|
575
693
|
return nonTyped;
|
|
576
694
|
}
|
|
577
695
|
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
? "array"
|
|
586
|
-
: undefined;
|
|
696
|
+
const type = inferSchemaType(schema, ctx);
|
|
697
|
+
if (type !== "object" && type !== "array") {
|
|
698
|
+
const combinator = convertCombinatorSchema(schema, type, opts, ctx);
|
|
699
|
+
if (combinator) {
|
|
700
|
+
return combinator;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
587
703
|
|
|
588
704
|
return convertTypedSchema(type, schema, opts, ctx);
|
|
589
705
|
}
|