@skydiveai/pi-extensions 0.1.0-beta.2304 → 0.1.0-beta.2305

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/dist/index.mjs +88 -12
  2. package/package.json +3 -1
package/dist/index.mjs CHANGED
@@ -12,6 +12,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
12
12
  import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
13
13
  import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
14
14
  import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
15
+ import { Ajv } from "ajv";
16
+ import addFormats from "ajv-formats";
15
17
  import { Check, Errors } from "typebox/value";
16
18
  import { hc } from "hono/client";
17
19
  import { ROOT_CONTEXT, SpanStatusCode, propagation, trace } from "@opentelemetry/api";
@@ -259,7 +261,7 @@ function createHealthHandler({ metadata }) {
259
261
  * read on the hot path before every LLM call), it falls back to the default
260
262
  * for that knob and logs once.
261
263
  */
262
- const log$15 = logger.child({ module: "context-management-config" });
264
+ const log$16 = logger.child({ module: "context-management-config" });
263
265
  const DEFAULT_CONTEXT_MANAGEMENT_CONFIG = {
264
266
  enabled: false,
265
267
  perResultMaxBytes: 16 * 1024,
@@ -307,7 +309,7 @@ function resolveContextManagementConfig(env = process.env) {
307
309
  maxModelCallsPerTurn: env.SKYDIVE_CTX_MAX_MODEL_CALLS
308
310
  });
309
311
  if (!parsed.success) {
310
- log$15.warn({
312
+ log$16.warn({
311
313
  event: "context_management_config_invalid",
312
314
  err: parsed.error
313
315
  }, "falling back to default context-management config");
@@ -419,7 +421,7 @@ function installIterationCap({ session, log }, configOverride = null) {
419
421
  const CAPABILITY_SOUL_NUDGE = "New capability gained — once the current task is done, if this changes what you can do for the user, record it in `soul.md` so it carries into future conversations rather than being rediscovered from scratch (then commit and push).";
420
422
  //#endregion
421
423
  //#region src/extensions/tool-call-summary.ts
422
- const log$14 = logger.child({ module: "tool-call-summary-extension" });
424
+ const log$15 = logger.child({ module: "tool-call-summary-extension" });
423
425
  /**
424
426
  * The injected parameter name: a namespaced sentinel, so it can never collide
425
427
  * with a real tool argument and is unmistakable in transcripts and logs. The
@@ -627,7 +629,7 @@ function withSummaryStrippedBeforeValidation(tool) {
627
629
  try {
628
630
  stripped = withoutInjectedSummary(args);
629
631
  } catch (err) {
630
- log$14.error({
632
+ log$15.error({
631
633
  err,
632
634
  event: "tool_call_summary_prepare_strip_failed",
633
635
  toolName: tool.name
@@ -657,7 +659,7 @@ function stripInjectedSummary(pi, toolName, input) {
657
659
  if (toolDeclaresSummaryParam(pi, toolName)) return;
658
660
  delete input[TOOL_CALL_SUMMARY_FIELD];
659
661
  } catch (err) {
660
- log$14.error({
662
+ log$15.error({
661
663
  err,
662
664
  event: "tool_call_summary_strip_failed",
663
665
  toolName
@@ -673,7 +675,7 @@ function buildInjectedPayload(pi, payload) {
673
675
  try {
674
676
  return injectToolCallSummary(payload, getStrictToolNames(pi));
675
677
  } catch (err) {
676
- log$14.error({
678
+ log$15.error({
677
679
  err,
678
680
  event: "tool_call_summary_injection_failed"
679
681
  }, "tool_call_summary injection failed; passing payload through unchanged");
@@ -710,7 +712,7 @@ const toolCallSummaryExtension = (pi) => {
710
712
  * or `ToolDefinition[]`. Files starting with `_` or `.` are skipped, so
711
713
  * `tools/_example.ts` documents the shape without registering.
712
714
  */
713
- const log$13 = logger.child({ module: "local-tools-extension" });
715
+ const log$14 = logger.child({ module: "local-tools-extension" });
714
716
  const TOOLS_DIRNAME = "tools";
715
717
  const fileState = /* @__PURE__ */ new Map();
716
718
  let pendingLocalToolsUpdate = null;
@@ -854,7 +856,7 @@ async function reconcileAndQueue({ pi, dir, reason }) {
854
856
  dir
855
857
  });
856
858
  if (reason !== "session_start" && summaryHasChanges$1(summary)) pendingLocalToolsUpdate = summary;
857
- log$13.info({
859
+ log$14.info({
858
860
  event: "local_tools_reconcile",
859
861
  reason,
860
862
  total_tools: summary.totalTools,
@@ -876,7 +878,7 @@ const localToolsExtension = (pi) => {
876
878
  reason: "session_start"
877
879
  });
878
880
  } catch (err) {
879
- log$13.error({
881
+ log$14.error({
880
882
  err,
881
883
  event: "local_tools_reconcile_failed"
882
884
  }, "local tools reconcile failed");
@@ -888,7 +890,7 @@ const localToolsExtension = (pi) => {
888
890
  try {
889
891
  current = await listToolFiles(dir);
890
892
  } catch (err) {
891
- log$13.warn({
893
+ log$14.warn({
892
894
  err,
893
895
  event: "local_tools_listing_failed"
894
896
  }, "tools/ listing failed");
@@ -910,7 +912,7 @@ const localToolsExtension = (pi) => {
910
912
  reason: "auto_reload"
911
913
  });
912
914
  } catch (err) {
913
- log$13.error({
915
+ log$14.error({
914
916
  err,
915
917
  event: "local_tools_auto_reload_failed"
916
918
  }, "auto-reload after tools/ change failed");
@@ -941,6 +943,77 @@ function createStderrBuffer({ maxBytes }) {
941
943
  };
942
944
  }
943
945
  //#endregion
946
+ //#region src/extensions/mcp/schema-validator.ts
947
+ /**
948
+ * Lenient JSON-schema validator for MCP tool output schemas.
949
+ *
950
+ * The MCP SDK's `Client.listTools()` eagerly compiles every tool's
951
+ * `outputSchema` (`cacheToolMetadata`), and its default AJV instance
952
+ * compiles `pattern` with the `u` (unicode) flag. Real vendor servers
953
+ * ship legacy Annex-B escapes in their patterns — PayPal's hosted MCP
954
+ * publishes `^https\:\/\/`, whose `\:` is a hard SyntaxError under `/u`
955
+ * ("Invalid regular expression: /^https\:\/\//u: Invalid escape").
956
+ * With the default validator that one schema throws inside `listTools`,
957
+ * the whole server lands in `failed`, and zero tools bind even though
958
+ * the credential is live.
959
+ *
960
+ * Two layers of leniency, both scoped to *output* validation (tool
961
+ * results), never to anything security-relevant:
962
+ *
963
+ * 1. `unicodeRegExp: false` — compile `pattern` without the `u` flag,
964
+ * which restores Annex-B tolerance for escapes like `\:` and matches
965
+ * what most other MCP clients (and JSON Schema's own spec, which
966
+ * does not require unicode mode) accept.
967
+ * 2. A compile failure on one tool's schema must not kill the server:
968
+ * `getValidator` catches the throw and degrades to a pass-through
969
+ * validator for that tool only, so its results are simply not
970
+ * output-validated while every other tool stays fully functional.
971
+ */
972
+ const log$13 = logger.child({ module: "mcp-schema-validator" });
973
+ function createLenientAjv() {
974
+ const ajv = new Ajv({
975
+ strict: false,
976
+ validateFormats: true,
977
+ validateSchema: false,
978
+ allErrors: true,
979
+ unicodeRegExp: false
980
+ });
981
+ addFormats(ajv);
982
+ return ajv;
983
+ }
984
+ var LenientJsonSchemaValidator = class {
985
+ ajv = createLenientAjv();
986
+ getValidator(schema) {
987
+ let compiled = null;
988
+ try {
989
+ compiled = this.ajv.compile(schema);
990
+ } catch (err) {
991
+ log$13.warn({
992
+ err,
993
+ event: "mcp_output_schema_compile_failed"
994
+ }, "tool outputSchema failed to compile; skipping output validation for this tool");
995
+ return (input) => ({
996
+ valid: true,
997
+ data: input,
998
+ errorMessage: void 0
999
+ });
1000
+ }
1001
+ const validate = compiled;
1002
+ return (input) => {
1003
+ if (validate(input)) return {
1004
+ valid: true,
1005
+ data: input,
1006
+ errorMessage: void 0
1007
+ };
1008
+ return {
1009
+ valid: false,
1010
+ data: void 0,
1011
+ errorMessage: this.ajv.errorsText(validate.errors)
1012
+ };
1013
+ };
1014
+ }
1015
+ };
1016
+ //#endregion
944
1017
  //#region src/extensions/mcp/connect-client.ts
945
1018
  /**
946
1019
  * Spawns an MCP server's transport and races the initialize handshake
@@ -1091,7 +1164,10 @@ async function connectClient(id, config, opts = {}) {
1091
1164
  const client = new Client({
1092
1165
  name: "skydive-harness",
1093
1166
  version: "0.1.0"
1094
- }, { capabilities: {} });
1167
+ }, {
1168
+ capabilities: {},
1169
+ jsonSchemaValidator: new LenientJsonSchemaValidator()
1170
+ });
1095
1171
  if (config.transport === "http") return connectHttp(id, config, client);
1096
1172
  const params = {
1097
1173
  command: config.command,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skydiveai/pi-extensions",
3
- "version": "0.1.0-beta.2304",
3
+ "version": "0.1.0-beta.2305",
4
4
  "homepage": "https://skydive.com",
5
5
  "license": "MIT",
6
6
  "author": "Create, Inc.",
@@ -36,6 +36,8 @@
36
36
  "@opentelemetry/sdk-trace-node": "^1.30.1",
37
37
  "@opentelemetry/semantic-conventions": "^1.28.0",
38
38
  "@skydiveai/pi-server": "^0.1.0",
39
+ "ajv": "^8.17.1",
40
+ "ajv-formats": "^3.0.1",
39
41
  "hono": "^4.6.14",
40
42
  "pino": "^9.6.0",
41
43
  "shell-quote": "^1.8.4",