@zackbart/connecta 0.2.1 → 0.4.0
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 +98 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +62 -0
- package/dist/catalog.js.map +1 -1
- package/dist/connectors/api.d.ts +14 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +18 -1
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +22 -8
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +4 -1
- package/dist/credentials.js.map +1 -1
- package/dist/errors.d.ts +53 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +89 -0
- package/dist/errors.js.map +1 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +10 -6
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/json-schema.d.ts +3 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +6 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/meta-tools.d.ts +31 -2
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +75 -31
- package/dist/meta-tools.js.map +1 -1
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -1
- package/dist/server.js.map +1 -1
- package/dist/storage/file.d.ts +6 -2
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +3 -2
- package/dist/storage/file.js.map +1 -1
- package/dist/validate.d.ts +39 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +66 -0
- package/dist/validate.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -1
- package/src/catalog.ts +61 -0
- package/src/connectors/api.ts +25 -1
- package/src/connectors/remote-mcp.ts +32 -13
- package/src/credentials.ts +4 -1
- package/src/errors.ts +126 -0
- package/src/execute.ts +13 -6
- package/src/index.ts +24 -0
- package/src/json-schema.ts +11 -0
- package/src/meta-tools.ts +102 -46
- package/src/node.ts +1 -0
- package/src/server.ts +7 -1
- package/src/storage/file.ts +12 -3
- package/src/validate.ts +96 -0
- package/src/version.ts +1 -1
package/src/catalog.ts
CHANGED
|
@@ -79,6 +79,41 @@ function refName(ref: string): string {
|
|
|
79
79
|
return ref.split("/").pop() ?? ref;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Whether a schema declares anything renderSchema knows how to render on its
|
|
84
|
+
* own. Used to decide if the non-allOf half of a schema is worth rendering:
|
|
85
|
+
* without this, a plain `{ allOf: [...] }` would render its (empty) local half
|
|
86
|
+
* through the raw-JSON fallback and emit `{} & …`.
|
|
87
|
+
*/
|
|
88
|
+
function declaresShape(s: Record<string, unknown>): boolean {
|
|
89
|
+
return (
|
|
90
|
+
typeof s.$ref === "string" ||
|
|
91
|
+
Array.isArray(s.oneOf) ||
|
|
92
|
+
Array.isArray(s.anyOf) ||
|
|
93
|
+
Array.isArray(s.enum) ||
|
|
94
|
+
s.const !== undefined ||
|
|
95
|
+
s.items !== undefined ||
|
|
96
|
+
s.properties !== undefined ||
|
|
97
|
+
s.type !== undefined
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parenthesize a top-level union so it doesn't read as part of a surrounding
|
|
103
|
+
* `&`. Only separators outside braces count, so a nested union or a property
|
|
104
|
+
* description containing a pipe doesn't trigger stray parentheses.
|
|
105
|
+
*/
|
|
106
|
+
function grouped(part: string): string {
|
|
107
|
+
let nesting = 0;
|
|
108
|
+
for (let i = 0; i < part.length; i += 1) {
|
|
109
|
+
const char = part[i];
|
|
110
|
+
if (char === "{" || char === "(" || char === "[") nesting += 1;
|
|
111
|
+
else if (char === "}" || char === ")" || char === "]") nesting -= 1;
|
|
112
|
+
else if (nesting === 0 && part.startsWith(" | ", i)) return `(${part})`;
|
|
113
|
+
}
|
|
114
|
+
return part;
|
|
115
|
+
}
|
|
116
|
+
|
|
82
117
|
function renderSchema(
|
|
83
118
|
schema: unknown,
|
|
84
119
|
defs: Record<string, unknown>,
|
|
@@ -91,6 +126,27 @@ function renderSchema(
|
|
|
91
126
|
}
|
|
92
127
|
const s = schema as Record<string, unknown>;
|
|
93
128
|
|
|
129
|
+
// allOf composes rather than replaces: it is checked before every other
|
|
130
|
+
// keyword, and renders the schema's own shape alongside its members instead
|
|
131
|
+
// of returning early. A schema carrying both allOf and properties (the usual
|
|
132
|
+
// OpenAPI-derived "extend this base" shape, and equally legal with $ref,
|
|
133
|
+
// enum, const, or items) would otherwise silently drop whichever half lost
|
|
134
|
+
// the branch race. The schema's own shape comes first, being the more
|
|
135
|
+
// specific half, and is rendered at the current depth because its members
|
|
136
|
+
// sit at this nesting level, not one below.
|
|
137
|
+
if (Array.isArray(s.allOf)) {
|
|
138
|
+
const { allOf: _members, ...own } = s;
|
|
139
|
+
const parts = declaresShape(own)
|
|
140
|
+
? [renderSchema(own, defs, seen, depth)]
|
|
141
|
+
: [];
|
|
142
|
+
for (const member of s.allOf) {
|
|
143
|
+
parts.push(renderSchema(member, defs, seen, depth + 1));
|
|
144
|
+
}
|
|
145
|
+
if (parts.length === 0) return "unknown";
|
|
146
|
+
if (parts.length === 1) return parts[0] as string;
|
|
147
|
+
return parts.map(grouped).join(" & ");
|
|
148
|
+
}
|
|
149
|
+
|
|
94
150
|
if (typeof s.$ref === "string") {
|
|
95
151
|
const name = refName(s.$ref);
|
|
96
152
|
if (seen.has(name)) return name;
|
|
@@ -112,6 +168,11 @@ function renderSchema(
|
|
|
112
168
|
if (Array.isArray(s.enum)) {
|
|
113
169
|
return s.enum.map((value) => JSON.stringify(value)).join(" | ");
|
|
114
170
|
}
|
|
171
|
+
// Checked before type/properties so a discriminator like
|
|
172
|
+
// { type: "string", const: "emoji" } renders as "emoji" rather than string.
|
|
173
|
+
// JSON.stringify(undefined) returns undefined (not a string), so an explicit
|
|
174
|
+
// `const: undefined` must fall through to the regular type rendering.
|
|
175
|
+
if (s.const !== undefined) return JSON.stringify(s.const);
|
|
115
176
|
|
|
116
177
|
const type = s.type;
|
|
117
178
|
if (type === "array" || s.items) {
|
package/src/connectors/api.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateToolInput } from "../validate.js";
|
|
1
2
|
import type {
|
|
2
3
|
Connector,
|
|
3
4
|
ConnectorCredentialConfig,
|
|
@@ -40,6 +41,14 @@ export interface ApiOptions {
|
|
|
40
41
|
values: ConnectorCredentialValues,
|
|
41
42
|
ctx: ConnectorContext,
|
|
42
43
|
) => Promise<CredentialTestResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Validate call arguments against each tool's `inputSchema` before invoking
|
|
46
|
+
* the handler (default true). Mismatches fail with a non-retryable
|
|
47
|
+
* `invalid_args` ConnectorCallError instead of reaching the handler. Set
|
|
48
|
+
* false to restore the pre-validation pass-through for deployments relying
|
|
49
|
+
* on loose coercion.
|
|
50
|
+
*/
|
|
51
|
+
validateArgs?: boolean;
|
|
43
52
|
tools: ApiTool[];
|
|
44
53
|
}
|
|
45
54
|
|
|
@@ -47,6 +56,12 @@ export interface ApiOptions {
|
|
|
47
56
|
* A connector defined entirely in code: static tool defs + fetch handlers.
|
|
48
57
|
* Tool inputs are plain JSON Schema objects (bring your own zod-to-json-schema
|
|
49
58
|
* conversion if you prefer zod). call_tool JSON-wraps the handler's return.
|
|
59
|
+
*
|
|
60
|
+
* Arguments are validated against `inputSchema` before the handler runs
|
|
61
|
+
* (disable with `validateArgs: false`). This is deliberately asymmetric with
|
|
62
|
+
* remote MCP connectors, which stay pass-through: the downstream server is
|
|
63
|
+
* authoritative for its own schemas, and re-validating with our JSON Schema
|
|
64
|
+
* draft/format semantics could reject calls the downstream would accept.
|
|
50
65
|
*/
|
|
51
66
|
export function api(id: string, opts: ApiOptions): Connector {
|
|
52
67
|
const defs: ToolDef[] = opts.tools.map((t) => ({
|
|
@@ -57,6 +72,7 @@ export function api(id: string, opts: ApiOptions): Connector {
|
|
|
57
72
|
annotations: t.annotations,
|
|
58
73
|
}));
|
|
59
74
|
const byName = new Map(opts.tools.map((t) => [t.name, t]));
|
|
75
|
+
const validateArgs = opts.validateArgs ?? true;
|
|
60
76
|
return {
|
|
61
77
|
id,
|
|
62
78
|
title: opts.title,
|
|
@@ -74,7 +90,15 @@ export function api(id: string, opts: ApiOptions): Connector {
|
|
|
74
90
|
if (!tool) {
|
|
75
91
|
throw new Error(`Unknown tool "${name}" on connector "${id}"`);
|
|
76
92
|
}
|
|
77
|
-
|
|
93
|
+
const input = args ?? {};
|
|
94
|
+
if (validateArgs && tool.inputSchema) {
|
|
95
|
+
const invalid = validateToolInput(tool.inputSchema, input, {
|
|
96
|
+
address: `${id}.${name}`,
|
|
97
|
+
logger: ctx.logger,
|
|
98
|
+
});
|
|
99
|
+
if (invalid) throw invalid;
|
|
100
|
+
}
|
|
101
|
+
return tool.handler(input, ctx);
|
|
78
102
|
},
|
|
79
103
|
};
|
|
80
104
|
}
|
|
@@ -4,6 +4,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|
|
4
4
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
5
5
|
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
|
|
6
6
|
import { KvOAuthProvider } from "../auth/downstream-oauth.js";
|
|
7
|
+
import { ConnectorCallError } from "../errors.js";
|
|
7
8
|
import { CONNECTA_VERSION } from "../version.js";
|
|
8
9
|
import type {
|
|
9
10
|
Connector,
|
|
@@ -60,6 +61,14 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
60
61
|
const states = new WeakMap<object, ConnectionState>();
|
|
61
62
|
const isOauth = opts.auth?.type === "oauth";
|
|
62
63
|
|
|
64
|
+
/** Typed per-call auth signal; the SDK's UnauthorizedError stays as cause. */
|
|
65
|
+
const authRequiredError = (cause: unknown) =>
|
|
66
|
+
new ConnectorCallError(
|
|
67
|
+
"auth_required",
|
|
68
|
+
`Connector "${id}" requires authorization — call authorize_connector({ connector: "${id}" }) and open the returned URL.`,
|
|
69
|
+
{ cause },
|
|
70
|
+
);
|
|
71
|
+
|
|
63
72
|
const stateFor = (ctx: ConnectorContext): ConnectionState => {
|
|
64
73
|
const scope = ctx.requestScope ?? ctx;
|
|
65
74
|
let state = states.get(scope);
|
|
@@ -176,6 +185,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
176
185
|
// "auth_required".
|
|
177
186
|
if (err instanceof UnauthorizedError) {
|
|
178
187
|
state.authRequired = true;
|
|
188
|
+
throw authRequiredError(err);
|
|
179
189
|
}
|
|
180
190
|
throw err;
|
|
181
191
|
} finally {
|
|
@@ -209,19 +219,28 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
209
219
|
async callTool(name, args, ctx) {
|
|
210
220
|
const state = stateFor(ctx);
|
|
211
221
|
await ensureConnected(ctx, state);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
try {
|
|
223
|
+
return await state.client!.callTool(
|
|
224
|
+
{
|
|
225
|
+
name,
|
|
226
|
+
arguments: (args ?? {}) as Record<string, unknown>,
|
|
227
|
+
},
|
|
228
|
+
undefined,
|
|
229
|
+
ctx.timeoutMs || ctx.signal
|
|
230
|
+
? {
|
|
231
|
+
...(ctx.timeoutMs ? { timeout: ctx.timeoutMs } : {}),
|
|
232
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
233
|
+
}
|
|
234
|
+
: undefined,
|
|
235
|
+
);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// A grant revoked after connect surfaces here, not in ensureConnected.
|
|
238
|
+
if (err instanceof UnauthorizedError) {
|
|
239
|
+
state.authRequired = true;
|
|
240
|
+
throw authRequiredError(err);
|
|
241
|
+
}
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
225
244
|
},
|
|
226
245
|
|
|
227
246
|
async status(ctx): Promise<ConnectorStatus> {
|
package/src/credentials.ts
CHANGED
|
@@ -219,7 +219,10 @@ export class CredentialVault {
|
|
|
219
219
|
value: string,
|
|
220
220
|
updatedBy: string,
|
|
221
221
|
): Promise<CredentialMetadata> {
|
|
222
|
-
|
|
222
|
+
// `await` (not a bare promise return) so a validation throw inside setAll
|
|
223
|
+
// never sits handler-less for the thenable-adoption microtask — workerd
|
|
224
|
+
// reports that gap as an unhandled rejection.
|
|
225
|
+
return await this.setAll(connectorId, { value }, updatedBy);
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
async setAll(
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Typed failure contract for connector tool calls. Web-API only — no node:
|
|
2
|
+
// imports here.
|
|
3
|
+
|
|
4
|
+
/** Machine-readable classification of a failed connector tool call. */
|
|
5
|
+
export type ConnectorCallErrorCode =
|
|
6
|
+
| "timeout"
|
|
7
|
+
| "auth_required"
|
|
8
|
+
| "rate_limited"
|
|
9
|
+
| "unavailable"
|
|
10
|
+
| "invalid_args"
|
|
11
|
+
| "connector_call_failed";
|
|
12
|
+
|
|
13
|
+
const RETRYABLE_BY_CODE: Record<ConnectorCallErrorCode, boolean> = {
|
|
14
|
+
timeout: true,
|
|
15
|
+
rate_limited: true,
|
|
16
|
+
unavailable: true,
|
|
17
|
+
auth_required: false,
|
|
18
|
+
invalid_args: false,
|
|
19
|
+
connector_call_failed: false,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Non-negative integer milliseconds, or undefined for anything else. */
|
|
23
|
+
function normalizeRetryAfterMs(value: number | undefined): number | undefined {
|
|
24
|
+
if (value === undefined) return undefined;
|
|
25
|
+
if (!Number.isFinite(value) || value < 0) return undefined;
|
|
26
|
+
return Math.trunc(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Throw from `Connector.callTool` (or anything beneath it) to classify a
|
|
31
|
+
* failure exactly. Untyped errors fall back to a message-text heuristic, so a
|
|
32
|
+
* connector whose legitimate error text mentions "timeout" is misread as a
|
|
33
|
+
* retryable timeout — this class is the escape hatch. `retryable` defaults per
|
|
34
|
+
* code (timeout, rate_limited, and unavailable retry; the rest do not) and may
|
|
35
|
+
* be overridden.
|
|
36
|
+
*
|
|
37
|
+
* `retryAfterMs` carries a wait window the connector already knows — a
|
|
38
|
+
* `Retry-After` header, say — so the engine can wait that long instead of
|
|
39
|
+
* guessing, and so an agent that receives the failure can decide when to
|
|
40
|
+
* re-issue.
|
|
41
|
+
*/
|
|
42
|
+
export class ConnectorCallError extends Error {
|
|
43
|
+
readonly code: ConnectorCallErrorCode;
|
|
44
|
+
readonly retryable: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Connector-known wait window in ms before this call is worth repeating,
|
|
47
|
+
* or undefined when the connector reported none. Always an own property —
|
|
48
|
+
* under ES2022 class fields the declaration itself defines it, so guarding
|
|
49
|
+
* the assignment would not keep it off the instance. Keeping the window out
|
|
50
|
+
* of the wire format is `classifyCallError`'s job, not this constructor's.
|
|
51
|
+
*/
|
|
52
|
+
readonly retryAfterMs?: number;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
code: ConnectorCallErrorCode,
|
|
56
|
+
message: string,
|
|
57
|
+
opts: { retryable?: boolean; retryAfterMs?: number; cause?: unknown } = {},
|
|
58
|
+
) {
|
|
59
|
+
super(
|
|
60
|
+
message,
|
|
61
|
+
opts.cause !== undefined ? { cause: opts.cause } : undefined,
|
|
62
|
+
);
|
|
63
|
+
this.name = "ConnectorCallError";
|
|
64
|
+
this.code = code;
|
|
65
|
+
this.retryable = opts.retryable ?? RETRYABLE_BY_CODE[code];
|
|
66
|
+
this.retryAfterMs = normalizeRetryAfterMs(opts.retryAfterMs);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The `error` object surfaced in call_tool/batch_call value-mode results. */
|
|
71
|
+
export interface CallErrorDetails {
|
|
72
|
+
code: string;
|
|
73
|
+
message: string;
|
|
74
|
+
retryable: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Connector-reported wait window in ms, when known. Reported verbatim — the
|
|
77
|
+
* engine bounds how long it will itself wait, but the caller sees the real
|
|
78
|
+
* window so it can schedule a re-issue.
|
|
79
|
+
*/
|
|
80
|
+
retryAfterMs?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const RETRYABLE_MESSAGE_RE =
|
|
84
|
+
/timeout|timed out|econnreset|econnrefused|temporar|rate.?limit|429|502|503|504|refcountedcanceler|different request/i;
|
|
85
|
+
const TIMEOUT_MESSAGE_RE = /timed out|timeout/i;
|
|
86
|
+
|
|
87
|
+
/** Message-text fallback used when an error carries no typed classification. */
|
|
88
|
+
export function messageLooksRetryable(message: string): boolean {
|
|
89
|
+
return RETRYABLE_MESSAGE_RE.test(message);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Classify a value thrown by a connector call. A `ConnectorCallError` is
|
|
94
|
+
* authoritative; anything else falls back to the historical message-text
|
|
95
|
+
* heuristic.
|
|
96
|
+
*/
|
|
97
|
+
export function classifyCallError(
|
|
98
|
+
err: unknown,
|
|
99
|
+
fallbackCode = "connector_call_failed",
|
|
100
|
+
): CallErrorDetails {
|
|
101
|
+
if (err instanceof ConnectorCallError) {
|
|
102
|
+
return {
|
|
103
|
+
code: err.code,
|
|
104
|
+
message: err.message,
|
|
105
|
+
retryable: err.retryable,
|
|
106
|
+
...(err.retryAfterMs !== undefined
|
|
107
|
+
? { retryAfterMs: err.retryAfterMs }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// An aborted fetch rejects with a DOMException named "AbortError" whose
|
|
112
|
+
// message ("The operation was aborted", and variants across runtimes) matches
|
|
113
|
+
// neither heuristic below — so a call the engine itself cancelled would read
|
|
114
|
+
// as a non-retryable failure, the opposite of the truth. Note this also
|
|
115
|
+
// covers an abort the connector triggered for its own reasons; running out of
|
|
116
|
+
// time is by far the likelier cause and retryable/timeout is the safer read.
|
|
117
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
118
|
+
return { code: "timeout", message: err.message, retryable: true };
|
|
119
|
+
}
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
+
return {
|
|
122
|
+
code: TIMEOUT_MESSAGE_RE.test(message) ? "timeout" : fallbackCode,
|
|
123
|
+
message,
|
|
124
|
+
retryable: RETRYABLE_MESSAGE_RE.test(message),
|
|
125
|
+
};
|
|
126
|
+
}
|
package/src/execute.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
|
|
4
4
|
import { recordToolActivity, type ActivityRequestContext } from "./activity.js";
|
|
5
5
|
import { errorResult, jsonResult, type ToolResult } from "./meta-tools.js";
|
|
6
|
+
import { classifyCallError, ConnectorCallError } from "./errors.js";
|
|
6
7
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
7
8
|
import type { Registry } from "./registry.js";
|
|
8
9
|
import type {
|
|
@@ -191,7 +192,10 @@ export async function buildSandboxProviders(
|
|
|
191
192
|
try {
|
|
192
193
|
timer = setTimeout(() => {
|
|
193
194
|
controller.abort(
|
|
194
|
-
new
|
|
195
|
+
new ConnectorCallError(
|
|
196
|
+
"timeout",
|
|
197
|
+
`Tool call timed out after ${hostCallTimeoutMs}ms`,
|
|
198
|
+
),
|
|
195
199
|
);
|
|
196
200
|
}, hostCallTimeoutMs);
|
|
197
201
|
const pending = resolved.connector.callTool(
|
|
@@ -216,16 +220,16 @@ export async function buildSandboxProviders(
|
|
|
216
220
|
return value;
|
|
217
221
|
} catch (err) {
|
|
218
222
|
registry.recordFailure(resolved.connector.id, Date.now() - started, err);
|
|
219
|
-
const
|
|
223
|
+
const details = classifyCallError(err);
|
|
220
224
|
recordToolActivity(activity, {
|
|
221
225
|
connectorId: resolved.connector.id,
|
|
222
226
|
toolName: resolved.toolName,
|
|
223
227
|
address: `${resolved.connector.id}.${resolved.toolName}`,
|
|
224
228
|
source: "execute_code",
|
|
225
|
-
outcome:
|
|
229
|
+
outcome: details.code === "timeout" ? "timeout" : "error",
|
|
226
230
|
durationMs: Date.now() - started,
|
|
227
231
|
attempts: 1,
|
|
228
|
-
errorCode:
|
|
232
|
+
errorCode: details.code,
|
|
229
233
|
});
|
|
230
234
|
throw err;
|
|
231
235
|
} finally {
|
|
@@ -269,8 +273,11 @@ export async function buildSandboxProviders(
|
|
|
269
273
|
);
|
|
270
274
|
continue;
|
|
271
275
|
}
|
|
276
|
+
// `await` (not a bare promise return) so a synchronous throw inside
|
|
277
|
+
// callAddress never sits handler-less for the thenable-adoption
|
|
278
|
+
// microtask — workerd reports that gap as an unhandled rejection.
|
|
272
279
|
fns[key] = async (args: unknown) =>
|
|
273
|
-
callAddress(`${connector.id}.${t.name}`, args);
|
|
280
|
+
await callAddress(`${connector.id}.${t.name}`, args);
|
|
274
281
|
}
|
|
275
282
|
if (Object.keys(fns).length > 0) {
|
|
276
283
|
providers.push({ name: ns, fns });
|
|
@@ -288,7 +295,7 @@ export async function buildSandboxProviders(
|
|
|
288
295
|
`connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`,
|
|
289
296
|
);
|
|
290
297
|
}
|
|
291
|
-
return Promise.all(
|
|
298
|
+
return await Promise.all(
|
|
292
299
|
calls.map(async (call) => {
|
|
293
300
|
const item = call as { address?: unknown; args?: unknown };
|
|
294
301
|
try {
|
package/src/index.ts
CHANGED
|
@@ -61,6 +61,22 @@ export interface ConnectaConfig {
|
|
|
61
61
|
* stash the full text for get_result paging. Default 50_000.
|
|
62
62
|
*/
|
|
63
63
|
maxResultBytes?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Deadline (ms) applied to call_tool/batch_call calls that pass no
|
|
66
|
+
* `timeoutMs`, giving the connector both a budget (`ctx.timeoutMs`) and a
|
|
67
|
+
* cancellation signal (`ctx.signal`). An explicit per-call `timeoutMs` always
|
|
68
|
+
* wins. **Opt-in — undefined by default**, because switching it on globally
|
|
69
|
+
* would put a deadline on every call in an existing deployment and the
|
|
70
|
+
* failure mode is a working long-running call starting to time out.
|
|
71
|
+
* `execute_code` host calls are unaffected; they already carry a 15 s bound.
|
|
72
|
+
*
|
|
73
|
+
* Bounds a single attempt, not the whole call — the same as an explicit
|
|
74
|
+
* `timeoutMs` has always done. A call that also passes `maxRetries` can
|
|
75
|
+
* therefore run to roughly `(maxRetries + 1)` times this value plus backoff.
|
|
76
|
+
* `maxRetries` defaults to 0, so this is the total for every call that does
|
|
77
|
+
* not explicitly ask to retry.
|
|
78
|
+
*/
|
|
79
|
+
defaultToolTimeoutMs?: number;
|
|
64
80
|
serverInfo?: {
|
|
65
81
|
name?: string;
|
|
66
82
|
version?: string;
|
|
@@ -141,6 +157,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
141
157
|
activityReadGate: config.activityReadGate,
|
|
142
158
|
activityDeploymentId: config.activityDeploymentId,
|
|
143
159
|
executor: config.executor,
|
|
160
|
+
defaultToolTimeoutMs: config.defaultToolTimeoutMs,
|
|
144
161
|
credentialVault,
|
|
145
162
|
deploymentInfo: config.deploymentInfo,
|
|
146
163
|
branding: config.branding,
|
|
@@ -159,6 +176,13 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
159
176
|
|
|
160
177
|
export { remoteMcp } from "./connectors/remote-mcp.js";
|
|
161
178
|
export { api } from "./connectors/api.js";
|
|
179
|
+
export { ConnectorCallError } from "./errors.js";
|
|
180
|
+
export type { ConnectorCallErrorCode, CallErrorDetails } from "./errors.js";
|
|
181
|
+
// The same argument validation api() performs, usable by connectors that
|
|
182
|
+
// implement the Connector interface directly. Returns the error rather than
|
|
183
|
+
// throwing so the caller decides what to do with it.
|
|
184
|
+
export { validateToolInput } from "./validate.js";
|
|
185
|
+
export type { ValidateToolInputOptions } from "./validate.js";
|
|
162
186
|
export { bearerToken } from "./auth/bearer.js";
|
|
163
187
|
export { memoryStorage } from "./storage/memory.js";
|
|
164
188
|
export { CONNECTA_VERSION } from "./version.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Public re-export of the JSON Schema validator connecta itself uses, so
|
|
2
|
+
// downstream code that validates at build time (a manifest generator asserting
|
|
3
|
+
// its own output, say) resolves the same implementation and version through an
|
|
4
|
+
// explicit subpath rather than through npm hoisting.
|
|
5
|
+
export { Validator } from "@cfworker/json-schema";
|
|
6
|
+
export type {
|
|
7
|
+
OutputUnit,
|
|
8
|
+
Schema,
|
|
9
|
+
SchemaDraft,
|
|
10
|
+
ValidationResult,
|
|
11
|
+
} from "@cfworker/json-schema";
|