@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/CHANGELOG.md +19 -0
- package/dist/catalog-service.d.ts +1 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +4 -1
- package/dist/catalog-service.js.map +1 -1
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +40 -0
- package/dist/errors.js.map +1 -1
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +33 -1
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +2 -0
- package/dist/meta-tools.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +100 -1
- package/dist/validate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-mode.md +7 -8
- package/documentation/connectors.md +6 -0
- package/documentation/meta-tools.md +18 -0
- package/package.json +1 -1
- package/src/catalog-service.ts +4 -1
- package/src/errors.ts +88 -1
- package/src/invocation.ts +44 -0
- package/src/meta-tools.ts +2 -0
- package/src/validate.ts +128 -0
- package/src/version.ts +1 -1
- package/templates/node/package.json +1 -1
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