@zackbart/connecta 0.8.0 → 0.9.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 +128 -0
- package/README.md +31 -55
- package/SECURITY.md +5 -11
- package/dist/activity.d.ts +0 -8
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +0 -1
- package/dist/activity.js.map +1 -1
- package/dist/auth/bearer.d.ts +1 -7
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +2 -23
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +2 -11
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +31 -46
- package/dist/auth/clerk.js.map +1 -1
- package/dist/auth/downstream-oauth.d.ts +15 -6
- package/dist/auth/downstream-oauth.d.ts.map +1 -1
- package/dist/auth/downstream-oauth.js +60 -11
- package/dist/auth/downstream-oauth.js.map +1 -1
- package/dist/call-admission.d.ts.map +1 -1
- package/dist/call-admission.js +0 -1
- package/dist/call-admission.js.map +1 -1
- package/dist/catalog-service.d.ts +1 -2
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +12 -18
- package/dist/catalog-service.js.map +1 -1
- package/dist/catalog.d.ts +6 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +51 -0
- package/dist/catalog.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +73 -71
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/errors.d.ts +19 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +4 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +41 -20
- package/dist/execute.js.map +1 -1
- package/dist/executor-result.js +1 -1
- package/dist/executor-result.js.map +1 -1
- package/dist/index.d.ts +3 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -90
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +5 -0
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +46 -10
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +4 -12
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +119 -169
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/operator-ui/model.d.ts +1 -22
- package/dist/operator-ui/model.d.ts.map +1 -1
- package/dist/operator-ui/model.js.map +1 -1
- package/dist/registry.d.ts +11 -178
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +52 -330
- package/dist/registry.js.map +1 -1
- package/dist/retired-toolkits.d.ts +3 -0
- package/dist/retired-toolkits.d.ts.map +1 -0
- package/dist/retired-toolkits.js +9 -0
- package/dist/retired-toolkits.js.map +1 -0
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/activity.js +2 -5
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/credentials.js +1 -12
- package/dist/routes/credentials.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +94 -188
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/oauth.d.ts.map +1 -1
- package/dist/routes/oauth.js +3 -8
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/shared.d.ts +9 -30
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js +10 -69
- package/dist/routes/shared.js.map +1 -1
- package/dist/routes/ui.d.ts.map +1 -1
- package/dist/routes/ui.js +4 -10
- package/dist/routes/ui.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -31
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +3 -6
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +9 -38
- package/dist/skills.js.map +1 -1
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +2 -4
- package/dist/timeout.js.map +1 -1
- package/dist/types.d.ts +8 -61
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +1 -2
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +5 -35
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/activity.ts +0 -9
- package/src/auth/bearer.ts +3 -32
- package/src/auth/clerk.ts +35 -48
- package/src/auth/downstream-oauth.ts +106 -24
- package/src/call-admission.ts +0 -1
- package/src/catalog-service.ts +23 -23
- package/src/catalog.ts +55 -0
- package/src/connectors/remote-mcp.ts +106 -86
- package/src/errors.ts +22 -0
- package/src/execute.ts +43 -24
- package/src/executor-result.ts +1 -1
- package/src/index.ts +24 -208
- package/src/invocation.ts +63 -10
- package/src/meta-tools.ts +139 -187
- package/src/operator-ui/browser.css +1 -54
- package/src/operator-ui/browser.ts +16 -150
- package/src/operator-ui/generated.ts +2 -2
- package/src/operator-ui/model.ts +1 -23
- package/src/registry.ts +59 -429
- package/src/retired-toolkits.ts +14 -0
- package/src/routes/activity.ts +1 -6
- package/src/routes/credentials.ts +0 -12
- package/src/routes/mcp.ts +110 -230
- package/src/routes/oauth.ts +2 -8
- package/src/routes/shared.ts +10 -100
- package/src/routes/ui.ts +2 -12
- package/src/server.ts +1 -31
- package/src/skills.ts +9 -38
- package/src/timeout.ts +2 -4
- package/src/types.ts +11 -62
- package/src/ui.ts +9 -46
- package/src/version.ts +1 -1
- package/dist/credential-health.d.ts +0 -205
- package/dist/credential-health.d.ts.map +0 -1
- package/dist/credential-health.js +0 -610
- package/dist/credential-health.js.map +0 -1
- package/dist/toolkits.d.ts +0 -124
- package/dist/toolkits.d.ts.map +0 -1
- package/dist/toolkits.js +0 -322
- package/dist/toolkits.js.map +0 -1
- package/src/credential-health.ts +0 -835
- package/src/toolkits.ts +0 -461
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
Client,
|
|
3
|
+
isInputRequiredResult,
|
|
4
|
+
specTypeSchemas,
|
|
5
|
+
StreamableHTTPClientTransport,
|
|
6
|
+
UnauthorizedError,
|
|
7
|
+
} from "@modelcontextprotocol/client";
|
|
4
8
|
import type {
|
|
5
9
|
FetchLike,
|
|
10
|
+
ListToolsResult,
|
|
11
|
+
StandardSchemaV1,
|
|
12
|
+
Tool,
|
|
6
13
|
Transport,
|
|
7
|
-
} from "@modelcontextprotocol/
|
|
8
|
-
import { ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
|
|
10
|
-
import { z } from "zod";
|
|
14
|
+
} from "@modelcontextprotocol/client";
|
|
11
15
|
import { KvOAuthProvider } from "../auth/downstream-oauth.js";
|
|
12
16
|
import { MAX_CATALOG_TOOLS } from "../catalog-limits.js";
|
|
13
17
|
import { ConnectorCallError } from "../errors.js";
|
|
@@ -131,36 +135,29 @@ type ListedTool = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
|
|
|
131
135
|
* end-of-pagination as `null`. Only the cursor is widened; every tool and every
|
|
132
136
|
* other result field still passes through the SDK's pinned schema.
|
|
133
137
|
*/
|
|
134
|
-
const CompatibleListToolsResultSchema
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
client as unknown as {
|
|
158
|
-
cacheToolMetadata?: (tools: ListedTool[]) => void;
|
|
159
|
-
}
|
|
160
|
-
).cacheToolMetadata;
|
|
161
|
-
if (typeof prime !== "function") return;
|
|
162
|
-
prime.call(client, tools);
|
|
163
|
-
}
|
|
138
|
+
const CompatibleListToolsResultSchema: StandardSchemaV1<
|
|
139
|
+
unknown,
|
|
140
|
+
ListToolsResult
|
|
141
|
+
> = {
|
|
142
|
+
"~standard": {
|
|
143
|
+
version: 1,
|
|
144
|
+
vendor: "connecta",
|
|
145
|
+
validate(value) {
|
|
146
|
+
const normalized =
|
|
147
|
+
typeof value === "object" &&
|
|
148
|
+
value !== null &&
|
|
149
|
+
"nextCursor" in value &&
|
|
150
|
+
value.nextCursor === null
|
|
151
|
+
? (() => {
|
|
152
|
+
const copy = { ...value };
|
|
153
|
+
delete copy.nextCursor;
|
|
154
|
+
return copy;
|
|
155
|
+
})()
|
|
156
|
+
: value;
|
|
157
|
+
return specTypeSchemas.ListToolsResult["~standard"].validate(normalized);
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
164
161
|
|
|
165
162
|
/**
|
|
166
163
|
* True for a result-parse failure caused by the page's `nextCursor` itself.
|
|
@@ -171,13 +168,18 @@ function primeToolMetadata(client: Client, tools: ListedTool[]): void {
|
|
|
171
168
|
*/
|
|
172
169
|
function isCursorShapeError(err: unknown): boolean {
|
|
173
170
|
const issues = (err as { issues?: unknown } | null)?.issues;
|
|
174
|
-
|
|
171
|
+
if (
|
|
175
172
|
Array.isArray(issues) &&
|
|
176
173
|
issues.some((issue) => {
|
|
177
174
|
const path = (issue as { path?: unknown }).path;
|
|
178
175
|
return Array.isArray(path) && path[0] === "nextCursor";
|
|
179
176
|
})
|
|
180
|
-
)
|
|
177
|
+
) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
// SDK v2 wraps Standard Schema failures in a ProtocolError and preserves the
|
|
181
|
+
// failing path in the message rather than exposing the validator's issues.
|
|
182
|
+
return msg(err).startsWith("Invalid result for tools/list: nextCursor:");
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
function msg(err: unknown): string {
|
|
@@ -190,8 +192,7 @@ function msg(err: unknown): string {
|
|
|
190
192
|
* `Client.close()` only unwinds our side — it aborts the transport's controller
|
|
191
193
|
* and fires `onclose`. Spec session termination is a separate DELETE carrying
|
|
192
194
|
* `Mcp-Session-Id`, and without it a stateful provider keeps the session alive
|
|
193
|
-
* until its own (often hour-long) timeout,
|
|
194
|
-
* accumulate several of per connector.
|
|
195
|
+
* until its own (often hour-long) timeout, accumulating abandoned sessions.
|
|
195
196
|
*
|
|
196
197
|
* Ordering is load-bearing: the SDK sends that DELETE on the transport's
|
|
197
198
|
* AbortSignal, so calling this *after* close would abort the request on issue
|
|
@@ -394,6 +395,14 @@ export function redirectSafeFetch(
|
|
|
394
395
|
interface ConnectionState {
|
|
395
396
|
client: Client | null;
|
|
396
397
|
transport: Transport | null;
|
|
398
|
+
/**
|
|
399
|
+
* The last complete raw catalog, retained only for this request scope.
|
|
400
|
+
*
|
|
401
|
+
* SDK v2 exposes `toolDefinition` as the public call-time seam for output
|
|
402
|
+
* validation and header mirroring, replacing the v1 private
|
|
403
|
+
* `cacheToolMetadata` reach-through.
|
|
404
|
+
*/
|
|
405
|
+
toolDefinitions: Map<string, Tool>;
|
|
397
406
|
connecting: Promise<void> | null;
|
|
398
407
|
authRequired: boolean;
|
|
399
408
|
provider: KvOAuthProvider | null;
|
|
@@ -468,6 +477,14 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
468
477
|
const scopeEndedError = () =>
|
|
469
478
|
new Error(`Connector "${id}" scope ended during connection.`);
|
|
470
479
|
|
|
480
|
+
const requestOptions = (ctx: ConnectorContext) =>
|
|
481
|
+
ctx.timeoutMs || ctx.signal
|
|
482
|
+
? {
|
|
483
|
+
...(ctx.timeoutMs ? { timeout: ctx.timeoutMs } : {}),
|
|
484
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
485
|
+
}
|
|
486
|
+
: undefined;
|
|
487
|
+
|
|
471
488
|
/**
|
|
472
489
|
* One `tools/list` request. The SDK schema is retained wholesale except for
|
|
473
490
|
* accepting `null` as the common, unambiguous end-of-chain spelling. Other
|
|
@@ -485,12 +502,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
485
502
|
...(cursor === undefined ? {} : { params: { cursor } }),
|
|
486
503
|
},
|
|
487
504
|
CompatibleListToolsResultSchema,
|
|
488
|
-
ctx
|
|
489
|
-
? {
|
|
490
|
-
...(ctx.timeoutMs ? { timeout: ctx.timeoutMs } : {}),
|
|
491
|
-
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
492
|
-
}
|
|
493
|
-
: undefined,
|
|
505
|
+
requestOptions(ctx),
|
|
494
506
|
);
|
|
495
507
|
} catch (err) {
|
|
496
508
|
if (!isCursorShapeError(err)) throw err;
|
|
@@ -509,6 +521,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
509
521
|
state = {
|
|
510
522
|
client: null,
|
|
511
523
|
transport: null,
|
|
524
|
+
toolDefinitions: new Map(),
|
|
512
525
|
connecting: null,
|
|
513
526
|
authRequired: false,
|
|
514
527
|
provider: null,
|
|
@@ -547,29 +560,23 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
547
560
|
const url = new URL(opts.url);
|
|
548
561
|
const guardedFetch = redirectSafeFetch(id, opts.redirects);
|
|
549
562
|
if (opts.auth?.type === "oauth") {
|
|
550
|
-
// The SDK class declares `sessionId` as an own `string | undefined`
|
|
551
|
-
// property while its Transport interface declares it optional. They are
|
|
552
|
-
// runtime-compatible; exact optional types only exposes that declaration
|
|
553
|
-
// mismatch at this boundary.
|
|
554
563
|
return new StreamableHTTPClientTransport(url, {
|
|
555
564
|
authProvider: provider ?? newProvider(ctx),
|
|
556
565
|
fetch: guardedFetch,
|
|
557
|
-
})
|
|
566
|
+
});
|
|
558
567
|
}
|
|
559
568
|
const headers =
|
|
560
569
|
opts.auth?.type === "headers" ? opts.auth.headers : undefined;
|
|
561
|
-
return new StreamableHTTPClientTransport(
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
fetch: guardedFetch,
|
|
566
|
-
},
|
|
567
|
-
) as unknown as Transport;
|
|
570
|
+
return new StreamableHTTPClientTransport(url, {
|
|
571
|
+
...(headers ? { requestInit: { headers } } : {}),
|
|
572
|
+
fetch: guardedFetch,
|
|
573
|
+
});
|
|
568
574
|
};
|
|
569
575
|
|
|
570
576
|
const reset = (state: ConnectionState) => {
|
|
571
577
|
state.client = null;
|
|
572
578
|
state.transport = null;
|
|
579
|
+
state.toolDefinitions.clear();
|
|
573
580
|
state.connecting = null;
|
|
574
581
|
state.authRequired = false;
|
|
575
582
|
state.provider = null;
|
|
@@ -650,13 +657,17 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
650
657
|
throw operatorDisconnectedError();
|
|
651
658
|
}
|
|
652
659
|
provider?.captureGeneration(genAtStart);
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
// the SDK's edge-safe validator is selected explicitly.
|
|
660
|
+
// SDK v2 selects its validator by runtime export condition: AJV on
|
|
661
|
+
// Node and @cfworker/json-schema under workerd. The Workers-safe path
|
|
662
|
+
// no longer needs Connecta-specific wiring.
|
|
657
663
|
const c = new Client(
|
|
658
664
|
{ name: "connecta", version: CONNECTA_VERSION },
|
|
659
|
-
{
|
|
665
|
+
{
|
|
666
|
+
versionNegotiation: { mode: "auto" },
|
|
667
|
+
// Connecta has no interactive relay. Surface the result manually
|
|
668
|
+
// below as one structured, non-retryable connector failure.
|
|
669
|
+
inputRequired: { autoFulfill: false },
|
|
670
|
+
},
|
|
660
671
|
);
|
|
661
672
|
const t = buildTransport(ctx, provider);
|
|
662
673
|
if (!ownsAttempt()) await abandon(t);
|
|
@@ -869,9 +880,9 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
869
880
|
`Connector "${id}" kept advertising more tools/list pages after ${MAX_TOOL_PAGES} — refusing to page further.`,
|
|
870
881
|
);
|
|
871
882
|
}
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
|
|
883
|
+
// Publish definitions only after the full walk succeeds. A later-page
|
|
884
|
+
// failure must not leave a partial validation/header view behind.
|
|
885
|
+
state.toolDefinitions = new Map(listed.map((tool) => [tool.name, tool]));
|
|
875
886
|
return listed.map((t) => ({
|
|
876
887
|
name: t.name,
|
|
877
888
|
...(t.description !== undefined ? { description: t.description } : {}),
|
|
@@ -904,19 +915,32 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
904
915
|
await ensureConnected(ctx, state);
|
|
905
916
|
const client = state.client!;
|
|
906
917
|
try {
|
|
907
|
-
|
|
918
|
+
const toolDefinition = state.toolDefinitions.get(name);
|
|
919
|
+
if (toolDefinition?.execution?.taskSupport === "required") {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`Tool "${name}" requires task-based execution, which Connecta does not support.`,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
const result = await client.callTool(
|
|
908
925
|
{
|
|
909
926
|
name,
|
|
910
927
|
arguments: (args ?? {}) as Record<string, unknown>,
|
|
911
928
|
},
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
: undefined,
|
|
929
|
+
{
|
|
930
|
+
...requestOptions(ctx),
|
|
931
|
+
allowInputRequired: true,
|
|
932
|
+
...(toolDefinition ? { toolDefinition } : {}),
|
|
933
|
+
},
|
|
919
934
|
);
|
|
935
|
+
if (isInputRequiredResult(result)) {
|
|
936
|
+
throw new ConnectorCallError(
|
|
937
|
+
"input_required_unsupported",
|
|
938
|
+
`Connector "${id}" returned input_required for "${name}". ` +
|
|
939
|
+
"Connecta cannot relay multi-round-trip input yet; this " +
|
|
940
|
+
"capability is gated pending real host and downstream adoption.",
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return result;
|
|
920
944
|
} catch (err) {
|
|
921
945
|
// A grant revoked after connect surfaces here, not in ensureConnected.
|
|
922
946
|
if (err instanceof UnauthorizedError) {
|
|
@@ -942,6 +966,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
942
966
|
const transport = state.transport;
|
|
943
967
|
state.client = null;
|
|
944
968
|
state.transport = null;
|
|
969
|
+
state.toolDefinitions.clear();
|
|
945
970
|
state.connecting = null;
|
|
946
971
|
state.authRequired = false;
|
|
947
972
|
state.connectedGeneration = null;
|
|
@@ -982,7 +1007,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
982
1007
|
}
|
|
983
1008
|
},
|
|
984
1009
|
|
|
985
|
-
async finishAuth(code, ctx) {
|
|
1010
|
+
async finishAuth(code, ctx, callbackParams) {
|
|
986
1011
|
const state = stateFor(ctx);
|
|
987
1012
|
const provider = getProvider(ctx, state);
|
|
988
1013
|
// verifyState ran on this request-scoped provider first and captured the
|
|
@@ -990,7 +1015,11 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
990
1015
|
// token write remains tagged with that older generation and is unreadable.
|
|
991
1016
|
const t = (state.transport ??
|
|
992
1017
|
buildTransport(ctx, provider)) as StreamableHTTPClientTransport;
|
|
993
|
-
|
|
1018
|
+
if (callbackParams !== undefined) {
|
|
1019
|
+
await t.finishAuth(callbackParams);
|
|
1020
|
+
} else {
|
|
1021
|
+
await t.finishAuth(code);
|
|
1022
|
+
}
|
|
994
1023
|
await provider.clearPending();
|
|
995
1024
|
// Reset so the next use reconnects with the freshly stored tokens.
|
|
996
1025
|
reset(state);
|
|
@@ -998,15 +1027,6 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
998
1027
|
};
|
|
999
1028
|
|
|
1000
1029
|
if (opts.auth?.type === "oauth") {
|
|
1001
|
-
// The credential liveness checks (issue #24) probe only connectors that
|
|
1002
|
-
// actually hold a stored grant: with no tokens there is nothing whose
|
|
1003
|
-
// liveness could have lapsed, and a `status()` probe would kick off DCR +
|
|
1004
|
-
// consent on a timer for a connector nobody has authorized yet.
|
|
1005
|
-
connector.hasStoredCredential = async (ctx) => {
|
|
1006
|
-
const state = stateFor(ctx);
|
|
1007
|
-
return (await getProvider(ctx, state).tokens()) !== undefined;
|
|
1008
|
-
};
|
|
1009
|
-
|
|
1010
1030
|
connector.verifyState = async (oauthState, ctx) => {
|
|
1011
1031
|
const state = stateFor(ctx);
|
|
1012
1032
|
return getProvider(ctx, state).verifyState(oauthState);
|
package/src/errors.ts
CHANGED
|
@@ -8,14 +8,22 @@ export type ConnectorCallErrorCode =
|
|
|
8
8
|
| "rate_limited"
|
|
9
9
|
| "unavailable"
|
|
10
10
|
| "invalid_args"
|
|
11
|
+
| "input_required_unsupported"
|
|
11
12
|
| "connector_call_failed";
|
|
12
13
|
|
|
14
|
+
/** Agent-visible recovery class attached only to `auth_required` failures. */
|
|
15
|
+
export type AuthRecoveryMode =
|
|
16
|
+
| "oauth"
|
|
17
|
+
| "operator_config"
|
|
18
|
+
| "unavailable";
|
|
19
|
+
|
|
13
20
|
const RETRYABLE_BY_CODE: Record<ConnectorCallErrorCode, boolean> = {
|
|
14
21
|
timeout: true,
|
|
15
22
|
rate_limited: true,
|
|
16
23
|
unavailable: true,
|
|
17
24
|
auth_required: false,
|
|
18
25
|
invalid_args: false,
|
|
26
|
+
input_required_unsupported: false,
|
|
19
27
|
connector_call_failed: false,
|
|
20
28
|
};
|
|
21
29
|
|
|
@@ -78,6 +86,20 @@ export interface CallErrorDetails {
|
|
|
78
86
|
* window so it can schedule a re-issue.
|
|
79
87
|
*/
|
|
80
88
|
retryAfterMs?: number;
|
|
89
|
+
/** Connector whose failed operation needs recovery. */
|
|
90
|
+
connector?: string;
|
|
91
|
+
/** Canonical downstream address the agent may retry after recovery. */
|
|
92
|
+
operation?: string;
|
|
93
|
+
/** Which safe recovery path `authorize_connector` will return. */
|
|
94
|
+
recovery?: AuthRecoveryMode;
|
|
95
|
+
/** The single model-facing entry point for every credential class. */
|
|
96
|
+
nextAction?: {
|
|
97
|
+
tool: "authorize_connector";
|
|
98
|
+
arguments: { connector: string };
|
|
99
|
+
operatorHandoff: string;
|
|
100
|
+
};
|
|
101
|
+
/** Explicit retry guidance; recovery never retries or mutates by itself. */
|
|
102
|
+
retry?: string;
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
const RETRYABLE_MESSAGE_RE =
|
package/src/execute.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { McpServer } from "@modelcontextprotocol/
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { ActivityRequestContext } from "./activity.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
boundedDiscoveryText,
|
|
6
6
|
CatalogService,
|
|
7
7
|
flatSearchResult,
|
|
8
8
|
} from "./catalog-service.js";
|
|
@@ -16,14 +16,12 @@ import {
|
|
|
16
16
|
ExecutorAdmissionError,
|
|
17
17
|
isAdmittingExecutor,
|
|
18
18
|
} from "./executor-admission.js";
|
|
19
|
-
import { unwrapMcpResult } from "./mcp-result.js";
|
|
20
19
|
import {
|
|
21
20
|
InvocationFailure,
|
|
22
21
|
InvocationService,
|
|
23
22
|
} from "./invocation.js";
|
|
24
23
|
import type { RegistryView } from "./registry.js";
|
|
25
24
|
import type {
|
|
26
|
-
Connector,
|
|
27
25
|
Executor,
|
|
28
26
|
ExecutorProvider,
|
|
29
27
|
Logger,
|
|
@@ -137,19 +135,6 @@ ${declarations}
|
|
|
137
135
|
})();`;
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
/**
|
|
141
|
-
* Unwrap an MCP CallToolResult so sandbox code sees plain values:
|
|
142
|
-
* isError throws (a real exception the code can catch), structuredContent
|
|
143
|
-
* wins when present, all-text content is JSON.parsed when possible.
|
|
144
|
-
* Non-MCP connectors already return plain values.
|
|
145
|
-
*/
|
|
146
|
-
export function unwrapForSandbox(
|
|
147
|
-
kind: Connector["kind"],
|
|
148
|
-
result: unknown,
|
|
149
|
-
): unknown {
|
|
150
|
-
return unwrapMcpResult(kind, result);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
138
|
/**
|
|
154
139
|
* Expose one fixed host provider plus trusted sandbox setup that creates a
|
|
155
140
|
* lazy proxy global per connector. No connector catalog is touched until code
|
|
@@ -165,6 +150,7 @@ export async function buildSandboxProviders(
|
|
|
165
150
|
maxHostCalls?: number;
|
|
166
151
|
hostCallTimeoutMs?: number;
|
|
167
152
|
discoveryConcurrency?: number;
|
|
153
|
+
onInvocationFailure?: (failure: InvocationFailure) => void;
|
|
168
154
|
} = {},
|
|
169
155
|
): Promise<ExecutorProvider[]> {
|
|
170
156
|
// All host calls made by one execute_code invocation share a downstream
|
|
@@ -225,7 +211,11 @@ export async function buildSandboxProviders(
|
|
|
225
211
|
args ?? {},
|
|
226
212
|
invocationContext(),
|
|
227
213
|
);
|
|
228
|
-
if (!outcome.ok)
|
|
214
|
+
if (!outcome.ok) {
|
|
215
|
+
const failure = new InvocationFailure(outcome.error);
|
|
216
|
+
limits.onInvocationFailure?.(failure);
|
|
217
|
+
throw failure;
|
|
218
|
+
}
|
|
229
219
|
return outcome.value;
|
|
230
220
|
};
|
|
231
221
|
const callNamespace = async (
|
|
@@ -240,7 +230,11 @@ export async function buildSandboxProviders(
|
|
|
240
230
|
args ?? {},
|
|
241
231
|
invocationContext(),
|
|
242
232
|
);
|
|
243
|
-
if (!outcome.ok)
|
|
233
|
+
if (!outcome.ok) {
|
|
234
|
+
const failure = new InvocationFailure(outcome.error);
|
|
235
|
+
limits.onInvocationFailure?.(failure);
|
|
236
|
+
throw failure;
|
|
237
|
+
}
|
|
244
238
|
return outcome.value;
|
|
245
239
|
};
|
|
246
240
|
|
|
@@ -287,7 +281,7 @@ export async function buildSandboxProviders(
|
|
|
287
281
|
includeSchemas?: "compact" | "json";
|
|
288
282
|
};
|
|
289
283
|
const result = flatSearchResult(await catalog.search(args));
|
|
290
|
-
|
|
284
|
+
boundedDiscoveryText(
|
|
291
285
|
result,
|
|
292
286
|
"Request a smaller limit, omit fullDescriptions, or use compact schemas.",
|
|
293
287
|
);
|
|
@@ -300,7 +294,7 @@ export async function buildSandboxProviders(
|
|
|
300
294
|
fullDescriptions?: boolean;
|
|
301
295
|
};
|
|
302
296
|
const result = { tools: await catalog.describe(args) };
|
|
303
|
-
|
|
297
|
+
boundedDiscoveryText(
|
|
304
298
|
result,
|
|
305
299
|
'Split the address list or use format: "compact".',
|
|
306
300
|
);
|
|
@@ -332,6 +326,7 @@ export function createExecuteTool(
|
|
|
332
326
|
}
|
|
333
327
|
let lease;
|
|
334
328
|
let outcome;
|
|
329
|
+
const invocationFailures: InvocationFailure[] = [];
|
|
335
330
|
try {
|
|
336
331
|
// Admission comes before provider construction: queued calls retain no
|
|
337
332
|
// catalogs, request scopes, or one-closure-per-tool provider arrays.
|
|
@@ -350,6 +345,9 @@ export function createExecuteTool(
|
|
|
350
345
|
activity,
|
|
351
346
|
{
|
|
352
347
|
signal: controller.signal,
|
|
348
|
+
onInvocationFailure: (failure) => {
|
|
349
|
+
invocationFailures.push(failure);
|
|
350
|
+
},
|
|
353
351
|
...(config.discoveryConcurrency !== undefined
|
|
354
352
|
? { discoveryConcurrency: config.discoveryConcurrency }
|
|
355
353
|
: {}),
|
|
@@ -401,6 +399,27 @@ export function createExecuteTool(
|
|
|
401
399
|
)
|
|
402
400
|
: undefined;
|
|
403
401
|
if (outcome.error) {
|
|
402
|
+
// Executor bridges necessarily reduce thrown host errors to strings.
|
|
403
|
+
// Match that terminal string back to the request-local typed failure so
|
|
404
|
+
// an unhandled tool failure keeps the same structured contract as
|
|
405
|
+
// call_tool and batch_call. Failures caught by model code never reach
|
|
406
|
+
// outcome.error and therefore remain under that code's control.
|
|
407
|
+
let invocationFailure: InvocationFailure | undefined;
|
|
408
|
+
for (let i = invocationFailures.length - 1; i >= 0; i--) {
|
|
409
|
+
const candidate = invocationFailures[i];
|
|
410
|
+
if (candidate && outcome.error.includes(candidate.message)) {
|
|
411
|
+
invocationFailure = candidate;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (invocationFailure) {
|
|
416
|
+
const result = jsonResult({
|
|
417
|
+
error: invocationFailure.details,
|
|
418
|
+
...(logs ? { logs } : {}),
|
|
419
|
+
});
|
|
420
|
+
result.isError = true;
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
404
423
|
return errorResult(
|
|
405
424
|
`Error: ${outcome.error}${logs ? `\n\nLogs:\n${logs}` : ""}`,
|
|
406
425
|
);
|
|
@@ -463,11 +482,11 @@ export function registerExecuteTool(
|
|
|
463
482
|
"execute_code",
|
|
464
483
|
{
|
|
465
484
|
description: EXECUTE_DESC,
|
|
466
|
-
inputSchema: {
|
|
485
|
+
inputSchema: z.object({
|
|
467
486
|
code: z
|
|
468
487
|
.string()
|
|
469
488
|
.describe("A JavaScript async arrow function to execute."),
|
|
470
|
-
},
|
|
489
|
+
}),
|
|
471
490
|
// The sandbox exposes only tools that are explicitly read-only, and the
|
|
472
491
|
// executor grants no network, filesystem, env, or timer capabilities.
|
|
473
492
|
annotations: {
|
|
@@ -478,7 +497,7 @@ export function registerExecuteTool(
|
|
|
478
497
|
},
|
|
479
498
|
async (args, extra) => {
|
|
480
499
|
const controller = new AbortController();
|
|
481
|
-
const signals = [extra.signal, ctx.requestSignal].filter(
|
|
500
|
+
const signals = [extra.mcpReq.signal, ctx.requestSignal].filter(
|
|
482
501
|
(signal): signal is AbortSignal => signal !== undefined,
|
|
483
502
|
);
|
|
484
503
|
const forwarders = signals.map((signal) => {
|
package/src/executor-result.ts
CHANGED
|
@@ -9,7 +9,7 @@ function msg(err: unknown): string {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function serializeExecuteValue(value: unknown): string {
|
|
12
|
-
const serialized = JSON.stringify(value
|
|
12
|
+
const serialized = JSON.stringify(value);
|
|
13
13
|
return serialized === undefined ? String(value) : serialized;
|
|
14
14
|
}
|
|
15
15
|
|