@zackbart/connecta 0.8.0 → 0.8.1
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 +87 -0
- package/README.md +31 -55
- 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/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.map +1 -1
- package/dist/connectors/remote-mcp.js +9 -22
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +1 -8
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +3 -13
- 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 +3 -11
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +105 -157
- 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 +32 -145
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/oauth.d.ts.map +1 -1
- package/dist/routes/oauth.js +2 -7
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/shared.d.ts +7 -28
- 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 +2 -59
- 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 +1 -1
- package/src/activity.ts +0 -9
- package/src/auth/bearer.ts +3 -32
- package/src/auth/clerk.ts +35 -48
- 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 +11 -23
- package/src/errors.ts +20 -0
- package/src/execute.ts +3 -18
- package/src/executor-result.ts +1 -1
- package/src/index.ts +24 -208
- package/src/invocation.ts +63 -10
- package/src/meta-tools.ts +122 -174
- 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 +40 -186
- package/src/routes/oauth.ts +1 -7
- package/src/routes/shared.ts +8 -98
- 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 +1 -60
- 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
package/src/invocation.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
classifyCallError,
|
|
14
14
|
ConnectorCallError,
|
|
15
15
|
messageLooksRetryable,
|
|
16
|
+
type AuthRecoveryMode,
|
|
16
17
|
type CallErrorDetails,
|
|
17
18
|
} from "./errors.js";
|
|
18
19
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
@@ -81,6 +82,21 @@ function callerCancelledDetails(): CallErrorDetails {
|
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
function recoveryMode(
|
|
86
|
+
registry: RegistryView,
|
|
87
|
+
connector: ResolvedCatalogTool["connector"],
|
|
88
|
+
baseUrl: string,
|
|
89
|
+
): AuthRecoveryMode {
|
|
90
|
+
if (connector.startAuth) return "oauth";
|
|
91
|
+
if (
|
|
92
|
+
connector.credential &&
|
|
93
|
+
registry.contextFor(connector.id, baseUrl).credential
|
|
94
|
+
) {
|
|
95
|
+
return "operator_config";
|
|
96
|
+
}
|
|
97
|
+
return "unavailable";
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
function isCallerCancellation(
|
|
85
101
|
error: unknown,
|
|
86
102
|
signal: AbortSignal | undefined,
|
|
@@ -155,6 +171,11 @@ export class InvocationFailure extends Error {
|
|
|
155
171
|
readonly code: string;
|
|
156
172
|
readonly retryable: boolean;
|
|
157
173
|
readonly retryAfterMs: number | undefined;
|
|
174
|
+
readonly connector: string | undefined;
|
|
175
|
+
readonly operation: string | undefined;
|
|
176
|
+
readonly recovery: CallErrorDetails["recovery"];
|
|
177
|
+
readonly nextAction: CallErrorDetails["nextAction"];
|
|
178
|
+
readonly retry: string | undefined;
|
|
158
179
|
|
|
159
180
|
constructor(readonly details: CallErrorDetails) {
|
|
160
181
|
super(details.message);
|
|
@@ -162,6 +183,11 @@ export class InvocationFailure extends Error {
|
|
|
162
183
|
this.code = details.code;
|
|
163
184
|
this.retryable = details.retryable;
|
|
164
185
|
this.retryAfterMs = details.retryAfterMs;
|
|
186
|
+
this.connector = details.connector;
|
|
187
|
+
this.operation = details.operation;
|
|
188
|
+
this.recovery = details.recovery;
|
|
189
|
+
this.nextAction = details.nextAction;
|
|
190
|
+
this.retry = details.retry;
|
|
165
191
|
}
|
|
166
192
|
}
|
|
167
193
|
|
|
@@ -262,13 +288,36 @@ export class InvocationService {
|
|
|
262
288
|
};
|
|
263
289
|
const failed = (error: CallErrorDetails): InvocationOutcome<T> => {
|
|
264
290
|
const diagnostics = timing();
|
|
291
|
+
const target = resolved ?? activityTarget;
|
|
292
|
+
const details =
|
|
293
|
+
error.code === "auth_required" && target
|
|
294
|
+
? {
|
|
295
|
+
...error,
|
|
296
|
+
connector: target.connector.id,
|
|
297
|
+
operation: `${target.connector.id}.${target.toolName}`,
|
|
298
|
+
recovery: recoveryMode(
|
|
299
|
+
this.registry,
|
|
300
|
+
target.connector,
|
|
301
|
+
this.catalog.baseUrl,
|
|
302
|
+
),
|
|
303
|
+
nextAction: {
|
|
304
|
+
tool: "authorize_connector" as const,
|
|
305
|
+
arguments: { connector: target.connector.id },
|
|
306
|
+
operatorHandoff:
|
|
307
|
+
"Give the URL and instructions it returns to the operator.",
|
|
308
|
+
},
|
|
309
|
+
retry:
|
|
310
|
+
`Retry ${target.connector.id}.${target.toolName} after ` +
|
|
311
|
+
"the operator completes recovery.",
|
|
312
|
+
}
|
|
313
|
+
: error;
|
|
265
314
|
record(
|
|
266
|
-
|
|
315
|
+
details.code === "timeout"
|
|
267
316
|
? "timeout"
|
|
268
|
-
:
|
|
317
|
+
: details.code === "cancelled"
|
|
269
318
|
? "cancelled"
|
|
270
319
|
: "error",
|
|
271
|
-
|
|
320
|
+
details.code,
|
|
272
321
|
);
|
|
273
322
|
return {
|
|
274
323
|
ok: false,
|
|
@@ -276,7 +325,7 @@ export class InvocationService {
|
|
|
276
325
|
attempts,
|
|
277
326
|
timing: diagnostics,
|
|
278
327
|
...(resolved ? { resolved } : {}),
|
|
279
|
-
error,
|
|
328
|
+
error: details,
|
|
280
329
|
};
|
|
281
330
|
};
|
|
282
331
|
|
|
@@ -299,12 +348,9 @@ export class InvocationService {
|
|
|
299
348
|
// against (a revoked downstream grant, say) still reads clean from the
|
|
300
349
|
// cheap `list_connectors({ probe: false })` signal.
|
|
301
350
|
//
|
|
302
|
-
// Recorded HERE rather than inside the registry's catalog fetch
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
// which `Registry.refreshTools` could not reach. A cache hit that
|
|
306
|
-
// avoids a live listTools call records nothing either way — it is not
|
|
307
|
-
// evidence of health, and success stays what it has always been: an
|
|
351
|
+
// Recorded HERE rather than inside the registry's catalog fetch because
|
|
352
|
+
// a cache hit that avoids a live listTools call records nothing — it is
|
|
353
|
+
// not evidence of health. Success stays what it has always been: an
|
|
308
354
|
// actual downstream call that returned.
|
|
309
355
|
this.registry.recordFailure(
|
|
310
356
|
resolution.connector.id,
|
|
@@ -383,6 +429,13 @@ export class InvocationService {
|
|
|
383
429
|
: {}),
|
|
384
430
|
},
|
|
385
431
|
);
|
|
432
|
+
if (resolved.connector.credential && !connectorContext.credential) {
|
|
433
|
+
throw new ConnectorCallError(
|
|
434
|
+
"auth_required",
|
|
435
|
+
"Operator-managed credential storage is not configured. Call " +
|
|
436
|
+
`authorize_connector({ connector: "${resolved.connector.id}" }).`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
386
439
|
let rejectCancelled!: (reason: unknown) => void;
|
|
387
440
|
const cancelled = controller
|
|
388
441
|
? new Promise<never>((_, reject) => {
|
package/src/meta-tools.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
boundedDiscoveryText,
|
|
9
9
|
CatalogService,
|
|
10
|
+
DEFAULT_SEARCH_LIMIT,
|
|
10
11
|
DiscoveryPolicyError,
|
|
11
12
|
groupedSearchResult,
|
|
12
13
|
MAX_DESCRIBE_ADDRESSES,
|
|
@@ -48,7 +49,6 @@ import {
|
|
|
48
49
|
normalizeTimeoutMs,
|
|
49
50
|
withAbortableTimeout,
|
|
50
51
|
} from "./timeout.js";
|
|
51
|
-
import { credentialVerdictApplies } from "./credential-health.js";
|
|
52
52
|
import type { ConnectorStatus, KVStorage } from "./types.js";
|
|
53
53
|
|
|
54
54
|
export {
|
|
@@ -56,7 +56,6 @@ export {
|
|
|
56
56
|
MAX_DISCOVERY_RESULT_BYTES,
|
|
57
57
|
MAX_RETRY_BACKOFF_MS,
|
|
58
58
|
MAX_SEARCH_LIMIT,
|
|
59
|
-
DiscoveryPolicyError,
|
|
60
59
|
retryBackoffMs,
|
|
61
60
|
};
|
|
62
61
|
|
|
@@ -77,7 +76,7 @@ const dec = new TextDecoder();
|
|
|
77
76
|
|
|
78
77
|
export function jsonResult(obj: unknown): ToolResult {
|
|
79
78
|
return {
|
|
80
|
-
content: [{ type: "text", text: JSON.stringify(obj
|
|
79
|
+
content: [{ type: "text", text: JSON.stringify(obj) }],
|
|
81
80
|
...(obj !== null && typeof obj === "object" && !Array.isArray(obj)
|
|
82
81
|
? { structuredContent: obj as Record<string, unknown> }
|
|
83
82
|
: {}),
|
|
@@ -108,8 +107,12 @@ function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
|
|
|
108
107
|
return result;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
function discoveryResult(
|
|
110
|
+
async function discoveryResult(
|
|
111
|
+
operation: () => unknown | Promise<unknown>,
|
|
112
|
+
hint: string,
|
|
113
|
+
): Promise<ToolResult> {
|
|
112
114
|
try {
|
|
115
|
+
const value = await operation();
|
|
113
116
|
const text = boundedDiscoveryText(value, hint);
|
|
114
117
|
return {
|
|
115
118
|
content: [{ type: "text", text }],
|
|
@@ -255,7 +258,7 @@ function applyFieldsToContent(
|
|
|
255
258
|
} catch {
|
|
256
259
|
return b;
|
|
257
260
|
}
|
|
258
|
-
return { ...b, text: JSON.stringify(applyFields(parsed, fields)
|
|
261
|
+
return { ...b, text: JSON.stringify(applyFields(parsed, fields)) };
|
|
259
262
|
});
|
|
260
263
|
}
|
|
261
264
|
|
|
@@ -277,7 +280,7 @@ function applyFieldsToContent(
|
|
|
277
280
|
* at all (a BigInt) still throws, as before, and is reported as a failure.
|
|
278
281
|
*/
|
|
279
282
|
function serializeResultText(value: unknown): string {
|
|
280
|
-
const serialized = JSON.stringify(value
|
|
283
|
+
const serialized = JSON.stringify(value);
|
|
281
284
|
return serialized === undefined ? String(value) : serialized;
|
|
282
285
|
}
|
|
283
286
|
|
|
@@ -391,7 +394,7 @@ async function guardContent(
|
|
|
391
394
|
): Promise<ToolResult> {
|
|
392
395
|
let text: string;
|
|
393
396
|
try {
|
|
394
|
-
text = JSON.stringify(content
|
|
397
|
+
text = JSON.stringify(content);
|
|
395
398
|
} catch {
|
|
396
399
|
// A block carrying a BigInt or a cycle cannot be serialized, so it cannot
|
|
397
400
|
// be measured, stashed, or paged either — there is nothing this guard could
|
|
@@ -453,15 +456,7 @@ export interface GetResultArgs {
|
|
|
453
456
|
/** Page size in bytes; a whole number >= 1. Defaults to the deployment cap. */
|
|
454
457
|
maxBytes?: number;
|
|
455
458
|
}
|
|
456
|
-
export
|
|
457
|
-
address: string;
|
|
458
|
-
args?: Record<string, unknown>;
|
|
459
|
-
fields?: string[];
|
|
460
|
-
resultMode?: ResultMode;
|
|
461
|
-
timeoutMs?: number;
|
|
462
|
-
maxRetries?: number;
|
|
463
|
-
diagnostics?: boolean;
|
|
464
|
-
}
|
|
459
|
+
export type BatchCall = CallArgs;
|
|
465
460
|
export interface BatchArgs {
|
|
466
461
|
calls: BatchCall[];
|
|
467
462
|
resultMode?: ResultMode;
|
|
@@ -614,17 +609,22 @@ export function createMetaTools(
|
|
|
614
609
|
},
|
|
615
610
|
);
|
|
616
611
|
if (!outcome.ok) {
|
|
612
|
+
const failedResult =
|
|
613
|
+
outcome.error.code === "auth_required" ||
|
|
614
|
+
call.resultMode === "value"
|
|
615
|
+
? jsonResult({
|
|
616
|
+
ok: false,
|
|
617
|
+
error: outcome.error,
|
|
618
|
+
durationMs: outcome.durationMs,
|
|
619
|
+
attempts: outcome.attempts,
|
|
620
|
+
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
621
|
+
})
|
|
622
|
+
: errorResult(outcome.error.message);
|
|
623
|
+
if (outcome.error.code === "auth_required") {
|
|
624
|
+
failedResult.isError = true;
|
|
625
|
+
}
|
|
617
626
|
return {
|
|
618
|
-
toolResult:
|
|
619
|
-
call.resultMode === "value"
|
|
620
|
-
? jsonResult({
|
|
621
|
-
ok: false,
|
|
622
|
-
error: outcome.error,
|
|
623
|
-
durationMs: outcome.durationMs,
|
|
624
|
-
attempts: outcome.attempts,
|
|
625
|
-
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
626
|
-
})
|
|
627
|
-
: errorResult(outcome.error.message),
|
|
627
|
+
toolResult: failedResult,
|
|
628
628
|
durationMs: outcome.durationMs,
|
|
629
629
|
attempts: outcome.attempts,
|
|
630
630
|
timing: outcome.timing,
|
|
@@ -683,11 +683,13 @@ export function createMetaTools(
|
|
|
683
683
|
const inspect = async (c: (typeof connectors)[number]) => {
|
|
684
684
|
const statusStarted = Date.now();
|
|
685
685
|
const observed = registry.healthFor(c.id);
|
|
686
|
-
const
|
|
686
|
+
const drift = await registry.credentialDriftFor(c.id);
|
|
687
687
|
let status:
|
|
688
688
|
| ConnectorStatus
|
|
689
689
|
| { state: "ok" | "error" | "unknown"; message?: string };
|
|
690
|
-
if (
|
|
690
|
+
if (drift) {
|
|
691
|
+
status = { state: "auth_required", message: drift };
|
|
692
|
+
} else if (probe) {
|
|
691
693
|
try {
|
|
692
694
|
status = await withProbeDeadline(
|
|
693
695
|
`list_connectors probe of "${c.id}"`,
|
|
@@ -700,32 +702,7 @@ export function createMetaTools(
|
|
|
700
702
|
// hanging the whole list_connectors call.
|
|
701
703
|
status = { state: "error", message: msg(err) };
|
|
702
704
|
}
|
|
703
|
-
} else if (
|
|
704
|
-
verdict &&
|
|
705
|
-
// Deployment-wide, deliberately, like `hasObservedSuccess` beside
|
|
706
|
-
// it: a sibling toolkit's successful call proves the shared
|
|
707
|
-
// credential works, and a verdict retired for one view but not
|
|
708
|
-
// another would make the same connector read differently per scope
|
|
709
|
-
// for a reason that has nothing to do with scope.
|
|
710
|
-
credentialVerdictApplies(verdict, registry.observedSuccessAt(c.id))
|
|
711
|
-
) {
|
|
712
|
-
// The proactive layer (issue #24): a liveness check already found
|
|
713
|
-
// the stored credential dead, so say so on the cheap path instead of
|
|
714
|
-
// waiting for an agent's real call to discover it. Only while it is
|
|
715
|
-
// the freshest evidence — a successful call since then retires it.
|
|
716
|
-
status = {
|
|
717
|
-
state: verdict.state,
|
|
718
|
-
...(verdict.message ? { message: verdict.message } : {}),
|
|
719
|
-
...(verdict.authorizationUrl
|
|
720
|
-
? { authorizationUrl: verdict.authorizationUrl }
|
|
721
|
-
: {}),
|
|
722
|
-
};
|
|
723
705
|
} else {
|
|
724
|
-
// "error" comes from THIS view's own observations — a sibling
|
|
725
|
-
// toolkit's failure is not this session's experience — while
|
|
726
|
-
// ok/unknown may lean on the deployment-wide success signal, since
|
|
727
|
-
// "the connector answers at all" is a fact about the connector.
|
|
728
|
-
// Unscoped, the two are the same log, so this is unchanged there.
|
|
729
706
|
const derived =
|
|
730
707
|
observed?.consecutiveFailures && observed.consecutiveFailures > 0
|
|
731
708
|
? ("error" as const)
|
|
@@ -733,43 +710,13 @@ export function createMetaTools(
|
|
|
733
710
|
? ("ok" as const)
|
|
734
711
|
: ("unknown" as const);
|
|
735
712
|
status = {
|
|
736
|
-
|
|
737
|
-
// been called yet, but the credential was verified, which is how
|
|
738
|
-
// re-authorization shows up here as ok rather than as an absence
|
|
739
|
-
// of evidence. It never DOWNgrades an observed failure: a real
|
|
740
|
-
// call that failed is stronger evidence than a background check.
|
|
741
|
-
state:
|
|
742
|
-
derived === "unknown" && verdict?.state === "ok"
|
|
743
|
-
? ("ok" as const)
|
|
744
|
-
: derived,
|
|
713
|
+
state: derived,
|
|
745
714
|
...(observed?.lastError ? { message: observed.lastError } : {}),
|
|
746
715
|
};
|
|
747
716
|
}
|
|
748
|
-
// Stamped
|
|
749
|
-
//
|
|
750
|
-
// report a verdict older than it is, and would lose the race against a
|
|
751
|
-
// real call that succeeded WHILE it ran (that success must retire the
|
|
752
|
-
// verdict, and only an honest timestamp says so).
|
|
717
|
+
// Stamped after any live probe so the response reports when its
|
|
718
|
+
// observation completed, not when a potentially slow request began.
|
|
753
719
|
const checkedAt = new Date().toISOString();
|
|
754
|
-
// A live status probe IS a liveness observation of the stored
|
|
755
|
-
// credential, so it updates the same verdict a background check
|
|
756
|
-
// writes: the cached read afterwards agrees with what the operator
|
|
757
|
-
// just saw, and they are not swept again moments later. Recorded from
|
|
758
|
-
// the STATUS phase only, and only when the connector actually answered
|
|
759
|
-
// — a catalog refresh below is not a credential check (the sweep never
|
|
760
|
-
// fetches one), it is already counted in the health log, and letting
|
|
761
|
-
// its failure land here would spend the freshness budget on it. The
|
|
762
|
-
// registry ignores this for connectors storing no credential of ours.
|
|
763
|
-
if (probe && (status.state === "ok" || status.state === "auth_required")) {
|
|
764
|
-
await registry.recordCredentialHealth(c.id, {
|
|
765
|
-
state: status.state,
|
|
766
|
-
checkedAt,
|
|
767
|
-
...(status.message ? { message: status.message } : {}),
|
|
768
|
-
...("authorizationUrl" in status && status.authorizationUrl
|
|
769
|
-
? { authorizationUrl: status.authorizationUrl }
|
|
770
|
-
: {}),
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
720
|
let tools = registry.peekTools(c.id);
|
|
774
721
|
// An auth_required status may have just started OAuth. A second
|
|
775
722
|
// listTools probe would overwrite its state/verifier while returning
|
|
@@ -803,15 +750,6 @@ export function createMetaTools(
|
|
|
803
750
|
state: "auth_required" as const,
|
|
804
751
|
message: details.message,
|
|
805
752
|
};
|
|
806
|
-
await registry.recordCredentialHealth(c.id, {
|
|
807
|
-
state: "auth_required",
|
|
808
|
-
checkedAt,
|
|
809
|
-
...(status.message && { message: status.message }),
|
|
810
|
-
...("authorizationUrl" in status &&
|
|
811
|
-
status.authorizationUrl
|
|
812
|
-
? { authorizationUrl: status.authorizationUrl }
|
|
813
|
-
: {}),
|
|
814
|
-
});
|
|
815
753
|
} else {
|
|
816
754
|
status = { state: "error" as const, message: msg(err) };
|
|
817
755
|
}
|
|
@@ -820,9 +758,6 @@ export function createMetaTools(
|
|
|
820
758
|
}
|
|
821
759
|
const latencyMs = Date.now() - statusStarted;
|
|
822
760
|
const latestObserved = registry.healthFor(c.id);
|
|
823
|
-
const credentialCheck = probe
|
|
824
|
-
? await registry.credentialHealthFor(c.id)
|
|
825
|
-
: verdict;
|
|
826
761
|
return {
|
|
827
762
|
id: c.id,
|
|
828
763
|
...(c.title ? { title: c.title } : {}),
|
|
@@ -833,7 +768,6 @@ export function createMetaTools(
|
|
|
833
768
|
latencyMs,
|
|
834
769
|
probe,
|
|
835
770
|
...(latestObserved ?? observed),
|
|
836
|
-
...(credentialCheck ? { credentialCheck } : {}),
|
|
837
771
|
...("authorizationUrl" in status &&
|
|
838
772
|
status.authorizationUrl && {
|
|
839
773
|
authorizationUrl: status.authorizationUrl,
|
|
@@ -866,31 +800,18 @@ export function createMetaTools(
|
|
|
866
800
|
},
|
|
867
801
|
|
|
868
802
|
async searchTools(args: SearchArgs): Promise<ToolResult> {
|
|
869
|
-
|
|
870
|
-
|
|
803
|
+
return discoveryResult(
|
|
804
|
+
async () =>
|
|
871
805
|
groupedSearchResult(await catalog.search(args)),
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
} catch (err) {
|
|
875
|
-
if (err instanceof DiscoveryPolicyError) {
|
|
876
|
-
return discoveryErrorResult(err);
|
|
877
|
-
}
|
|
878
|
-
throw err;
|
|
879
|
-
}
|
|
806
|
+
"Request a smaller limit, omit fullDescriptions, or use compact schemas.",
|
|
807
|
+
);
|
|
880
808
|
},
|
|
881
809
|
|
|
882
810
|
async describeTools(args: DescribeArgs): Promise<ToolResult> {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
);
|
|
888
|
-
} catch (err) {
|
|
889
|
-
if (err instanceof DiscoveryPolicyError) {
|
|
890
|
-
return discoveryErrorResult(err);
|
|
891
|
-
}
|
|
892
|
-
throw err;
|
|
893
|
-
}
|
|
811
|
+
return discoveryResult(
|
|
812
|
+
async () => ({ tools: await catalog.describe(args) }),
|
|
813
|
+
'Split the address list or use format: "compact".',
|
|
814
|
+
);
|
|
894
815
|
},
|
|
895
816
|
|
|
896
817
|
async callTool(args: CallArgs): Promise<ToolResult> {
|
|
@@ -1076,6 +997,21 @@ export function createMetaTools(
|
|
|
1076
997
|
...(details.retryAfterMs !== undefined
|
|
1077
998
|
? { retryAfterMs: details.retryAfterMs }
|
|
1078
999
|
: {}),
|
|
1000
|
+
...(details.connector !== undefined
|
|
1001
|
+
? { connector: batchSummaryString(details.connector) }
|
|
1002
|
+
: {}),
|
|
1003
|
+
...(details.operation !== undefined
|
|
1004
|
+
? { operation: batchSummaryString(details.operation) }
|
|
1005
|
+
: {}),
|
|
1006
|
+
...(details.recovery !== undefined
|
|
1007
|
+
? { recovery: details.recovery }
|
|
1008
|
+
: {}),
|
|
1009
|
+
...(details.nextAction !== undefined
|
|
1010
|
+
? { nextAction: details.nextAction }
|
|
1011
|
+
: {}),
|
|
1012
|
+
...(details.retry !== undefined
|
|
1013
|
+
? { retry: batchSummaryString(details.retry) }
|
|
1014
|
+
: {}),
|
|
1079
1015
|
},
|
|
1080
1016
|
};
|
|
1081
1017
|
}),
|
|
@@ -1090,9 +1026,55 @@ export function createMetaTools(
|
|
|
1090
1026
|
return errorResult(`Unknown connector "${args.connector}"`);
|
|
1091
1027
|
}
|
|
1092
1028
|
if (!connector.startAuth) {
|
|
1093
|
-
|
|
1094
|
-
|
|
1029
|
+
if (!connector.credential) {
|
|
1030
|
+
return jsonResult({
|
|
1031
|
+
connector: connector.id,
|
|
1032
|
+
recovery: "unavailable",
|
|
1033
|
+
message:
|
|
1034
|
+
`Connector "${connector.id}" declares neither downstream OAuth ` +
|
|
1035
|
+
"nor an operator-managed credential slot. Update the connector " +
|
|
1036
|
+
"or deployment configuration before retrying.",
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
const ctx = registry.contextFor(
|
|
1040
|
+
connector.id,
|
|
1041
|
+
baseUrl,
|
|
1042
|
+
requestScope,
|
|
1095
1043
|
);
|
|
1044
|
+
if (!ctx.credential) {
|
|
1045
|
+
return jsonResult({
|
|
1046
|
+
connector: connector.id,
|
|
1047
|
+
recovery: "unavailable",
|
|
1048
|
+
message:
|
|
1049
|
+
"Credential storage is not configured. Configure " +
|
|
1050
|
+
"credentials.encryptionKey, redeploy, then call " +
|
|
1051
|
+
"authorize_connector again.",
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
const fields = connector.credential.fields?.map((field) => ({
|
|
1055
|
+
name: field.name,
|
|
1056
|
+
guidance: field.description ?? field.label,
|
|
1057
|
+
})) ?? [
|
|
1058
|
+
{
|
|
1059
|
+
name: "value",
|
|
1060
|
+
guidance:
|
|
1061
|
+
connector.credential.description ??
|
|
1062
|
+
connector.credential.label,
|
|
1063
|
+
},
|
|
1064
|
+
];
|
|
1065
|
+
return jsonResult({
|
|
1066
|
+
connector: connector.id,
|
|
1067
|
+
recovery: "operator_config",
|
|
1068
|
+
credential: {
|
|
1069
|
+
label: connector.credential.label,
|
|
1070
|
+
fields,
|
|
1071
|
+
},
|
|
1072
|
+
operatorUrl: new URL("/credentials", baseUrl).toString(),
|
|
1073
|
+
instructions:
|
|
1074
|
+
"Have the operator open operatorUrl, set and test the credential, " +
|
|
1075
|
+
"then retry the original call. No redeploy is needed. Credential " +
|
|
1076
|
+
"mutation requires a Clerk-authenticated operator.",
|
|
1077
|
+
});
|
|
1096
1078
|
}
|
|
1097
1079
|
const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
|
|
1098
1080
|
try {
|
|
@@ -1100,17 +1082,6 @@ export function createMetaTools(
|
|
|
1100
1082
|
ctx,
|
|
1101
1083
|
args.force !== undefined ? { force: args.force } : {},
|
|
1102
1084
|
);
|
|
1103
|
-
// startAuth just spoke to the downstream about this exact credential, so
|
|
1104
|
-
// its answer replaces any older liveness verdict — including the stale
|
|
1105
|
-
// `auth_required` that sent the agent here, once it reports ok.
|
|
1106
|
-
await registry.recordCredentialHealth(connector.id, {
|
|
1107
|
-
state: status.state,
|
|
1108
|
-
checkedAt: new Date().toISOString(),
|
|
1109
|
-
...(status.message ? { message: status.message } : {}),
|
|
1110
|
-
...(status.authorizationUrl
|
|
1111
|
-
? { authorizationUrl: status.authorizationUrl }
|
|
1112
|
-
: {}),
|
|
1113
|
-
});
|
|
1114
1085
|
if (status.state === "auth_required" && !status.authorizationUrl) {
|
|
1115
1086
|
// auth_required with nothing to open is a dead end for the operator.
|
|
1116
1087
|
return errorResult(
|
|
@@ -1119,6 +1090,7 @@ export function createMetaTools(
|
|
|
1119
1090
|
}
|
|
1120
1091
|
return jsonResult({
|
|
1121
1092
|
connector: connector.id,
|
|
1093
|
+
recovery: "oauth",
|
|
1122
1094
|
status: status.state,
|
|
1123
1095
|
...(status.authorizationUrl
|
|
1124
1096
|
? {
|
|
@@ -1142,7 +1114,7 @@ export function createMetaTools(
|
|
|
1142
1114
|
|
|
1143
1115
|
const LIST_DESC =
|
|
1144
1116
|
"List connectors with status, cached tool count, and recent real-call health. Use probe=false for a fast inventory; use probe=true (default) only to diagnose live health or authorization.";
|
|
1145
|
-
const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all.
|
|
1117
|
+
const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all. The default page has ${DEFAULT_SEARCH_LIMIT} tools; explicit limit can request up to ${MAX_SEARCH_LIMIT}. includeSchemas="compact" usually removes the describe_tools round trip.`;
|
|
1146
1118
|
const DESCRIBE_DESC = `Inspect up to ${MAX_DESCRIBE_ADDRESSES} known tool addresses when search_tools did not include a sufficient schema. Returns descriptions, input/output schemas, and behavior annotations; format "compact" is the default.`;
|
|
1147
1119
|
const CALL_DESC =
|
|
1148
1120
|
'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths, resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
@@ -1153,7 +1125,7 @@ const GET_RESULT_DESC =
|
|
|
1153
1125
|
const BATCH_DESC =
|
|
1154
1126
|
"Use for 2–10 independent tools explicitly annotated readOnlyHint: true. Calls run in parallel with shared request-scoped clients; use execute_code when available instead for dependencies or in-sandbox reduction. Unannotated, write-capable, and destructive tools are refused. Batch timeout, safe retry, result mode, and diagnostics defaults may be overridden per call. An oversized final envelope returns ordered outcome summaries plus a get_result page handle.";
|
|
1155
1127
|
const AUTHORIZE_DESC =
|
|
1156
|
-
"Use after
|
|
1128
|
+
"Use after auth_required. Returns an OAuth or operator-credential handoff, or reports required deployment configuration. force=true restarts OAuth only; this tool never accepts credentials.";
|
|
1157
1129
|
const SKILLS_DESC =
|
|
1158
1130
|
'List or fetch concise guidance for choosing among Connecta meta-tools. Call skills({ name: "usage" }) once when the routing workflow is unfamiliar; do not refetch it in the same task.';
|
|
1159
1131
|
|
|
@@ -1163,10 +1135,7 @@ const SKILLS_DESC =
|
|
|
1163
1135
|
* so a deployment with no guides gets every base description unchanged rather
|
|
1164
1136
|
* than paying for text about a feature it does not use.
|
|
1165
1137
|
*
|
|
1166
|
-
* Registration is per connection and reads the
|
|
1167
|
-
* so under a toolkit these sentences reflect the SCOPED connector set: a scoped
|
|
1168
|
-
* session whose connectors carry no guides sees the base descriptions, and
|
|
1169
|
-
* never learns from a tool description that guides exist out of scope.
|
|
1138
|
+
* Registration is per connection and reads the configured connector set.
|
|
1170
1139
|
*/
|
|
1171
1140
|
const GUIDE_NOTES = {
|
|
1172
1141
|
skills:
|
|
@@ -1208,6 +1177,16 @@ const READ_ONLY_LOCAL = {
|
|
|
1208
1177
|
openWorldHint: false,
|
|
1209
1178
|
} as const;
|
|
1210
1179
|
|
|
1180
|
+
const CALL_INPUT_SCHEMA = {
|
|
1181
|
+
address: z.string(),
|
|
1182
|
+
args: z.record(z.string(), z.unknown()).optional(),
|
|
1183
|
+
fields: z.array(z.string()).optional(),
|
|
1184
|
+
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1185
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
1186
|
+
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1187
|
+
diagnostics: z.boolean().optional(),
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1211
1190
|
/** Register the nine meta-tools onto an McpServer instance. */
|
|
1212
1191
|
export function registerMetaTools(
|
|
1213
1192
|
server: McpServer,
|
|
@@ -1244,12 +1223,7 @@ export function registerMetaTools(
|
|
|
1244
1223
|
{
|
|
1245
1224
|
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
1246
1225
|
inputSchema: { name: z.string().optional() },
|
|
1247
|
-
annotations:
|
|
1248
|
-
readOnlyHint: true,
|
|
1249
|
-
destructiveHint: false,
|
|
1250
|
-
idempotentHint: true,
|
|
1251
|
-
openWorldHint: false,
|
|
1252
|
-
},
|
|
1226
|
+
annotations: READ_ONLY_LOCAL,
|
|
1253
1227
|
},
|
|
1254
1228
|
async (args) => mt.skills(args as SkillArgs),
|
|
1255
1229
|
);
|
|
@@ -1299,15 +1273,7 @@ export function registerMetaTools(
|
|
|
1299
1273
|
"call_tool",
|
|
1300
1274
|
{
|
|
1301
1275
|
description: CALL_DESC,
|
|
1302
|
-
inputSchema:
|
|
1303
|
-
address: z.string(),
|
|
1304
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1305
|
-
fields: z.array(z.string()).optional(),
|
|
1306
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1307
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1308
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1309
|
-
diagnostics: z.boolean().optional(),
|
|
1310
|
-
},
|
|
1276
|
+
inputSchema: CALL_INPUT_SCHEMA,
|
|
1311
1277
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
1312
1278
|
// anything else is refused and routed to call_destructive_tool.
|
|
1313
1279
|
annotations: READ_ONLY_REMOTE,
|
|
@@ -1319,15 +1285,7 @@ export function registerMetaTools(
|
|
|
1319
1285
|
"call_destructive_tool",
|
|
1320
1286
|
{
|
|
1321
1287
|
description: CALL_DESTRUCTIVE_DESC,
|
|
1322
|
-
inputSchema:
|
|
1323
|
-
address: z.string(),
|
|
1324
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1325
|
-
fields: z.array(z.string()).optional(),
|
|
1326
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1327
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1328
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1329
|
-
diagnostics: z.boolean().optional(),
|
|
1330
|
-
},
|
|
1288
|
+
inputSchema: CALL_INPUT_SCHEMA,
|
|
1331
1289
|
annotations: {
|
|
1332
1290
|
destructiveHint: true,
|
|
1333
1291
|
readOnlyHint: false,
|
|
@@ -1380,17 +1338,7 @@ export function registerMetaTools(
|
|
|
1380
1338
|
description: BATCH_DESC,
|
|
1381
1339
|
inputSchema: {
|
|
1382
1340
|
calls: z
|
|
1383
|
-
.array(
|
|
1384
|
-
z.object({
|
|
1385
|
-
address: z.string(),
|
|
1386
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1387
|
-
fields: z.array(z.string()).optional(),
|
|
1388
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1389
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1390
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1391
|
-
diagnostics: z.boolean().optional(),
|
|
1392
|
-
}),
|
|
1393
|
-
)
|
|
1341
|
+
.array(z.object(CALL_INPUT_SCHEMA))
|
|
1394
1342
|
.min(1)
|
|
1395
1343
|
.max(10),
|
|
1396
1344
|
resultMode: z.enum(["mcp", "value"]).optional(),
|