@zackbart/connecta 0.28.0 → 0.29.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 +961 -5
- package/README.md +62 -39
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1632 -1509
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +526 -0
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +220 -0
- package/dist/connectors/api-connector.js +214 -0
- package/dist/connectors/api.d.ts +8 -71
- package/dist/connectors/api.js +13 -99
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +108 -25
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1780 -351
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +25 -257
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-partition.js +17 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/basecamp/index.d.ts +43 -0
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/breeze/index.d.ts +34 -0
- package/dist/providers/breeze/index.js +1446 -0
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/ccb/index.d.ts +103 -0
- package/dist/providers/ccb/index.js +1725 -0
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/overflow/index.d.ts +60 -0
- package/dist/providers/overflow/index.js +1535 -0
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/planning-center/index.d.ts +59 -0
- package/dist/providers/planning-center/index.js +2199 -0
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/tithely/index.d.ts +49 -0
- package/dist/providers/tithely/index.js +1003 -0
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +136 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +665 -230
- package/documentation/auth.md +962 -458
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +125 -35
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
package/dist/meta-tools.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import { bindMcpClient } from "./mcp-client-context.js";
|
|
2
|
+
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
1
3
|
import { z } from "zod";
|
|
2
4
|
import { advertisedSchema } from "./advertised-schema.js";
|
|
3
|
-
import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError,
|
|
5
|
+
import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, flatSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
|
|
4
6
|
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
5
|
-
import { boundedEchoText, msg } from "./errors.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
|
|
8
|
+
import { failureRecord, logFailure } from "./operator-record.js";
|
|
9
|
+
import { MAX_EXECUTE_RESULT_CHARS, serializeResultText } from "./executor-result.js";
|
|
10
|
+
import { InvocationService } from "./invocation.js";
|
|
8
11
|
import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
|
|
9
|
-
import { hasConnectorGuides,
|
|
10
|
-
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
import { hasConnectorGuides, SkillsRegistry } from "./skills.js";
|
|
13
|
+
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs } from "./timeout.js";
|
|
14
|
+
import { AUTHORIZE_OUTPUT, CALL_OUTPUT, SEARCH_OUTPUT, SKILLS_OUTPUT } from "./meta-output.js";
|
|
15
|
+
import { RESULT_TTL_SECONDS, resultKeys } from "./storage/keys.js";
|
|
16
|
+
import { agentOutputOperations, sentSecretsForRequest } from "./sent-secrets.js";
|
|
17
|
+
import { captureDownstreamInput } from "./downstream-input.js";
|
|
18
|
+
export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT };
|
|
14
19
|
const enc = new TextEncoder();
|
|
15
20
|
const dec = new TextDecoder();
|
|
16
21
|
export function jsonResult(obj, text = JSON.stringify(obj)) {
|
|
@@ -21,7 +26,7 @@ export function jsonResult(obj, text = JSON.stringify(obj)) {
|
|
|
21
26
|
: {}),
|
|
22
27
|
};
|
|
23
28
|
}
|
|
24
|
-
|
|
29
|
+
function errorResult(message) {
|
|
25
30
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
26
31
|
}
|
|
27
32
|
function discoveryErrorResult(error) {
|
|
@@ -57,7 +62,7 @@ async function discoveryResult(operation, hint) {
|
|
|
57
62
|
function isContinuationByte(b) {
|
|
58
63
|
return b !== undefined && (b & 0xc0) === 0x80;
|
|
59
64
|
}
|
|
60
|
-
/** Smallest accepted `
|
|
65
|
+
/** Smallest accepted `connecta.result` byte offset. */
|
|
61
66
|
const MIN_RESULT_OFFSET = 0;
|
|
62
67
|
/** Whole-byte offset accepted by the result representation documented in
|
|
63
68
|
* documentation/meta-tools.md#result-representation. */
|
|
@@ -101,17 +106,9 @@ export function alignEndToCharBoundary(bytes, offset, end, total) {
|
|
|
101
106
|
}
|
|
102
107
|
return e;
|
|
103
108
|
}
|
|
104
|
-
/**
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
* The current envelope: v2's header plus the inline cap of the call that
|
|
108
|
-
* stashed it, `<total>:<chunk>:<cap>:`, so get_result can clamp a page to the
|
|
109
|
-
* connector's own cap without the stash carrying connector identity.
|
|
110
|
-
*/
|
|
111
|
-
const RESULT_ENVELOPE_V3 = "connecta-result-v3:";
|
|
112
|
-
const RESULT_ENVELOPE_V3_HEADER = new RegExp(`^${RESULT_ENVELOPE_V3}(\\d+):(\\d+):(\\d+):`);
|
|
113
|
-
/** `<total bytes>:<bytes per chunk>:` follow the prefix, then chunk 0's base64. */
|
|
114
|
-
const RESULT_ENVELOPE_V2_HEADER = new RegExp(`^${RESULT_ENVELOPE_V2}(\\d+):(\\d+):`);
|
|
109
|
+
/** Every readable stash carries host-admitted bindings; older entries fail closed. */
|
|
110
|
+
const RESULT_ENVELOPE_V4 = "connecta-result-v4:";
|
|
111
|
+
const RESULT_ENVELOPE_V4_HEADER = /^connecta-result-v4:([A-Za-z0-9+/=]+):(\d+):(\d+):(\d+):/;
|
|
115
112
|
/**
|
|
116
113
|
* Smallest chunk of result text stored under one key. A multiple of three so
|
|
117
114
|
* every chunk's base64 stands alone and a byte offset inside it lands on a
|
|
@@ -122,7 +119,7 @@ const RESULT_CHUNK_BYTES = 49_152;
|
|
|
122
119
|
/**
|
|
123
120
|
* Keys one stashed result may occupy. Chunking trades write count for read
|
|
124
121
|
* count, and both are real: every chunk is a storage write at stash time, and
|
|
125
|
-
*
|
|
122
|
+
* a SQL store commits one row per write. Above roughly 1.5 MB the
|
|
126
123
|
* chunks widen instead of multiplying, so a result costs a bounded number of
|
|
127
124
|
* writes and a page still reads a small fraction of it.
|
|
128
125
|
*/
|
|
@@ -161,13 +158,13 @@ function pagingHint(results, totalBytes, preview) {
|
|
|
161
158
|
: preview.kind === "text-of-envelope"
|
|
162
159
|
? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
|
|
163
160
|
: [`The result is ${totalBytes} bytes.`, "to read it in full, page it"];
|
|
164
|
-
return `${shown} ${READ_REDUCE_FIRST}; ${full} with
|
|
161
|
+
return `${shown} ${READ_REDUCE_FIRST}; ${full} with connecta.result using nextAction.`;
|
|
165
162
|
}
|
|
166
163
|
const body = preview.kind === "prefix"
|
|
167
|
-
? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with
|
|
164
|
+
? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with connecta.result using nextAction.`
|
|
168
165
|
: preview.kind === "text-of-envelope"
|
|
169
|
-
? `Its text follows; page the full ${totalBytes}-byte content array as JSON with
|
|
170
|
-
: `Page all ${totalBytes} bytes with
|
|
166
|
+
? `Its text follows; page the full ${totalBytes}-byte content array as JSON with connecta.result using nextAction.`
|
|
167
|
+
: `Page all ${totalBytes} bytes with connecta.result using nextAction.`;
|
|
171
168
|
return `${WRITE_ALREADY_RAN} ${body}`;
|
|
172
169
|
}
|
|
173
170
|
/**
|
|
@@ -182,14 +179,21 @@ function pagingHint(results, totalBytes, preview) {
|
|
|
182
179
|
*/
|
|
183
180
|
async function stashResult(bytes, results, preview) {
|
|
184
181
|
const totalBytes = bytes.length;
|
|
182
|
+
if (!results.pageable)
|
|
183
|
+
return {
|
|
184
|
+
truncated: true,
|
|
185
|
+
totalBytes,
|
|
186
|
+
hint: `${WRITE_ALREADY_RAN} Paging is unavailable for write results on read-only pools.`,
|
|
187
|
+
};
|
|
185
188
|
const id = crypto.randomUUID();
|
|
186
189
|
try {
|
|
187
190
|
// Base64 permits byte-range decoding, and splitting the envelope across
|
|
188
191
|
// keys keeps a page's storage read proportional to the page instead of to
|
|
189
|
-
// the whole result (issue #540). Chunk 0 carries the header; the
|
|
192
|
+
// the whole result (issue #540). Chunk 0 carries the header; the connecta.result
|
|
190
193
|
// reader below maps a byte offset back to chunk index and base64 quad.
|
|
191
194
|
const chunkBytes = resultChunkBytes(totalBytes);
|
|
192
|
-
const
|
|
195
|
+
const binding = base64Of(enc.encode(JSON.stringify(results.binding)));
|
|
196
|
+
const chunks = [`${RESULT_ENVELOPE_V4}${binding}:${totalBytes}:${chunkBytes}:${results.cap}:`];
|
|
193
197
|
for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
|
|
194
198
|
const chunk = base64Of(bytes.subarray(offset, offset + chunkBytes));
|
|
195
199
|
if (offset === 0)
|
|
@@ -197,7 +201,7 @@ async function stashResult(bytes, results, preview) {
|
|
|
197
201
|
else
|
|
198
202
|
chunks.push(chunk);
|
|
199
203
|
}
|
|
200
|
-
if (!await results.set(
|
|
204
|
+
if (!(await results.set(id, chunks, RESULT_TTL_SECONDS))) {
|
|
201
205
|
throw new Error("Result stash capacity exhausted");
|
|
202
206
|
}
|
|
203
207
|
}
|
|
@@ -223,9 +227,14 @@ async function stashResult(bytes, results, preview) {
|
|
|
223
227
|
resultId: id,
|
|
224
228
|
totalBytes,
|
|
225
229
|
hint: pagingHint(results, totalBytes, preview),
|
|
230
|
+
nextOffset: preview.kind === "prefix" ? preview.bytes : 0,
|
|
226
231
|
nextAction: {
|
|
227
|
-
tool: "
|
|
228
|
-
|
|
232
|
+
tool: "execute_code",
|
|
233
|
+
// A byte may cost six JSON characters when escaped. Leave room for the
|
|
234
|
+
// result id, page counters, and continuation metadata in the returned page.
|
|
235
|
+
arguments: {
|
|
236
|
+
code: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${Math.floor((MAX_EXECUTE_RESULT_CHARS - 1024) / 6)} })`,
|
|
237
|
+
},
|
|
229
238
|
},
|
|
230
239
|
};
|
|
231
240
|
}
|
|
@@ -235,11 +244,14 @@ function headOf(bytes, cap) {
|
|
|
235
244
|
}
|
|
236
245
|
/** One text block: the notice line, then the preview. */
|
|
237
246
|
function noticeFirst(notice, preview) {
|
|
238
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: "text", text: `${JSON.stringify(notice)}\n${preview}` }],
|
|
249
|
+
structuredContent: { ...notice, format: "text" },
|
|
250
|
+
};
|
|
239
251
|
}
|
|
240
252
|
/**
|
|
241
253
|
* Return `text` as a single content block; if it exceeds `cap` bytes, stash the
|
|
242
|
-
* full text and return a JSON truncation notice pointing at
|
|
254
|
+
* full text and return a JSON truncation notice pointing at connecta.result,
|
|
243
255
|
* followed by the first `cap` bytes. The preview is a byte prefix of what
|
|
244
256
|
* pages, so the notice's next action continues where it stops. `bytes` is
|
|
245
257
|
* `text` already encoded, so a caller that had to measure it to make this
|
|
@@ -266,21 +278,25 @@ async function guardText(text, results, cap) {
|
|
|
266
278
|
// Normalizing at the door means the size check below always measures exactly
|
|
267
279
|
// the text that is emitted, and no future caller can launder a non-string
|
|
268
280
|
// through it the way issue #42 describes.
|
|
269
|
-
const
|
|
281
|
+
const serialized = typeof text === "string" ? text : serializeResultText(text);
|
|
282
|
+
const body = results.secrets?.text(serialized) ?? serialized;
|
|
270
283
|
return guardEncoded(body, enc.encode(body), results, cap);
|
|
271
284
|
}
|
|
272
285
|
/** Store an oversized JSON value and replace it with a page handle. */
|
|
273
286
|
async function guardValue(value, results, cap) {
|
|
274
|
-
const
|
|
287
|
+
const serialized = serializeResultText(value);
|
|
288
|
+
const text = results.secrets?.text(serialized) ?? serialized;
|
|
275
289
|
const bytes = enc.encode(text);
|
|
276
290
|
if (bytes.length <= cap)
|
|
277
|
-
return { result: value, truncated: false };
|
|
291
|
+
return { result: results.secrets?.redact(value) ?? value, truncated: false };
|
|
278
292
|
const notice = await stashResult(bytes, results, { kind: "none" });
|
|
279
293
|
return {
|
|
280
|
-
result: notice.resultId
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
result: notice.resultId
|
|
295
|
+
? notice
|
|
296
|
+
: {
|
|
297
|
+
...notice,
|
|
298
|
+
preview: escapedHeadOf(bytes, cap),
|
|
299
|
+
},
|
|
284
300
|
truncated: true,
|
|
285
301
|
};
|
|
286
302
|
}
|
|
@@ -317,12 +333,13 @@ function escapedHeadOf(bytes, cap) {
|
|
|
317
333
|
*
|
|
318
334
|
* Over the cap, what a client gets depends on whether a preview is usable.
|
|
319
335
|
* Several text blocks preview as their text joined by newlines, while
|
|
320
|
-
*
|
|
336
|
+
* connecta.result pages the envelope from offset 0, where the block boundaries
|
|
321
337
|
* live. An envelope carrying non-text blocks is replaced by the notice alone:
|
|
322
338
|
* the head of a half-written base64 image is of no use to anyone. Either way
|
|
323
|
-
* the full envelope is stashed and pages through `
|
|
339
|
+
* the full envelope is stashed and pages through `connecta.result`.
|
|
324
340
|
*/
|
|
325
341
|
async function guardContent(content, results, cap) {
|
|
342
|
+
content = results.secrets?.redact({ content }).content ?? content;
|
|
326
343
|
const only = content.length === 1 ? content[0] : undefined;
|
|
327
344
|
if (only?.type === "text" && typeof only.text === "string") {
|
|
328
345
|
const bytes = enc.encode(only.text);
|
|
@@ -333,7 +350,8 @@ async function guardContent(content, results, cap) {
|
|
|
333
350
|
}
|
|
334
351
|
let text;
|
|
335
352
|
try {
|
|
336
|
-
|
|
353
|
+
const serialized = JSON.stringify(content);
|
|
354
|
+
text = results.secrets?.text(serialized) ?? serialized;
|
|
337
355
|
}
|
|
338
356
|
catch {
|
|
339
357
|
// A block carrying a BigInt or a cycle cannot be serialized, so it cannot
|
|
@@ -385,40 +403,53 @@ function oauthFollowUp(connectorId) {
|
|
|
385
403
|
* override each have one runtime source of truth.
|
|
386
404
|
*/
|
|
387
405
|
export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
406
|
+
return agentOutputOperations((requestScope) => metaToolsForRequest(registry, baseUrl, { ...opts, requestScope }), opts.requestScope);
|
|
407
|
+
}
|
|
408
|
+
function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
388
409
|
// Already normalized and warned about at registry construction.
|
|
389
410
|
const globalCap = registry.maxResultBytes;
|
|
390
|
-
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
|
|
411
|
+
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs) ?? CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
|
|
391
412
|
const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
392
413
|
const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
|
|
393
414
|
// createMetaTools() is called once per inbound MCP request. Sharing this
|
|
394
415
|
// identity lets remote connectors reuse one downstream client inside that
|
|
395
416
|
// request without leaking request-bound I/O into the next one.
|
|
396
|
-
const requestScope = {};
|
|
417
|
+
const requestScope = opts.requestScope ?? {};
|
|
418
|
+
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
419
|
+
const skills = new SkillsRegistry(registry, baseUrl, {
|
|
420
|
+
requestScope,
|
|
421
|
+
requestSignal: opts.requestSignal,
|
|
422
|
+
probeTimeoutMs,
|
|
423
|
+
defer: opts.defer,
|
|
424
|
+
});
|
|
397
425
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
398
426
|
requestScope,
|
|
399
427
|
probeTimeoutMs,
|
|
400
428
|
concurrency: discoveryConcurrency,
|
|
401
429
|
defer: opts.defer,
|
|
402
430
|
requestSignal: opts.requestSignal,
|
|
403
|
-
approval: opts.approval,
|
|
404
431
|
// searchRoute keeps its top-level default. In-program callers use a
|
|
405
432
|
// separate CatalogService configured for connecta.search.
|
|
406
433
|
});
|
|
407
434
|
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
408
435
|
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
409
|
-
async function runCall(call, source
|
|
436
|
+
async function runCall(call, source) {
|
|
410
437
|
const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
|
|
411
438
|
const outcome = await invocation.invoke(call.address, call.args ?? {}, {
|
|
412
439
|
source,
|
|
413
|
-
|
|
414
|
-
? { allowDestructive: options.allowDestructive }
|
|
415
|
-
: {}),
|
|
440
|
+
sentSecrets,
|
|
416
441
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
417
|
-
...(opts.requestSignal !== undefined
|
|
418
|
-
? { requestSignal: opts.requestSignal }
|
|
419
|
-
: {}),
|
|
442
|
+
...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
|
|
420
443
|
unwrapResult: call.resultMode === "value",
|
|
421
|
-
|
|
444
|
+
...(opts.downstreamInput
|
|
445
|
+
? {
|
|
446
|
+
processInputRequired: async (result, resolved, secrets) => {
|
|
447
|
+
await captureDownstreamInput(requestScope, resolved.connector.id, `${resolved.connector.id}.${resolved.toolName}`, result, secrets);
|
|
448
|
+
return { toolResult: { content: [] } };
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
: {}),
|
|
452
|
+
processResult: async (result, resolved, secrets, format) => {
|
|
422
453
|
// Result-size cap for THIS call: the connector's own override wins,
|
|
423
454
|
// then the deployment-wide value, then the built-in default (already
|
|
424
455
|
// folded into `globalCap`). Resolved per call so one request can
|
|
@@ -427,13 +458,21 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
427
458
|
// here, so the connector simply inherits `globalCap`.
|
|
428
459
|
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
|
|
429
460
|
const results = {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
461
|
+
secrets,
|
|
462
|
+
pageable: resolved.definition.classification === "read" || opts.trust === "trusted",
|
|
463
|
+
binding: {
|
|
464
|
+
identity: registry.resultIdentity(),
|
|
465
|
+
baseUrl,
|
|
434
466
|
connector: resolved.connector.id,
|
|
435
|
-
tool: resolved.
|
|
436
|
-
|
|
467
|
+
tool: resolved.definition.name,
|
|
468
|
+
classification: resolved.definition.classification === "read" ? "read" : "write",
|
|
469
|
+
},
|
|
470
|
+
write: resolved.definition.classification !== "read",
|
|
471
|
+
cap,
|
|
472
|
+
set: (id, value, ttlSeconds) => registry.stashResult(id, value, ttlSeconds),
|
|
473
|
+
// The catalog entry, never `toolName`: a record names a tool
|
|
474
|
+
// only through the grammar check (src/operator-record.ts).
|
|
475
|
+
warn: () => logFailure(registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger, "result paging unavailable", failureRecord({ connector: resolved.connector.id, tool: resolved.definition })),
|
|
437
476
|
};
|
|
438
477
|
const processed = (toolResult, truncated, value) => ({
|
|
439
478
|
toolResult,
|
|
@@ -444,23 +483,27 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
444
483
|
let value = result;
|
|
445
484
|
const guarded = await guardValue(value, results, cap);
|
|
446
485
|
value = guarded.result;
|
|
447
|
-
return processed(jsonResult({ ok: true, data: value }), guarded.truncated, { value });
|
|
486
|
+
return processed(jsonResult({ ok: true, data: value, format }), guarded.truncated, { value });
|
|
448
487
|
}
|
|
449
488
|
if (resolved.connector.kind === "mcp") {
|
|
450
489
|
const mcpResult = result;
|
|
451
490
|
let content = mcpResult?.content ?? [];
|
|
452
|
-
if (!content.some((block) => block.type === "text") &&
|
|
453
|
-
|
|
454
|
-
|
|
491
|
+
if (!content.some((block) => block.type === "text") && mcpResult?.structuredContent !== undefined) {
|
|
492
|
+
content = [
|
|
493
|
+
...content,
|
|
494
|
+
{
|
|
455
495
|
type: "text",
|
|
456
496
|
text: JSON.stringify(mcpResult.structuredContent),
|
|
457
|
-
}
|
|
497
|
+
},
|
|
498
|
+
];
|
|
458
499
|
}
|
|
459
500
|
const guarded = await guardContent(content, results, cap);
|
|
501
|
+
guarded.result.structuredContent = { ...guarded.result.structuredContent, format };
|
|
460
502
|
return processed(guarded.result, guarded.truncated);
|
|
461
503
|
}
|
|
462
504
|
const value = result;
|
|
463
505
|
const guarded = await guardText(serializeResultText(value), results, cap);
|
|
506
|
+
guarded.result.structuredContent = { ...guarded.result.structuredContent, format };
|
|
464
507
|
return processed(guarded.result, guarded.truncated, { value });
|
|
465
508
|
},
|
|
466
509
|
activityFriction: (processed) => processed.friction,
|
|
@@ -473,23 +516,17 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
473
516
|
outcome.error.details !== undefined ||
|
|
474
517
|
// So does a conflict's `current`: where things stand is the retry.
|
|
475
518
|
outcome.error.current !== undefined ||
|
|
476
|
-
[
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
?
|
|
484
|
-
|
|
485
|
-
error: outcome.error,
|
|
486
|
-
durationMs: outcome.durationMs,
|
|
487
|
-
attempts: outcome.attempts,
|
|
488
|
-
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
489
|
-
})
|
|
490
|
-
: errorResult(outcome.error.message);
|
|
519
|
+
["auth_required", "invalid_args", "input_required_unsupported"].includes(outcome.error.code);
|
|
520
|
+
// Every mode carries the complete repair envelope in both MCP forms.
|
|
521
|
+
const makeFailedResult = () => jsonResult({
|
|
522
|
+
ok: false,
|
|
523
|
+
error: outcome.error,
|
|
524
|
+
durationMs: outcome.durationMs,
|
|
525
|
+
attempts: outcome.attempts,
|
|
526
|
+
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
527
|
+
});
|
|
491
528
|
let failedResult = makeFailedResult();
|
|
492
|
-
//
|
|
529
|
+
// Errors repeat in text and structuredContent. Account for
|
|
493
530
|
// both copies and JSON escaping when the bounded provider reason is large.
|
|
494
531
|
if (!recoveryRequired) {
|
|
495
532
|
const cap = resolveMaxResultBytes(outcome.resolved?.connector.maxResultBytes, globalCap);
|
|
@@ -500,9 +537,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
500
537
|
failedResult = makeFailedResult();
|
|
501
538
|
}
|
|
502
539
|
}
|
|
503
|
-
|
|
504
|
-
failedResult.isError = true;
|
|
505
|
-
}
|
|
540
|
+
failedResult.isError = true;
|
|
506
541
|
return {
|
|
507
542
|
toolResult: failedResult,
|
|
508
543
|
durationMs: outcome.durationMs,
|
|
@@ -515,6 +550,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
515
550
|
? jsonResult({
|
|
516
551
|
ok: true,
|
|
517
552
|
data: outcome.value.value,
|
|
553
|
+
format: outcome.format,
|
|
518
554
|
durationMs: outcome.durationMs,
|
|
519
555
|
attempts: outcome.attempts,
|
|
520
556
|
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
@@ -525,37 +561,37 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
525
561
|
durationMs: outcome.durationMs,
|
|
526
562
|
attempts: outcome.attempts,
|
|
527
563
|
timing: outcome.timing,
|
|
528
|
-
...(Object.prototype.hasOwnProperty.call(outcome.value, "value")
|
|
529
|
-
? { value: outcome.value.value }
|
|
530
|
-
: {}),
|
|
564
|
+
...(Object.prototype.hasOwnProperty.call(outcome.value, "value") ? { value: outcome.value.value } : {}),
|
|
531
565
|
};
|
|
532
566
|
}
|
|
533
567
|
return {
|
|
534
568
|
async skills(args = {}) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
{
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
.join("\n"),
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
569
|
+
try {
|
|
570
|
+
if (!args.name) {
|
|
571
|
+
const listing = await skills.summaries();
|
|
572
|
+
return {
|
|
573
|
+
structuredContent: { skills: listing },
|
|
574
|
+
content: [
|
|
575
|
+
{
|
|
576
|
+
type: "text",
|
|
577
|
+
text: 'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
|
|
578
|
+
listing.map((skill) => `- \`${skill.name}\` — ${skill.description}`).join("\n"),
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const text = await skills.text(args.name);
|
|
584
|
+
return { content: [{ type: "text", text }], structuredContent: { name: args.name, format: "text", text } };
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
return errorResult(error instanceof ConnectorCallError ? error.message : "Skills are unavailable.");
|
|
548
588
|
}
|
|
549
|
-
const skill = resolveSkill(args.name, connectors);
|
|
550
|
-
if (!skill.found)
|
|
551
|
-
return errorResult(skill.message);
|
|
552
|
-
return { content: [{ type: "text", text: skill.content }] };
|
|
553
589
|
},
|
|
554
590
|
async searchTools(args) {
|
|
555
591
|
if (args.connector !== undefined && enc.encode(args.connector).length > 512) {
|
|
556
592
|
return discoveryErrorResult(new DiscoveryPolicyError("invalid_args", "connector must be at most 512 UTF-8 bytes."));
|
|
557
593
|
}
|
|
558
|
-
return discoveryResult(async () =>
|
|
594
|
+
return discoveryResult(async () => flatSearchResult(await catalog.search({
|
|
559
595
|
...args,
|
|
560
596
|
includeSchemaKeys: args.includeSchemas !== undefined,
|
|
561
597
|
})), "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
|
|
@@ -566,16 +602,12 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
566
602
|
async callDestructiveTool(args) {
|
|
567
603
|
// `reason` is read by the host's approval view and stops there — runCall
|
|
568
604
|
// forwards only the call arguments, so it never reaches the connector.
|
|
569
|
-
return (await runCall(args, "call_destructive_tool"
|
|
605
|
+
return (await runCall(args, "call_destructive_tool")).toolResult;
|
|
570
606
|
},
|
|
571
|
-
async
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
// in-process callers of createMetaTools — which have no schema in front
|
|
576
|
-
// of them — and to keep the rules true of the handler on its own terms.
|
|
577
|
-
if (args.maxBytes !== undefined &&
|
|
578
|
-
!isValidMaxResultBytes(args.maxBytes)) {
|
|
607
|
+
async readResult(args, options = {}) {
|
|
608
|
+
// Defense for in-process callers too: guest paging validates these domains
|
|
609
|
+
// before calling here, while internal consumers may call readResult directly.
|
|
610
|
+
if (args.maxBytes !== undefined && !isValidMaxResultBytes(args.maxBytes)) {
|
|
579
611
|
return errorResult(`Invalid maxBytes ${args.maxBytes}: must be a whole number of bytes ` +
|
|
580
612
|
`>= ${MIN_MAX_RESULT_BYTES}. Omit it to use the deployment default.`);
|
|
581
613
|
}
|
|
@@ -598,52 +630,90 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
598
630
|
// is simply gone. Every key a page touches answers the same way, so a
|
|
599
631
|
// backend that dies halfway through a multi-chunk page says so.
|
|
600
632
|
const read = async (key) => {
|
|
633
|
+
options.signal?.throwIfAborted();
|
|
634
|
+
let value;
|
|
601
635
|
try {
|
|
602
|
-
|
|
636
|
+
value = (await results.get(key)) ?? null;
|
|
603
637
|
}
|
|
604
638
|
catch {
|
|
605
|
-
|
|
639
|
+
value = false;
|
|
606
640
|
}
|
|
641
|
+
// A storage driver may finish its pending read after cancellation. No
|
|
642
|
+
// subsequent chunk read or decode may start when that happens (INV-7).
|
|
643
|
+
options.signal?.throwIfAborted();
|
|
644
|
+
return value;
|
|
607
645
|
};
|
|
608
|
-
const stored = await read(
|
|
646
|
+
const stored = await read(resultKeys.chunk(args.id, 0));
|
|
609
647
|
if (stored === false)
|
|
610
648
|
return unavailableResult();
|
|
611
649
|
if (stored === null) {
|
|
612
650
|
return errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
|
|
613
651
|
}
|
|
652
|
+
const denied = () => errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
|
|
653
|
+
const header = RESULT_ENVELOPE_V4_HEADER.exec(stored);
|
|
654
|
+
if (!header)
|
|
655
|
+
return denied();
|
|
656
|
+
let binding;
|
|
657
|
+
try {
|
|
658
|
+
binding = JSON.parse(dec.decode(Uint8Array.from(atob(header[1]), (char) => char.charCodeAt(0))));
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
return denied();
|
|
662
|
+
}
|
|
663
|
+
const identity = registry.resultIdentity();
|
|
664
|
+
if (!binding ||
|
|
665
|
+
binding.baseUrl !== baseUrl ||
|
|
666
|
+
!binding.identity ||
|
|
667
|
+
binding.identity.subject !== identity.subject ||
|
|
668
|
+
binding.identity.principal !== identity.principal ||
|
|
669
|
+
binding.identity.endpoint !== identity.endpoint ||
|
|
670
|
+
binding.identity.origin !== identity.origin ||
|
|
671
|
+
typeof binding.connector !== "string" ||
|
|
672
|
+
typeof binding.tool !== "string" ||
|
|
673
|
+
!["read", "write"].includes(binding.classification))
|
|
674
|
+
return denied();
|
|
675
|
+
const allowed = async () => {
|
|
676
|
+
options.signal?.throwIfAborted();
|
|
677
|
+
try {
|
|
678
|
+
const address = `${binding.connector}.${binding.tool}`;
|
|
679
|
+
if (!(await registry.recheckResultAccess(address, binding.classification, options.signal)))
|
|
680
|
+
return false;
|
|
681
|
+
if (!registry.getConnector(binding.connector))
|
|
682
|
+
return false;
|
|
683
|
+
const tools = await registry.getTools(binding.connector, baseUrl, requestScope, options.signal ? { signal: options.signal } : {});
|
|
684
|
+
const tool = tools.find((tool) => tool.name === binding.tool);
|
|
685
|
+
return Boolean(tool && ((binding.classification === "read" && tool.classification === "read") || opts.trust === "trusted"));
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
options.signal?.throwIfAborted();
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
if (!(await allowed()))
|
|
693
|
+
return denied();
|
|
614
694
|
const requestedOffset = args.offset ?? 0;
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// whole envelope under one key, which is v2 with a single chunk as wide
|
|
619
|
-
// as the result, and raw text before that still pays one full encode
|
|
620
|
-
// per page.
|
|
621
|
-
const current = RESULT_ENVELOPE_V3_HEADER.exec(stored.slice(0, 96));
|
|
622
|
-
const chunked = current ?? RESULT_ENVELOPE_V2_HEADER.exec(stored.slice(0, 80));
|
|
623
|
-
const inline = chunked ? null : /^connecta-result-v1:(\d+):/.exec(stored.slice(0, 64));
|
|
624
|
-
const header = chunked ?? inline;
|
|
625
|
-
// A page never exceeds the inline cap of the call that stashed it — the
|
|
626
|
-
// connector's override when it had one — whatever maxBytes asks for.
|
|
627
|
-
// Clients cut an oversized page exactly as they cut the original result,
|
|
628
|
-
// so honouring a larger request would hand the agent an error in place
|
|
629
|
-
// of its data. Entries from before the cap was recorded use the
|
|
630
|
-
// deployment-wide one.
|
|
631
|
-
const cap = current ? Number(current[3]) : globalCap;
|
|
695
|
+
const cap = Number(header[4]);
|
|
696
|
+
if (!isValidMaxResultBytes(cap))
|
|
697
|
+
return denied();
|
|
632
698
|
const maxBytes = Math.min(args.maxBytes ?? cap, cap);
|
|
633
699
|
let bytes;
|
|
634
700
|
let total;
|
|
635
701
|
let start = 0;
|
|
636
|
-
|
|
637
|
-
total = Number(header[
|
|
638
|
-
const chunkBytes =
|
|
702
|
+
{
|
|
703
|
+
total = Number(header[2]);
|
|
704
|
+
const chunkBytes = Number(header[3]);
|
|
705
|
+
if (!Number.isSafeInteger(total) ||
|
|
706
|
+
total < 0 ||
|
|
707
|
+
!Number.isSafeInteger(chunkBytes) ||
|
|
708
|
+
chunkBytes < 3 ||
|
|
709
|
+
chunkBytes % 3 !== 0)
|
|
710
|
+
return denied();
|
|
639
711
|
start = Math.floor(Math.max(0, Math.min(requestedOffset, total) - 3) / 3) * 3;
|
|
640
712
|
const end = Math.min(total, requestedOffset + maxBytes + 4);
|
|
641
713
|
bytes = new Uint8Array(Math.max(0, end - start));
|
|
642
714
|
const lastChunk = Math.floor(Math.max(end - 1, start) / chunkBytes);
|
|
643
715
|
for (let index = Math.floor(start / chunkBytes); index <= lastChunk; index++) {
|
|
644
|
-
const encoded = index === 0
|
|
645
|
-
? stored.slice(header[0].length)
|
|
646
|
-
: await read(`result:${args.id}#${index}`);
|
|
716
|
+
const encoded = index === 0 ? stored.slice(header[0].length) : await read(resultKeys.chunk(args.id, index));
|
|
647
717
|
if (encoded === false)
|
|
648
718
|
return unavailableResult();
|
|
649
719
|
if (encoded === null) {
|
|
@@ -656,16 +726,12 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
656
726
|
// alignment because every chunk boundary is a multiple of three.
|
|
657
727
|
const from = Math.max(start, chunkStart) - chunkStart;
|
|
658
728
|
const to = Math.min(end, chunkStart + chunkBytes, total) - chunkStart;
|
|
659
|
-
const binary = atob(encoded.slice(from / 3 * 4, Math.ceil(to / 3) * 4));
|
|
729
|
+
const binary = atob(encoded.slice((from / 3) * 4, Math.ceil(to / 3) * 4));
|
|
660
730
|
for (let at = from; at < to; at++) {
|
|
661
731
|
bytes[chunkStart + at - start] = binary.charCodeAt(at - from);
|
|
662
732
|
}
|
|
663
733
|
}
|
|
664
734
|
}
|
|
665
|
-
else {
|
|
666
|
-
bytes = enc.encode(stored);
|
|
667
|
-
total = bytes.length;
|
|
668
|
-
}
|
|
669
735
|
// Validated above, so no coercion is needed here — only alignment. A
|
|
670
736
|
// client that computes its own offsets can land inside a multi-byte
|
|
671
737
|
// character, which would decode as U+FFFD; the offset actually served is
|
|
@@ -685,16 +751,19 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
685
751
|
// then the page as raw text. A JSON `text` field escaped every quote and
|
|
686
752
|
// newline in the page — a second layer over JSON payloads — which
|
|
687
753
|
// inflated pages past the size clients accept and made them hard to read.
|
|
688
|
-
|
|
754
|
+
const pageHeader = {
|
|
689
755
|
resultId: args.id,
|
|
690
756
|
offset,
|
|
691
757
|
bytes: Math.max(0, end - offset),
|
|
692
758
|
totalBytes: total,
|
|
693
759
|
hasMore,
|
|
694
|
-
...(hasMore
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
760
|
+
...(hasMore ? { nextOffset: end } : {}),
|
|
761
|
+
};
|
|
762
|
+
if (!(await allowed()))
|
|
763
|
+
return denied();
|
|
764
|
+
const page = noticeFirst(pageHeader, slice);
|
|
765
|
+
page.structuredContent = { ...pageHeader, text: slice, format: "text" };
|
|
766
|
+
return page;
|
|
698
767
|
},
|
|
699
768
|
async authorizeConnector(args) {
|
|
700
769
|
const connector = registry.getConnector(args.connector);
|
|
@@ -727,8 +796,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
727
796
|
})) ?? [
|
|
728
797
|
{
|
|
729
798
|
name: "value",
|
|
730
|
-
guidance: connector.credential.description ??
|
|
731
|
-
connector.credential.label,
|
|
799
|
+
guidance: connector.credential.description ?? connector.credential.label,
|
|
732
800
|
},
|
|
733
801
|
];
|
|
734
802
|
return jsonResult({
|
|
@@ -746,49 +814,40 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
746
814
|
: "Shared credential mutation requires a signed-in human with access to this connector."),
|
|
747
815
|
});
|
|
748
816
|
}
|
|
817
|
+
if (opts.oauthConnectUnavailable || !opts.oauthConnectUrl)
|
|
818
|
+
return jsonResult({
|
|
819
|
+
connector: connector.id,
|
|
820
|
+
recovery: "unavailable",
|
|
821
|
+
message: opts.oauthConnectUnavailable ??
|
|
822
|
+
"OAuth connection requires an interactive provider and a credential vault with a handoff signing key.",
|
|
823
|
+
});
|
|
749
824
|
if (!opts.canManageAuth?.(connector.id))
|
|
750
|
-
return jsonResult({
|
|
751
|
-
|
|
825
|
+
return jsonResult({
|
|
826
|
+
connector: connector.id,
|
|
827
|
+
recovery: "unavailable",
|
|
828
|
+
message: "Your identity is not permitted to manage authentication for this connection.",
|
|
829
|
+
});
|
|
752
830
|
try {
|
|
753
|
-
const status = await connector.startAuth(ctx, args.force !== undefined ? { force: args.force } : {});
|
|
754
|
-
if (status.authorizationUrl) {
|
|
755
|
-
await registry.bindOAuthHandoff(connector.id, status.authorizationUrl);
|
|
756
|
-
}
|
|
757
|
-
if (status.state === "auth_required" && !status.authorizationUrl) {
|
|
758
|
-
// auth_required with nothing to open is a dead end for the operator.
|
|
759
|
-
return errorResult(`Connector "${connector.id}": authorization required but no URL is available — retry authorize_connector.`);
|
|
760
|
-
}
|
|
761
831
|
return jsonResult({
|
|
762
832
|
connector: connector.id,
|
|
763
833
|
recovery: "oauth",
|
|
764
|
-
status:
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
instructions: "Have the operator open authorizationUrl in a browser and complete the consent flow. The provider then redirects back to this server's /oauth/callback/<connector> route, which finishes the flow automatically. " +
|
|
769
|
-
oauthFollowUp(connector.id),
|
|
770
|
-
}
|
|
771
|
-
: {}),
|
|
772
|
-
...(status.message ? { message: status.message } : {}),
|
|
834
|
+
status: "auth_required",
|
|
835
|
+
authorizationUrl: await opts.oauthConnectUrl(connector.id, args.force),
|
|
836
|
+
instructions: "Open authorizationUrl in a browser and sign in as the user who requested this connection. Connecta verifies your identity and permission before starting consent. " +
|
|
837
|
+
oauthFollowUp(connector.id),
|
|
773
838
|
});
|
|
774
839
|
}
|
|
775
840
|
catch (err) {
|
|
776
841
|
return errorResult(msg(err));
|
|
777
842
|
}
|
|
778
|
-
finally {
|
|
779
|
-
// Auth state may have changed — even on a throw or a half-wiped force —
|
|
780
|
-
// so don't serve a stale tool list.
|
|
781
|
-
await registry.invalidateStored(connector.id);
|
|
782
|
-
}
|
|
783
843
|
},
|
|
784
844
|
};
|
|
785
845
|
}
|
|
786
|
-
const SEARCH_DESC = `Use top-level search for catalog inspection or approval-required work before call_destructive_tool. Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds tools that run unasked; "approvalRequired" finds the fail-closed complement. These filters grant no authority. Empty query browses.`;
|
|
787
|
-
const CALL_DESC =
|
|
846
|
+
const SEARCH_DESC = `Use top-level search for catalog inspection or approval-required work before call_destructive_tool. Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds tools that run unasked; "approvalRequired" finds the fail-closed complement. These filters grant no authority. Empty query browses. Returns { catalogErrors, tools, total, offset, limit, hasMore }; read catalogErrors and absence before selecting a tool. Compact schemas have schemaFormat="text".`;
|
|
847
|
+
const CALL_DESC = "Call one known-address tool explicitly annotated readOnlyHint: true. Use execute_code for unknown-address, multiple, dependent, or reduced read-only work. Unannotated or write-capable tools fail closed to call_destructive_tool. A truncated result carries a connecta.result action.";
|
|
788
848
|
const CALL_DESTRUCTIVE_DESC = "Call any tool not explicitly annotated readOnlyHint: true. Include a short reason for the human reviewer after checking the schema and consequences. The reason grants no authority and is not sent downstream.";
|
|
789
|
-
const
|
|
790
|
-
const
|
|
791
|
-
const SKILLS_DESC = 'List or fetch on-demand guidance. Fetch usage only when the always-loaded instructions are insufficient or a program needs repair.';
|
|
849
|
+
const AUTHORIZE_DESC = "Use after auth_required. Returns an OAuth or operator-credential handoff, or reports required deployment configuration. force=true requests an OAuth restart when the verified user opens the /connect URL; this tool never accepts credentials.";
|
|
850
|
+
const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the always-loaded instructions are insufficient or a program needs repair.";
|
|
792
851
|
/**
|
|
793
852
|
* Sentences appended to a meta-tool description only when this connection
|
|
794
853
|
* actually has connector guides. Tool descriptions are always-loaded context,
|
|
@@ -804,9 +863,7 @@ const GUIDE_NOTES = {
|
|
|
804
863
|
};
|
|
805
864
|
/** `base`, plus its guide note when any VISIBLE connector carries a guide. */
|
|
806
865
|
function describedFor(registry, base, note) {
|
|
807
|
-
return hasConnectorGuides(registry.listConnectors())
|
|
808
|
-
? base + GUIDE_NOTES[note]
|
|
809
|
-
: base;
|
|
866
|
+
return hasConnectorGuides(registry.listConnectors()) ? base + GUIDE_NOTES[note] : base;
|
|
810
867
|
}
|
|
811
868
|
/**
|
|
812
869
|
* Connecta refuses downstream tools that are not explicitly annotated
|
|
@@ -827,7 +884,7 @@ const READ_ONLY_LOCAL = {
|
|
|
827
884
|
openWorldHint: false,
|
|
828
885
|
};
|
|
829
886
|
const CALL_INPUT_SCHEMA = {
|
|
830
|
-
address: z.string(),
|
|
887
|
+
address: z.string().meta({ "x-mcp-header": "Address" }),
|
|
831
888
|
args: z.record(z.string(), z.unknown()).optional(),
|
|
832
889
|
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
833
890
|
timeoutMs: z.number().int().positive().optional(),
|
|
@@ -860,18 +917,9 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
|
|
|
860
917
|
connector: z.string(),
|
|
861
918
|
force: z.boolean().optional(),
|
|
862
919
|
}));
|
|
863
|
-
const GET_RESULT_INPUT = advertisedSchema(z.object({
|
|
864
|
-
id: z.string(),
|
|
865
|
-
// Both bounds are the shared rules (isValidResultOffset,
|
|
866
|
-
// isValidMaxResultBytes) expressed for the wire: spelling them against
|
|
867
|
-
// the same constants keeps the schema from drifting away from the
|
|
868
|
-
// in-handler checks if either floor ever moves.
|
|
869
|
-
offset: z.number().int().min(MIN_RESULT_OFFSET).optional(),
|
|
870
|
-
maxBytes: z.number().int().min(MIN_MAX_RESULT_BYTES).optional(),
|
|
871
|
-
}));
|
|
872
920
|
/**
|
|
873
|
-
* Register the
|
|
874
|
-
* `registerExecuteTool` adds the
|
|
921
|
+
* Register the five explicit meta-tools onto an McpServer instance.
|
|
922
|
+
* `registerExecuteTool` adds the sixth, `execute_code`. Broad discovery and
|
|
875
923
|
* multi-call work uses discovery and ordinary JavaScript promises inside a
|
|
876
924
|
* program, which `execute_code` builds over the same
|
|
877
925
|
* `CatalogService` and `InvocationService` these handlers use — one shared
|
|
@@ -879,53 +927,81 @@ const GET_RESULT_INPUT = advertisedSchema(z.object({
|
|
|
879
927
|
*/
|
|
880
928
|
export function registerMetaTools(server, registry, ctx) {
|
|
881
929
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
930
|
+
downstreamInput: Boolean(ctx.authElicitation),
|
|
931
|
+
requestScope: ctx.requestScope,
|
|
932
|
+
trust: ctx.trust,
|
|
882
933
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
883
934
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
884
935
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
885
936
|
activity: ctx.activity,
|
|
937
|
+
client: ctx.client,
|
|
886
938
|
canManageAuth: ctx.canManageAuth,
|
|
887
939
|
credentialHandoffUrl: ctx.credentialHandoffUrl,
|
|
940
|
+
oauthConnectUrl: ctx.oauthConnectUrl,
|
|
941
|
+
oauthConnectUnavailable: ctx.oauthConnectUnavailable,
|
|
888
942
|
requestSignal: ctx.requestSignal,
|
|
889
943
|
defer: ctx.defer,
|
|
890
|
-
approval: ctx.approval,
|
|
891
944
|
});
|
|
945
|
+
const bindRequest = (request) => {
|
|
946
|
+
if (ctx.client)
|
|
947
|
+
bindMcpClient(request, ctx.client, ctx.activity);
|
|
948
|
+
};
|
|
892
949
|
server.registerTool("skills", {
|
|
893
950
|
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
894
951
|
inputSchema: SKILLS_INPUT,
|
|
952
|
+
outputSchema: SKILLS_OUTPUT,
|
|
895
953
|
annotations: READ_ONLY_LOCAL,
|
|
896
|
-
}, async (args) =>
|
|
954
|
+
}, async (args, request) => {
|
|
955
|
+
bindRequest(request);
|
|
956
|
+
return mt.skills(args);
|
|
957
|
+
});
|
|
897
958
|
server.registerTool("search_tools", {
|
|
898
959
|
description: describedFor(registry, SEARCH_DESC, "search"),
|
|
899
960
|
inputSchema: SEARCH_INPUT,
|
|
961
|
+
outputSchema: SEARCH_OUTPUT,
|
|
900
962
|
annotations: READ_ONLY_REMOTE,
|
|
901
|
-
}, async (args) =>
|
|
963
|
+
}, async (args, request) => {
|
|
964
|
+
bindRequest(request);
|
|
965
|
+
return mt.searchTools(args);
|
|
966
|
+
});
|
|
902
967
|
server.registerTool("call_tool", {
|
|
903
968
|
description: CALL_DESC,
|
|
904
969
|
inputSchema: CALL_INPUT,
|
|
970
|
+
outputSchema: CALL_OUTPUT,
|
|
905
971
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
906
972
|
// anything else is refused and routed to call_destructive_tool.
|
|
907
973
|
annotations: READ_ONLY_REMOTE,
|
|
908
|
-
}, async (args) =>
|
|
974
|
+
}, async (args, request) => {
|
|
975
|
+
bindRequest(request);
|
|
976
|
+
return ctx.authElicitation
|
|
977
|
+
? ctx.authElicitation.run("call_tool", args, request, () => mt.callTool(args))
|
|
978
|
+
: mt.callTool(args);
|
|
979
|
+
});
|
|
909
980
|
server.registerTool("call_destructive_tool", {
|
|
910
981
|
description: describedFor(registry, CALL_DESTRUCTIVE_DESC, "destructive"),
|
|
911
982
|
inputSchema: CALL_DESTRUCTIVE_INPUT,
|
|
983
|
+
outputSchema: CALL_OUTPUT,
|
|
912
984
|
annotations: {
|
|
913
985
|
destructiveHint: true,
|
|
914
986
|
readOnlyHint: false,
|
|
915
987
|
openWorldHint: true,
|
|
916
988
|
},
|
|
917
|
-
}, async (args) => {
|
|
989
|
+
}, async (args, request) => {
|
|
990
|
+
bindRequest(request);
|
|
918
991
|
// `reason` is the host's to display and connecta's to keep out of the
|
|
919
992
|
// downstream call, so this destructuring is the whole of its handling:
|
|
920
993
|
// nothing below reads it. Dropping it is also what makes an empty or
|
|
921
994
|
// whitespace-only one "absent" rather than a validation failure — there
|
|
922
995
|
// is no field left for it to be absent from.
|
|
923
996
|
const { reason: _hostContext, ...call } = args;
|
|
924
|
-
return
|
|
997
|
+
return ctx.authElicitation
|
|
998
|
+
? ctx.authElicitation.run("call_destructive_tool", call, request, () => mt.callDestructiveTool(call))
|
|
999
|
+
: mt.callDestructiveTool(call);
|
|
925
1000
|
});
|
|
926
1001
|
server.registerTool("authorize_connector", {
|
|
927
1002
|
description: AUTHORIZE_DESC,
|
|
928
1003
|
inputSchema: AUTHORIZE_INPUT,
|
|
1004
|
+
outputSchema: AUTHORIZE_OUTPUT,
|
|
929
1005
|
// Starts (or with force, resets) a downstream OAuth flow — it changes
|
|
930
1006
|
// stored connector auth state, so it is deliberately not read-only.
|
|
931
1007
|
annotations: {
|
|
@@ -933,10 +1009,10 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
933
1009
|
destructiveHint: false,
|
|
934
1010
|
openWorldHint: true,
|
|
935
1011
|
},
|
|
936
|
-
}, async (args) =>
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
1012
|
+
}, async (args, request) => {
|
|
1013
|
+
bindRequest(request);
|
|
1014
|
+
return ctx.authElicitation
|
|
1015
|
+
? ctx.authElicitation.run("authorize_connector", args, request, () => mt.authorizeConnector(args))
|
|
1016
|
+
: mt.authorizeConnector(args);
|
|
1017
|
+
});
|
|
942
1018
|
}
|