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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/schema-convert.ts +164 -48
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-extension",
3
- "version": "1.11.1",
3
+ "version": "1.11.2",
4
4
  "description": "MCP (Model Context Protocol) client extension for the Pi coding agent — connect Pi to any MCP server",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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
- /** Resolve an `allOf` sub-schema's `$ref` (unless it is already being resolved). */
351
- function resolveAllOfSubSchema(sub: Record<string, unknown>, ctx: ConversionContext): Record<string, unknown> {
352
- const ref = sub["$ref"];
353
- if (typeof ref !== "string" || ctx.seenRefs.has(ref)) {
354
- return sub;
355
- }
356
- return resolveRefTarget(ref, ctx.rootDefs, ctx.effectiveRoot) ?? sub;
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(props: unknown, rawProperties: Record<string, unknown>): void {
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 rawProperties)) {
366
- rawProperties[key] = value;
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
- rawProperties: Record<string, unknown>,
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
- const resolvedSub = resolveAllOfSubSchema(sub, ctx);
393
- mergeSubSchemaProperties(resolvedSub["properties"], rawProperties);
394
- mergeSubSchemaRequired(resolvedSub["required"], required);
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 rawProperties: Record<string, unknown> = isPlainObject(schema["properties"])
403
- ? { ...schema["properties"] }
404
- : {};
405
- const required = new Set<string>(
406
- Array.isArray(schema["required"])
407
- ? (schema["required"] as unknown[]).filter((r): r is string => typeof r === "string")
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, rawProperties, required, ctx);
530
+ mergeAllOfSubSchema(sub, merge, ctx);
424
531
  }
425
532
  }
426
533
 
427
534
  const props: Record<string, TSchema> = {};
428
- for (const [key, value] of Object.entries(rawProperties)) {
535
+ for (const [key, entry] of Object.entries(merge.properties)) {
429
536
  const converted = convertJsonSchemaToTypebox(
430
- value as Record<string, unknown>,
537
+ entry.schema,
431
538
  ctx.rootDefs,
432
539
  ctx.effectiveRoot,
433
- ctx.seenRefs,
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 rawType = schema["type"];
579
- const type =
580
- typeof rawType === "string"
581
- ? rawType
582
- : hasObjectSchemaKeywords(schema)
583
- ? "object"
584
- : hasArraySchemaKeywords(schema)
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
  }