@zackbart/connecta 0.10.2 → 0.10.3

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/src/invocation.ts CHANGED
@@ -20,6 +20,7 @@ import { unwrapMcpResult } from "./mcp-result.js";
20
20
  import type { RegistryView } from "./registry.js";
21
21
  import { isExplicitlyReadOnly } from "./tool-safety.js";
22
22
  import type { ToolDef } from "./types.js";
23
+ import { validateToolInput } from "./validate.js";
23
24
 
24
25
  /**
25
26
  * The longest the engine will park a synchronous inbound request in *waiting
@@ -299,6 +300,25 @@ export class InvocationService {
299
300
  `Retry ${target.connector.id}.${target.toolName} after ` +
300
301
  "the operator completes recovery.",
301
302
  }
303
+ : error.code === "invalid_args" && error.validation && target
304
+ ? {
305
+ ...error,
306
+ connector: target.connector.id,
307
+ operation: `${target.connector.id}.${target.toolName}`,
308
+ nextAction: {
309
+ tool: "search_tools" as const,
310
+ arguments: {
311
+ query: target.toolName,
312
+ connector: target.connector.id,
313
+ includeSchemas: "compact" as const,
314
+ },
315
+ purpose:
316
+ "Inspect the current input shape if the validation findings are not sufficient.",
317
+ },
318
+ retry:
319
+ `Correct the listed arguments and retry ` +
320
+ `${target.connector.id}.${target.toolName}.`,
321
+ }
302
322
  : error;
303
323
  record(
304
324
  details.code === "timeout"
@@ -362,6 +382,30 @@ export class InvocationService {
362
382
  );
363
383
  }
364
384
 
385
+ // Remote MCP tools advertise their input schema in the catalog. Validate
386
+ // against that same request-local definition before admission or provider
387
+ // dispatch, so a predictable mismatch stays structured instead of being
388
+ // flattened into provider-specific error prose. Unsupported schemas retain
389
+ // validateToolInput's fail-open behavior and reach the downstream normally.
390
+ if (
391
+ resolved.connector.kind === "mcp" &&
392
+ resolved.definition.inputSchema
393
+ ) {
394
+ const invalid = validateToolInput(
395
+ resolved.definition.inputSchema,
396
+ args ?? {},
397
+ {
398
+ address: `${resolved.connector.id}.${resolved.toolName}`,
399
+ logger: this.registry.contextFor(
400
+ resolved.connector.id,
401
+ this.catalog.baseUrl,
402
+ this.catalog.requestScope,
403
+ ).logger,
404
+ },
405
+ );
406
+ if (invalid) return failed(classifyCallError(invalid));
407
+ }
408
+
365
409
  try {
366
410
  context.beforeDispatch?.();
367
411
  } catch (error) {
package/src/meta-tools.ts CHANGED
@@ -1114,6 +1114,7 @@ export function createMetaTools(
1114
1114
  if (!outcome.ok) {
1115
1115
  const failedResult =
1116
1116
  outcome.error.code === "auth_required" ||
1117
+ outcome.error.code === "invalid_args" ||
1117
1118
  outcome.error.code === "input_required_unsupported" ||
1118
1119
  call.resultMode === "value"
1119
1120
  ? jsonResult({
@@ -1126,6 +1127,7 @@ export function createMetaTools(
1126
1127
  : errorResult(outcome.error.message);
1127
1128
  if (
1128
1129
  outcome.error.code === "auth_required" ||
1130
+ outcome.error.code === "invalid_args" ||
1129
1131
  outcome.error.code === "input_required_unsupported"
1130
1132
  ) {
1131
1133
  failedResult.isError = true;
package/src/validate.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Validator } from "@cfworker/json-schema";
2
2
  import { ConnectorCallError } from "./errors.js";
3
+ import type {
4
+ ArgumentValidationDetails,
5
+ ArgumentValidationIssue,
6
+ } from "./errors.js";
7
+ import { MAX_ARGUMENT_VALIDATION_ISSUES } from "./errors.js";
3
8
  import type { JsonSchema, Logger } from "./types.js";
4
9
 
5
10
  export interface ValidateToolInputOptions {
@@ -42,6 +47,128 @@ export interface PrecompileValidatorOptions {
42
47
  // breaking a working tool). A WeakMap so schemas belonging to a discarded
43
48
  // connector are collectable, the same pattern compactSchema uses.
44
49
  const validators = new WeakMap<JsonSchema, Validator | null>();
50
+ const REQUIRED_PROPERTY_RE =
51
+ /^Instance does not have required property "([^"]+)"\.$/;
52
+
53
+ interface ValidationUnit {
54
+ keyword: string;
55
+ keywordLocation: string;
56
+ instanceLocation: string;
57
+ error: string;
58
+ }
59
+
60
+ function decodePointerPart(value: string): string {
61
+ return value.replaceAll("~1", "/").replaceAll("~0", "~");
62
+ }
63
+
64
+ function encodePointerPart(value: string): string {
65
+ return value.replaceAll("~", "~0").replaceAll("/", "~1");
66
+ }
67
+
68
+ function pointerValue(value: unknown, pointer: string): unknown {
69
+ if (pointer === "#") return value;
70
+ if (!pointer.startsWith("#/")) return undefined;
71
+ let current = value;
72
+ for (const part of pointer.slice(2).split("/").map(decodePointerPart)) {
73
+ if (current === null || typeof current !== "object") return undefined;
74
+ current = (current as Record<string, unknown>)[part];
75
+ }
76
+ return current;
77
+ }
78
+
79
+ function argumentPath(location: string): string {
80
+ if (location === "#") return "/";
81
+ return location.startsWith("#") ? location.slice(1) || "/" : "/";
82
+ }
83
+
84
+ function expectedType(schema: JsonSchema, unit: ValidationUnit): string | undefined {
85
+ if (unit.keyword === "type") {
86
+ const value = pointerValue(schema, unit.keywordLocation);
87
+ if (typeof value === "string") return value;
88
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
89
+ return value.join(" | ");
90
+ }
91
+ }
92
+ if (unit.keyword === "required") {
93
+ const missing = REQUIRED_PROPERTY_RE.exec(unit.error)?.[1];
94
+ if (!missing) return undefined;
95
+ const parentLocation = unit.keywordLocation.replace(/\/required$/, "");
96
+ const value = pointerValue(
97
+ schema,
98
+ `${parentLocation}/properties/${encodePointerPart(missing)}/type`,
99
+ );
100
+ if (typeof value === "string") return value;
101
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
102
+ return value.join(" | ");
103
+ }
104
+ return "present";
105
+ }
106
+ const fixed: Record<string, string> = {
107
+ additionalProperties: "no additional properties",
108
+ enum: "one of the declared values",
109
+ const: "the declared constant",
110
+ minLength: "the declared minimum length",
111
+ maxLength: "the declared maximum length",
112
+ minimum: "the declared minimum",
113
+ maximum: "the declared maximum",
114
+ pattern: "the declared string pattern",
115
+ };
116
+ return fixed[unit.keyword];
117
+ }
118
+
119
+ function validationDetails(
120
+ schema: JsonSchema,
121
+ units: ValidationUnit[],
122
+ ): ArgumentValidationDetails {
123
+ const leafUnits = units.filter(
124
+ (unit) =>
125
+ ![
126
+ "properties",
127
+ "items",
128
+ "allOf",
129
+ "anyOf",
130
+ "oneOf",
131
+ "if",
132
+ "not",
133
+ "patternProperties",
134
+ "additionalProperties",
135
+ ].includes(unit.keyword),
136
+ );
137
+ const issues: ArgumentValidationIssue[] = [];
138
+ for (const unit of leafUnits) {
139
+ const missing =
140
+ unit.keyword === "required"
141
+ ? REQUIRED_PROPERTY_RE.exec(unit.error)?.[1]
142
+ : undefined;
143
+ const path =
144
+ missing !== undefined
145
+ ? `${argumentPath(unit.instanceLocation).replace(/\/$/, "")}/${encodePointerPart(missing)}`
146
+ : argumentPath(unit.instanceLocation);
147
+ const code = unit.keyword === "false" ? "additionalProperties" : unit.keyword;
148
+ const expected =
149
+ expectedType(schema, unit) ??
150
+ (code === "additionalProperties"
151
+ ? "no additional properties"
152
+ : "the declared schema constraint");
153
+ const issue = { path, code, expected };
154
+ if (
155
+ !issues.some(
156
+ (existing) =>
157
+ existing.path === issue.path &&
158
+ existing.code === issue.code &&
159
+ existing.expected === issue.expected,
160
+ )
161
+ ) {
162
+ issues.push(issue);
163
+ }
164
+ }
165
+ return {
166
+ issues: issues.slice(0, MAX_ARGUMENT_VALIDATION_ISSUES),
167
+ ...(issues.length > MAX_ARGUMENT_VALIDATION_ISSUES
168
+ ? { truncated: true as const }
169
+ : {}),
170
+ };
171
+ }
45
172
 
46
173
  function unevaluableSchema(address: string): ConnectorCallError {
47
174
  return new ConnectorCallError(
@@ -127,6 +254,7 @@ export function validateToolInput(
127
254
  return new ConnectorCallError(
128
255
  "invalid_args",
129
256
  `Invalid arguments for "${opts.address}": ${detail || "input does not match the tool's inputSchema"}`,
257
+ { validation: validationDetails(schema, result.errors) },
130
258
  );
131
259
  }
132
260
  return null;
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.10.2";
7
+ export const CONNECTA_VERSION = "0.10.3";
@@ -12,7 +12,7 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@zackbart/connecta": "0.10.2",
15
+ "@zackbart/connecta": "0.10.3",
16
16
  "quickjs-emscripten": "0.32.0"
17
17
  },
18
18
  "devDependencies": {