@zackbart/connecta 0.37.1 → 0.39.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 +94 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/agent-routes.js +231 -0
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2516
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/native.js +27 -0
- package/dist/auth/oauth-grant.js +24 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +9 -3
- package/dist/authorization-handoff.js +11 -5
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +130 -43
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +285 -48
- package/dist/catalog.js +18 -2
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +57 -25
- package/dist/config.js +30 -77
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +11 -7
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +671 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +469 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +544 -0
- package/dist/connectors/remote-mcp.js +61 -2898
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +4 -2
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +172 -125
- package/dist/executor-result.js +5 -2
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +63 -20
- package/dist/guest-types.d.ts +24 -10
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +7 -6
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +4 -3
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +33 -5
- package/dist/http/safety.js +9 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +45 -30
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +21 -6
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +50 -35
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +6 -2
- package/dist/oauth-handoff.js +2 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +136 -14
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -49
- package/dist/operator-ui/contract.d.ts +12 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +49 -53
- package/dist/providers/_shared/rest/value-safety.js +106 -28
- package/dist/providers/breeze/api.js +9 -8
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +7 -7
- package/dist/providers/ccb/api.js +3 -7
- package/dist/providers/ccb/index.js +7 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +10 -3
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +4 -14
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +23 -41
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +11 -16
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +12 -26
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +8 -4
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +5 -2
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +14 -23
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +19 -26
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +5 -8
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +2 -7
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +6 -4
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +11 -5
- package/dist/registry.js +46 -20
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +130 -61
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +34 -17
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +6 -5
- package/dist/routes/shared.d.ts +13 -8
- package/dist/routes/shared.js +15 -9
- package/dist/routes/ui-config.js +3 -3
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +17 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +8 -3
- package/dist/runtime/native-storage.js +9 -0
- package/dist/runtime/request-reads.js +182 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/runtime/storage.js +12 -0
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +44 -54
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +69 -128
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +27 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +13 -14
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +33 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +18 -37
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +19 -55
- package/documentation/architecture.md +436 -515
- package/documentation/auth.md +229 -88
- package/documentation/code-mode.md +201 -94
- package/documentation/deploying.md +25 -43
- package/documentation/integrating.md +18 -19
- package/documentation/meta-tools.md +225 -51
- package/documentation/migration-0.33.md +10 -6
- package/documentation/migration-0.34.md +5 -5
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +20 -120
- package/documentation/operator-ui.md +108 -16
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +14 -16
- package/examples/worker/README.md +24 -118
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +9 -31
- package/guides/how-it-works.md +6 -4
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { retryAfterMs } from "../http-policy.js";
|
|
2
|
-
import { type ProviderRequest } from "../provider-call.js";
|
|
3
2
|
import type { ConnectorContext } from "../types.js";
|
|
4
3
|
/** The methods a hand-written provider surface actually uses. */
|
|
5
4
|
type GuardedMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -182,11 +181,6 @@ type GuardedFetcher = (url: string, init: RequestInit, ctx: ConnectorContext) =>
|
|
|
182
181
|
export declare function oauthBearer(provider: string): Pick<GuardedFetchOptions, "authenticate" | "fetch">;
|
|
183
182
|
/** Send one guarded request and map its response with provider knowledge. */
|
|
184
183
|
export type GuardedTransport = <T>(request: GuardedRequest, ctx: ConnectorContext, map: GuardedResponseMapper<T>) => Promise<T>;
|
|
185
|
-
/**
|
|
186
|
-
* Test-only legacy response lookup for renewal-provenance fixtures outside
|
|
187
|
-
* the kit. Production renewal uses the kit ledger. Undefined for other objects.
|
|
188
|
-
*/
|
|
189
|
-
export declare function answeredRequest(response: object): ProviderRequest | undefined;
|
|
190
184
|
/**
|
|
191
185
|
* Build the transport a connector sends every request through. `maxResponseBytes`
|
|
192
186
|
* is required rather than defaulted: what counts as an absurd response is a fact
|
|
@@ -3,7 +3,7 @@ export { retryAfterMs } from "../http-policy.js";
|
|
|
3
3
|
import { callerSignal, duringViewCall, settleOnClose, viewSignal } from "../runtime/call.js";
|
|
4
4
|
/**
|
|
5
5
|
* The safety machinery every hand-written HTTP surface needs, extracted once
|
|
6
|
-
|
|
6
|
+
*: path confinement, encoded query and framed bodies, per-request
|
|
7
7
|
* credential headers applied last, a refused redirect, a bounded read, and
|
|
8
8
|
* network-failure normalization. It owns no vendor-specific meaning and never
|
|
9
9
|
* invents an auth scheme, because the same 403 means an ungranted
|
|
@@ -280,15 +280,6 @@ async function jsonResult(read) {
|
|
|
280
280
|
return { parseError: cause };
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
-
// Each response a mapper reads, and the ledger entry of the request it answered.
|
|
284
|
-
const answeredRequests = new WeakMap();
|
|
285
|
-
/**
|
|
286
|
-
* Test-only legacy response lookup for renewal-provenance fixtures outside
|
|
287
|
-
* the kit. Production renewal uses the kit ledger. Undefined for other objects.
|
|
288
|
-
*/
|
|
289
|
-
export function answeredRequest(response) {
|
|
290
|
-
return answeredRequests.get(response);
|
|
291
|
-
}
|
|
292
283
|
/**
|
|
293
284
|
* Build the transport a connector sends every request through. `maxResponseBytes`
|
|
294
285
|
* is required rather than defaulted: what counts as an absurd response is a fact
|
|
@@ -463,7 +454,6 @@ export function guardedFetch(options) {
|
|
|
463
454
|
throw oversized(provider, limit, `a declared ${declared} bytes`);
|
|
464
455
|
}
|
|
465
456
|
const answer = boundedResponse(provider, response, limit, signal);
|
|
466
|
-
answeredRequests.set(answer, dispatch);
|
|
467
457
|
const abort = () => {
|
|
468
458
|
void response?.body?.cancel().catch(() => { });
|
|
469
459
|
};
|
|
@@ -4,7 +4,8 @@ import { isSpecType } from "@modelcontextprotocol/client";
|
|
|
4
4
|
import { failureRecord, logFailure } from "../operator-record.js";
|
|
5
5
|
import { sentSecretsForRequest } from "../sent-secrets.js";
|
|
6
6
|
import { assertDownstreamOutputSafe } from "../downstream-input-context.js";
|
|
7
|
-
import { negotiationKeys, NEGOTIATION_TTL_SECONDS } from "../storage/keys.js";
|
|
7
|
+
import { LEGACY_NEGOTIATION_TTL_SECONDS, negotiationKeys, NEGOTIATION_TTL_SECONDS } from "../storage/keys.js";
|
|
8
|
+
import { viewReads } from "../runtime/call.js";
|
|
8
9
|
/** Verdicts carry data only. Clients, signals and secrets remain request-local. */
|
|
9
10
|
export function readNegotiation(ctx, digest) {
|
|
10
11
|
return runEdge(authProgram(readNegotiationNative, [ctx, digest]));
|
|
@@ -12,7 +13,11 @@ export function readNegotiation(ctx, digest) {
|
|
|
12
13
|
const readNegotiationNative = /* @__PURE__ */ registerAuth(readNegotiation, (ctx, digest) => authFlow(function* () {
|
|
13
14
|
let prior;
|
|
14
15
|
try {
|
|
15
|
-
|
|
16
|
+
// Memoized per request: a verdict only orders the versions a handshake
|
|
17
|
+
// tries, so one read earlier in this request at worst costs a fallback.
|
|
18
|
+
// This request's own storeNegotiation is a write, which clears the memo.
|
|
19
|
+
const key = negotiationKeys.verdict(digest);
|
|
20
|
+
const raw = (yield viewReads(ctx).read(ctx.storage, key, authStorage(ctx.storage, "get", [key])));
|
|
16
21
|
if (!raw)
|
|
17
22
|
return;
|
|
18
23
|
const value = JSON.parse(raw);
|
|
@@ -40,17 +45,30 @@ const storeNegotiationNative = /* @__PURE__ */ registerAuth(storeNegotiation, (c
|
|
|
40
45
|
const clean = sentSecretsForRequest(ctx.requestScope ?? ctx).redactCredentials(prior);
|
|
41
46
|
if (clean.kind === "modern" && !isSpecType.DiscoverResult(clean.discover))
|
|
42
47
|
return;
|
|
48
|
+
const ttlSeconds = clean.kind === "legacy" ? LEGACY_NEGOTIATION_TTL_SECONDS : NEGOTIATION_TTL_SECONDS;
|
|
43
49
|
try {
|
|
44
50
|
(yield authStorage(ctx.storage, "set", [
|
|
45
51
|
negotiationKeys.verdict(digest),
|
|
46
52
|
JSON.stringify({
|
|
47
53
|
prior: clean,
|
|
48
|
-
expiresAt: Date.now() +
|
|
54
|
+
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
49
55
|
}),
|
|
50
|
-
{ ttlSeconds
|
|
56
|
+
{ ttlSeconds },
|
|
51
57
|
]));
|
|
52
58
|
}
|
|
53
59
|
catch (error) {
|
|
54
60
|
logFailure(ctx.logger, "negotiation cache write failed", failureRecord({}, error));
|
|
55
61
|
}
|
|
56
62
|
}));
|
|
63
|
+
/** Drop a verdict the downstream has contradicted, so the next connect negotiates. */
|
|
64
|
+
export function forgetNegotiation(ctx, digest) {
|
|
65
|
+
return runEdge(authProgram(forgetNegotiationNative, [ctx, digest]));
|
|
66
|
+
}
|
|
67
|
+
const forgetNegotiationNative = /* @__PURE__ */ registerAuth(forgetNegotiation, (ctx, digest) => authFlow(function* () {
|
|
68
|
+
try {
|
|
69
|
+
(yield authStorage(ctx.storage, "delete", [negotiationKeys.verdict(digest)]));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
logFailure(ctx.logger, "negotiation cache write failed", failureRecord({}, error));
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// tools/call: classification, write-once dispatch and the write's outcome
|
|
2
|
+
// when it fails.
|
|
3
|
+
import { authView } from "../../auth/native-call.js";
|
|
4
|
+
import { authFlow, authCall, authInvoke, authMethod, authCatch } from "../../auth/native.js";
|
|
5
|
+
import { ProtocolError, isInputRequiredResult, fromJsonSchema, UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
6
|
+
import { Effect } from "effect";
|
|
7
|
+
import { requestSentSecrets } from "../../sent-secrets.js";
|
|
8
|
+
import { ConnectorCallError } from "../../errors.js";
|
|
9
|
+
import { carryFailureFacts, failureRecord } from "../../operator-record.js";
|
|
10
|
+
import { assertDownstreamOutputSafe } from "../../downstream-input-context.js";
|
|
11
|
+
import { callClassification } from "../../call-classification.js";
|
|
12
|
+
import { sourceClassification } from "../../tool-safety.js";
|
|
13
|
+
import { enterProviderBoundary, knownProviderFailure, providerRequest, vendorRefusal, } from "../../provider-call.js";
|
|
14
|
+
import { providerCallFailure, unsentAfterUnauthorized } from "../../write-outcome.js";
|
|
15
|
+
import { newDispatch, withDispatch } from "./transport.js";
|
|
16
|
+
import { downstreamCallError, hasSdkPayload, ownAbortReason, requiresAuthorization } from "./errors.js";
|
|
17
|
+
/** One connector's callTool operation, before the entry's wrappers. */
|
|
18
|
+
export function remoteMcpCall(rt) {
|
|
19
|
+
const { isOauth, id } = rt;
|
|
20
|
+
const callToolOperation = authMethod((name, args, ctx, options) => authFlow(function* () {
|
|
21
|
+
enterProviderBoundary(ctx, rt.connector);
|
|
22
|
+
let active;
|
|
23
|
+
try {
|
|
24
|
+
return (yield authView(ctx, authMethod(() => authFlow(function* () {
|
|
25
|
+
const state = rt.stateFor(ctx);
|
|
26
|
+
const client = (yield authCatch(authCall(rt.ensureConnected, [ctx, state]), (error) => {
|
|
27
|
+
throw knownProviderFailure(error);
|
|
28
|
+
}));
|
|
29
|
+
if (isOauth)
|
|
30
|
+
(yield authCatch(authCall(rt.renewBeforeDispatch, [ctx, state, client]), (error) => {
|
|
31
|
+
throw knownProviderFailure(error);
|
|
32
|
+
}));
|
|
33
|
+
const definition = options?.definition;
|
|
34
|
+
const facts = definition ?? state.toolDefinitions.get(name) ?? { name };
|
|
35
|
+
const sourceFacts = definition ?? {
|
|
36
|
+
name,
|
|
37
|
+
...(facts.annotations
|
|
38
|
+
? { annotations: facts.annotations }
|
|
39
|
+
: {}),
|
|
40
|
+
};
|
|
41
|
+
const source = sourceClassification(sourceFacts);
|
|
42
|
+
const serverWrite = source === "write";
|
|
43
|
+
const write = serverWrite || (callClassification(ctx) ?? definition?.classification ?? source) === "write";
|
|
44
|
+
const dispatch = newDispatch(write, serverWrite);
|
|
45
|
+
dispatch.request = {
|
|
46
|
+
entry: providerRequest(ctx, !write, `Connector "${id}"'s MCP server`, { method: "POST" }),
|
|
47
|
+
ctx,
|
|
48
|
+
};
|
|
49
|
+
active = { state, client, dispatch, write };
|
|
50
|
+
state.dispatches.set(dispatch, name);
|
|
51
|
+
const toolDefinition = definition
|
|
52
|
+
? {
|
|
53
|
+
...definition,
|
|
54
|
+
inputSchema: (definition.inputSchema ?? { type: "object" }),
|
|
55
|
+
}
|
|
56
|
+
: state.toolDefinitions.get(name);
|
|
57
|
+
if (toolDefinition?.execution?.taskSupport === "required") {
|
|
58
|
+
throw new ConnectorCallError("connector_call_failed", `Tool "${name}" requires task-based execution, which Connecta does not support.`);
|
|
59
|
+
}
|
|
60
|
+
let output;
|
|
61
|
+
const outputError = (code, message) => rt.atMcpBoundary(ctx, new ProtocolError(code, message), "tools/call", client.transport, [ctx.signal]);
|
|
62
|
+
if (toolDefinition?.outputSchema) {
|
|
63
|
+
try {
|
|
64
|
+
output = fromJsonSchema(toolDefinition.outputSchema);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw outputError(-32602, `Tool '${name}' has an invalid outputSchema: schema could not be compiled`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const result = (yield authCatch(authInvoke(client, "callTool", [
|
|
71
|
+
withDispatch({
|
|
72
|
+
name,
|
|
73
|
+
arguments: (args ?? {}),
|
|
74
|
+
...options?.input,
|
|
75
|
+
}, dispatch),
|
|
76
|
+
{
|
|
77
|
+
...rt.requestOptions(ctx),
|
|
78
|
+
allowInputRequired: true,
|
|
79
|
+
...(toolDefinition
|
|
80
|
+
? { toolDefinition: { ...toolDefinition, outputSchema: undefined } }
|
|
81
|
+
: write
|
|
82
|
+
? { toolDefinition: { name, inputSchema: { type: "object" } } }
|
|
83
|
+
: {}),
|
|
84
|
+
},
|
|
85
|
+
]), (err) => {
|
|
86
|
+
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, err);
|
|
87
|
+
throw rt.atMcpBoundary(ctx, err, "tools/call", client.transport, [ctx.signal]);
|
|
88
|
+
}));
|
|
89
|
+
if (isInputRequiredResult(result)) {
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, result);
|
|
93
|
+
if (output && !result.isError) {
|
|
94
|
+
if (result.structuredContent === undefined)
|
|
95
|
+
throw outputError(-32600, `Tool ${name} has an output schema but did not return structured content`);
|
|
96
|
+
const validation = (yield authInvoke(output["~standard"], "validate", [
|
|
97
|
+
result.structuredContent,
|
|
98
|
+
]));
|
|
99
|
+
if (validation.issues)
|
|
100
|
+
throw outputError(-32602, "Structured content does not match the tool's output schema: validation failed");
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}))));
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (!active)
|
|
107
|
+
throw knownProviderFailure(err);
|
|
108
|
+
const { state, client, dispatch, write } = active;
|
|
109
|
+
const direct = write && callClassification(ctx) === undefined;
|
|
110
|
+
const ownAbort = ownAbortReason(err, [ctx.signal]) && !hasSdkPayload(err);
|
|
111
|
+
// The SDK can reject its wait synchronously from the abort event,
|
|
112
|
+
// before the physical response's Promise observer records its facts.
|
|
113
|
+
// Yield only settlement; no closed caller regains live authority.
|
|
114
|
+
if (ownAbort)
|
|
115
|
+
yield Effect.yieldNow;
|
|
116
|
+
if (ownAbort && !direct)
|
|
117
|
+
throw err;
|
|
118
|
+
if (requiresAuthorization(err) && state.client === client)
|
|
119
|
+
state.authRequired = true;
|
|
120
|
+
const failure = ownAbort
|
|
121
|
+
? err
|
|
122
|
+
: err instanceof UnauthorizedError
|
|
123
|
+
? carryFailureFacts(err, rt.authRequiredError(ctx))
|
|
124
|
+
: downstreamCallError(err, undefined, undefined, undefined, requestSentSecrets(ctx));
|
|
125
|
+
if (!write)
|
|
126
|
+
throw failure;
|
|
127
|
+
const renewal = dispatch.request?.entry.renewed
|
|
128
|
+
? undefined
|
|
129
|
+
: dispatch.resendRefused
|
|
130
|
+
? rt.authRequiredError(ctx)
|
|
131
|
+
: failure;
|
|
132
|
+
const unsent = (entry) => {
|
|
133
|
+
const verdict = unsentAfterUnauthorized(`Connector "${id}"'s MCP server`, renewal, isOauth ? "downstream_oauth_required" : "auth_required");
|
|
134
|
+
return entry ? vendorRefusal(verdict, entry) : verdict;
|
|
135
|
+
};
|
|
136
|
+
const verdict = dispatch.unauthorized
|
|
137
|
+
? unsent(dispatch.request?.entry)
|
|
138
|
+
: dispatch.redirected || dispatch.resendRefused
|
|
139
|
+
? new ConnectorCallError("connector_call_failed", `Connector "${id}"'s MCP server ` +
|
|
140
|
+
(dispatch.redirected
|
|
141
|
+
? "answered this write with a redirect, and the write was not sent again, "
|
|
142
|
+
: "was sent this write, and a second send of it was stopped, ") +
|
|
143
|
+
"so whether it took effect is unknown: check the target before repeating it.", { retryable: false, writeOutcome: "unknown" })
|
|
144
|
+
: dispatch.ambiguous && failure instanceof ConnectorCallError
|
|
145
|
+
? new ConnectorCallError(failure.code, `${failure.message} Connector "${id}"'s MCP server answered this write with an HTTP error ` +
|
|
146
|
+
"and no MCP error, so whether it took effect is unknown: check the target before repeating it.", { retryable: false, writeOutcome: "unknown" })
|
|
147
|
+
: failure;
|
|
148
|
+
const vendor = { title: `connector "${id}"` };
|
|
149
|
+
const status = failureRecord({}, err).httpStatus;
|
|
150
|
+
const settled = providerCallFailure(ctx, vendor, verdict, {
|
|
151
|
+
dispatched: dispatch.sent,
|
|
152
|
+
...(status === undefined ? {} : { status }),
|
|
153
|
+
fallback: "write",
|
|
154
|
+
request: dispatch.request?.entry,
|
|
155
|
+
});
|
|
156
|
+
throw settled === failure ? failure : carryFailureFacts(failure, settled);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
if (active)
|
|
160
|
+
active.state.dispatches.delete(active.dispatch);
|
|
161
|
+
}
|
|
162
|
+
}));
|
|
163
|
+
return { callToolOperation };
|
|
164
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// Catalog intake: every tools/resources listing page is checked before the
|
|
2
|
+
// SDK can aggregate or cache it (INV-8), and listTools serves the result.
|
|
3
|
+
import { authFlow, authCall, authInvoke, authMethod } from "../../auth/native.js";
|
|
4
|
+
import { SdkHttpError, specTypeSchemas, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
5
|
+
import { redactCatalog } from "../../sent-secrets.js";
|
|
6
|
+
import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "../../catalog-limits.js";
|
|
7
|
+
import { catalogItems, observeCompletedCatalogRefresh, observeCatalogFetch, observeCatalogExpiry, } from "../../catalog-cache.js";
|
|
8
|
+
import { ConnectorCallError, msg } from "../../errors.js";
|
|
9
|
+
import { carryFailureFacts, failureRecord, logFailure } from "../../operator-record.js";
|
|
10
|
+
import { assertDownstreamOutputSafe } from "../../downstream-input-context.js";
|
|
11
|
+
import { hasSdkPayload, ownAbortReason, requiresAuthorization } from "./errors.js";
|
|
12
|
+
/** SDK aggregation cap; intake also refuses loops and non-progress (INV-8). */
|
|
13
|
+
export const MAX_TOOL_PAGES = 10_000;
|
|
14
|
+
/**
|
|
15
|
+
* Compatibility concession for hand-rolled servers that serialize
|
|
16
|
+
* end-of-pagination as `null`. Only the cursor is widened; every tool and every
|
|
17
|
+
* other result field still passes through the SDK's pinned schema.
|
|
18
|
+
*/
|
|
19
|
+
const CompatibleListToolsResultSchema = {
|
|
20
|
+
"~standard": {
|
|
21
|
+
version: 1,
|
|
22
|
+
vendor: "connecta",
|
|
23
|
+
validate(value) {
|
|
24
|
+
const normalized = typeof value === "object" && value !== null && "nextCursor" in value && value.nextCursor === null
|
|
25
|
+
? (() => {
|
|
26
|
+
const copy = { ...value };
|
|
27
|
+
delete copy.nextCursor;
|
|
28
|
+
return copy;
|
|
29
|
+
})()
|
|
30
|
+
: value;
|
|
31
|
+
return specTypeSchemas.ListToolsResult["~standard"].validate(normalized);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* True for a result-parse failure caused by the page's `nextCursor` itself.
|
|
37
|
+
* `null` is accepted deliberately; other non-string values remain a named
|
|
38
|
+
* downstream nonconformance instead of surfacing as a raw validation dump.
|
|
39
|
+
* Duck-typed rather than `instanceof ZodError`: the SDK may parse with its own
|
|
40
|
+
* zod instance, and cross-instance `instanceof` is a coin flip.
|
|
41
|
+
*/
|
|
42
|
+
function isCursorShapeError(err) {
|
|
43
|
+
const issues = err?.issues;
|
|
44
|
+
if (Array.isArray(issues) &&
|
|
45
|
+
issues.some((issue) => {
|
|
46
|
+
const path = issue.path;
|
|
47
|
+
return Array.isArray(path) && path[0] === "nextCursor";
|
|
48
|
+
})) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// SDK v2 wraps Standard Schema failures in a ProtocolError and preserves the
|
|
52
|
+
// failing path in the message rather than exposing the validator's issues.
|
|
53
|
+
return msg(err).startsWith("Invalid result for tools/list: nextCursor:");
|
|
54
|
+
}
|
|
55
|
+
/** One connector's catalog intake and listTools operation. */
|
|
56
|
+
export function remoteMcpCatalog(rt) {
|
|
57
|
+
const { id } = rt;
|
|
58
|
+
// Listings that must reach the downstream: a credential test proves the
|
|
59
|
+
// stored value against the server, never against a cached catalog.
|
|
60
|
+
const liveListings = new WeakSet();
|
|
61
|
+
/** Wrap the public request seam, before listTools aggregates or auto-caches.
|
|
62
|
+
* SDK 2.3.1 treats an identical repeated page as completion. Refuse it here:
|
|
63
|
+
* an advertised successor is never proof of a complete catalog (INV-8). */
|
|
64
|
+
const listingContexts = new WeakMap();
|
|
65
|
+
const installCatalogIntake = (client, ctx, cache) => {
|
|
66
|
+
let toolPages = 0;
|
|
67
|
+
const listingContext = (options) => {
|
|
68
|
+
const { signal: _signal, timeoutMs: _timeout, ...base } = ctx;
|
|
69
|
+
return ((options && listingContexts.get(options)) ?? {
|
|
70
|
+
...base,
|
|
71
|
+
requestScope: ctx.requestScope ?? ctx,
|
|
72
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
73
|
+
...(options?.timeout ? { timeoutMs: options.timeout } : {}),
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const listTools = client.listTools.bind(client);
|
|
77
|
+
client.listTools = authMethod((params, options) => authInvoke(cache, "withListing", [
|
|
78
|
+
listingContext(options),
|
|
79
|
+
authMethod(() => authFlow(function* () {
|
|
80
|
+
const before = toolPages;
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
result = (yield authCall(listTools, [params, options]));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const bounded = rt.atMcpBoundary(cache.currentContext(), error, "tools/list", client.transport, [
|
|
87
|
+
cache.currentContext().signal,
|
|
88
|
+
]);
|
|
89
|
+
const transient = (error instanceof SdkHttpError && [502, 503, 504].includes(error.status)) ||
|
|
90
|
+
(bounded instanceof ConnectorCallError && bounded.code === "unavailable");
|
|
91
|
+
const fallback = transient && toolPages === before
|
|
92
|
+
? (yield authInvoke(cache, "fallbackTools", []))
|
|
93
|
+
: undefined;
|
|
94
|
+
if (!fallback)
|
|
95
|
+
throw error;
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
98
|
+
if (toolPages !== before && params?.cursor === undefined) {
|
|
99
|
+
const refresh = (yield authInvoke(cache, "completeCatalogRefresh", [result]));
|
|
100
|
+
if (refresh) {
|
|
101
|
+
try {
|
|
102
|
+
(yield authCall(observeCompletedCatalogRefresh, [cache.currentContext(), refresh]));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
logFailure(cache.currentContext().logger, "catalog refresh observation failed", failureRecord({ connector: id }, error));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
})),
|
|
111
|
+
]));
|
|
112
|
+
const listResources = client.listResources.bind(client);
|
|
113
|
+
client.listResources = authMethod((params, options) => authInvoke(cache, "withListing", [
|
|
114
|
+
listingContext(options),
|
|
115
|
+
authMethod(() => authCall(listResources, [params, options])),
|
|
116
|
+
]));
|
|
117
|
+
const listResourceTemplates = client.listResourceTemplates.bind(client);
|
|
118
|
+
client.listResourceTemplates = authMethod((params, options) => authInvoke(cache, "withListing", [
|
|
119
|
+
listingContext(options),
|
|
120
|
+
authMethod(() => authCall(listResourceTemplates, [params, options])),
|
|
121
|
+
]));
|
|
122
|
+
const request = client.request.bind(client);
|
|
123
|
+
const walks = new WeakMap();
|
|
124
|
+
client.request = authMethod((...args) => authFlow(function* () {
|
|
125
|
+
const message = args[0];
|
|
126
|
+
if (message.method !== "tools/list" &&
|
|
127
|
+
message.method !== "resources/list" &&
|
|
128
|
+
message.method !== "resources/templates/list")
|
|
129
|
+
return yield authCall(request, args, client);
|
|
130
|
+
const method = message.method;
|
|
131
|
+
const ctx = cache.currentContext();
|
|
132
|
+
const options = (args.length === 3 ? args[2] : args[1]);
|
|
133
|
+
if (rt.isClosed(rt.entryFor(ctx)))
|
|
134
|
+
throw rt.scopeEndedError();
|
|
135
|
+
if (options?.signal?.aborted)
|
|
136
|
+
throw options.signal.reason;
|
|
137
|
+
const key = options ?? message;
|
|
138
|
+
let walk = walks.get(key);
|
|
139
|
+
if (message.params?.cursor === undefined || !walk) {
|
|
140
|
+
walk = { names: new Set(), cursors: new Set(), barren: 0, bytes: 0 };
|
|
141
|
+
walks.set(key, walk);
|
|
142
|
+
}
|
|
143
|
+
let page;
|
|
144
|
+
try {
|
|
145
|
+
page = (yield authCall(request, [
|
|
146
|
+
message,
|
|
147
|
+
method === "tools/list"
|
|
148
|
+
? CompatibleListToolsResultSchema
|
|
149
|
+
: method === "resources/list"
|
|
150
|
+
? specTypeSchemas.ListResourcesResult
|
|
151
|
+
: specTypeSchemas.ListResourceTemplatesResult,
|
|
152
|
+
options,
|
|
153
|
+
]));
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (isCursorShapeError(error))
|
|
157
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream catalog nextCursor must be a string, null, or absent.", { retryable: false });
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, page);
|
|
161
|
+
const clean = cache.intake(ctx, method, page);
|
|
162
|
+
if (method === "tools/list")
|
|
163
|
+
toolPages++;
|
|
164
|
+
if (message.params?.cursor === undefined)
|
|
165
|
+
observeCatalogFetch(ctx, Date.now(), method);
|
|
166
|
+
const tools = catalogItems(method, clean).filter((item) => {
|
|
167
|
+
const name = method === "tools/list"
|
|
168
|
+
? item.name
|
|
169
|
+
: method === "resources/list"
|
|
170
|
+
? item.uri
|
|
171
|
+
: item.uriTemplate;
|
|
172
|
+
if (walk.names.has(name))
|
|
173
|
+
return false;
|
|
174
|
+
walk.names.add(name);
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
walk.bytes += new TextEncoder().encode(JSON.stringify(tools)).byteLength;
|
|
178
|
+
if (walk.names.size > MAX_CATALOG_TOOLS || walk.bytes > MAX_SERIALIZED_CATALOG_BYTES) {
|
|
179
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream catalog exceeds the complete-catalog ceiling.", { retryable: false });
|
|
180
|
+
}
|
|
181
|
+
if (clean.nextCursor !== undefined) {
|
|
182
|
+
if (walk.cursors.has(clean.nextCursor))
|
|
183
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream catalog pagination chain loops.", {
|
|
184
|
+
retryable: false,
|
|
185
|
+
});
|
|
186
|
+
if (tools.length === 0 && ++walk.barren > 1)
|
|
187
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream catalog pagination is not advancing.", {
|
|
188
|
+
retryable: false,
|
|
189
|
+
});
|
|
190
|
+
if (tools.length > 0)
|
|
191
|
+
walk.barren = 0;
|
|
192
|
+
walk.cursors.add(clean.nextCursor);
|
|
193
|
+
}
|
|
194
|
+
const field = method === "tools/list" ? "tools" : method === "resources/list" ? "resources" : "resourceTemplates";
|
|
195
|
+
const result = { ...clean, [field]: tools };
|
|
196
|
+
if (!walk.first)
|
|
197
|
+
walk.first = result;
|
|
198
|
+
else {
|
|
199
|
+
walk.first.ttlMs = Math.min(walk.first.ttlMs, clean.ttlMs);
|
|
200
|
+
if (clean.cacheScope !== "public")
|
|
201
|
+
walk.first.cacheScope = "private";
|
|
202
|
+
}
|
|
203
|
+
observeCatalogExpiry(ctx, method, walk.first.ttlMs);
|
|
204
|
+
return result;
|
|
205
|
+
}));
|
|
206
|
+
};
|
|
207
|
+
// The SDK walks the whole chain and writes one complete result. Intake
|
|
208
|
+
// runs on its public request seam before any page can enter that cache.
|
|
209
|
+
const listToolsOperation = authMethod((ctx) => authFlow(function* () {
|
|
210
|
+
const state = rt.stateFor(ctx);
|
|
211
|
+
// Without a connection in this scope, a complete cached catalog is
|
|
212
|
+
// served as it stands. A live connection reads the same cache through
|
|
213
|
+
// the SDK, and a credential test always lists live.
|
|
214
|
+
const cached = state.client || state.connecting || state.authRequired || liveListings.has(ctx)
|
|
215
|
+
? {}
|
|
216
|
+
: (yield authCall(rt.cachedCatalog, [ctx]));
|
|
217
|
+
let clean;
|
|
218
|
+
if (cached.tools)
|
|
219
|
+
clean = redactCatalog(ctx, cached.tools);
|
|
220
|
+
else {
|
|
221
|
+
const client = (yield authCall(rt.ensureConnected, [ctx, state, cached]));
|
|
222
|
+
// A credential test must observe the downstream answer itself, never
|
|
223
|
+
// the SDK cache a modern connection reads without any request.
|
|
224
|
+
const options = {
|
|
225
|
+
...rt.requestOptions(ctx),
|
|
226
|
+
...(liveListings.has(ctx) ? { cacheMode: "bypass" } : {}),
|
|
227
|
+
};
|
|
228
|
+
listingContexts.set(options, ctx);
|
|
229
|
+
try {
|
|
230
|
+
const result = (yield authInvoke(client, "listTools", [undefined, options]));
|
|
231
|
+
clean = redactCatalog(ctx, result.tools);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (ownAbortReason(err, [ctx.signal]) && !hasSdkPayload(err))
|
|
235
|
+
throw err;
|
|
236
|
+
const bounded = rt.atMcpBoundary(ctx, err, "tools/list", client.transport, [ctx.signal]);
|
|
237
|
+
if (requiresAuthorization(bounded)) {
|
|
238
|
+
if (state.client === client)
|
|
239
|
+
state.authRequired = true;
|
|
240
|
+
if (bounded instanceof UnauthorizedError)
|
|
241
|
+
throw carryFailureFacts(bounded, rt.authRequiredError(ctx));
|
|
242
|
+
}
|
|
243
|
+
throw bounded;
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
listingContexts.delete(options);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
state.toolDefinitions = new Map(clean.map((tool) => [tool.name, tool]));
|
|
250
|
+
return clean.map((t) => ({
|
|
251
|
+
name: t.name,
|
|
252
|
+
...(t.title !== undefined ? { title: t.title } : {}),
|
|
253
|
+
...(t.icons !== undefined
|
|
254
|
+
? {
|
|
255
|
+
icons: t.icons.filter((icon) => !/^data:/i.test(icon.src)),
|
|
256
|
+
}
|
|
257
|
+
: {}),
|
|
258
|
+
...(t.execution !== undefined ? { execution: t.execution } : {}),
|
|
259
|
+
...(t.description !== undefined ? { description: t.description } : {}),
|
|
260
|
+
...(t.inputSchema !== undefined
|
|
261
|
+
? {
|
|
262
|
+
inputSchema: t.inputSchema,
|
|
263
|
+
}
|
|
264
|
+
: {}),
|
|
265
|
+
...(t.outputSchema !== undefined
|
|
266
|
+
? {
|
|
267
|
+
outputSchema: t.outputSchema,
|
|
268
|
+
}
|
|
269
|
+
: {}),
|
|
270
|
+
...(t.annotations !== undefined
|
|
271
|
+
? {
|
|
272
|
+
annotations: t.annotations,
|
|
273
|
+
}
|
|
274
|
+
: {}),
|
|
275
|
+
}));
|
|
276
|
+
}));
|
|
277
|
+
return { installCatalogIntake, listingContexts, liveListings, listToolsOperation };
|
|
278
|
+
}
|