@zackbart/connecta 0.28.1 → 0.30.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 +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -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/argument-redaction.js +230 -0
- 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 +1584 -1580
- 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 +93 -71
- package/dist/auth-elicitation.js +288 -0
- package/dist/authorization-handoff.js +111 -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 +409 -225
- 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 +633 -0
- package/dist/config.js +508 -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 +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +87 -38
- 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 +1774 -320
- 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 +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- 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 +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -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 +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +262 -79
- 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 +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- 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 +194 -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-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- 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/_shared/request-token.js +83 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- 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/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -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.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- 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.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- 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 +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -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 +365 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -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 +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -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 +336 -56
- 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 +146 -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 +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -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 +126 -54
- 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 +140 -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/basecamp.js +0 -445
- 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,18 +1,26 @@
|
|
|
1
|
+
import { authorizationHandoff, bindAuthorizationHandoffs } from "./authorization-handoff.js";
|
|
2
|
+
import { bindMcpClient } from "./mcp-client-context.js";
|
|
3
|
+
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
1
4
|
import { z } from "zod";
|
|
2
5
|
import { advertisedSchema } from "./advertised-schema.js";
|
|
3
|
-
import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError,
|
|
6
|
+
import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, flatSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
|
|
4
7
|
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
5
|
-
import { boundedEchoText, msg } from "./errors.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
+
import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
|
|
9
|
+
import { failureRecord, logFailure } from "./operator-record.js";
|
|
10
|
+
import { guardExecuteResultValue, MAX_EXECUTE_RESULT_CHARS, serializeResultText } from "./executor-result.js";
|
|
11
|
+
import { InvocationService } from "./invocation.js";
|
|
8
12
|
import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
|
|
9
|
-
import { hasConnectorGuides,
|
|
10
|
-
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
import { hasConnectorGuides, SkillsRegistry } from "./skills.js";
|
|
14
|
+
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs } from "./timeout.js";
|
|
15
|
+
import { AUTHORIZE_OUTPUT, SEARCH_OUTPUT, SKILLS_OUTPUT } from "./meta-output.js";
|
|
16
|
+
import { RESULT_TTL_SECONDS, resultKeys } from "./storage/keys.js";
|
|
17
|
+
import { agentOutputOperations, sentSecretsForRequest } from "./sent-secrets.js";
|
|
18
|
+
import { captureDownstreamInput } from "./downstream-input.js";
|
|
19
|
+
import { PROGRAM_RESULT_WIRE_BYTES } from "./program-result.js";
|
|
20
|
+
export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT };
|
|
14
21
|
const enc = new TextEncoder();
|
|
15
|
-
|
|
22
|
+
// Each page is a slice of payload bytes, so a leading U+FEFF is data.
|
|
23
|
+
const dec = new TextDecoder("utf-8", { ignoreBOM: true, fatal: false });
|
|
16
24
|
export function jsonResult(obj, text = JSON.stringify(obj)) {
|
|
17
25
|
return {
|
|
18
26
|
content: [{ type: "text", text }],
|
|
@@ -21,7 +29,7 @@ export function jsonResult(obj, text = JSON.stringify(obj)) {
|
|
|
21
29
|
: {}),
|
|
22
30
|
};
|
|
23
31
|
}
|
|
24
|
-
|
|
32
|
+
function errorResult(message) {
|
|
25
33
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
26
34
|
}
|
|
27
35
|
function discoveryErrorResult(error) {
|
|
@@ -57,7 +65,7 @@ async function discoveryResult(operation, hint) {
|
|
|
57
65
|
function isContinuationByte(b) {
|
|
58
66
|
return b !== undefined && (b & 0xc0) === 0x80;
|
|
59
67
|
}
|
|
60
|
-
/** Smallest accepted `
|
|
68
|
+
/** Smallest accepted `connecta.result` byte offset. */
|
|
61
69
|
const MIN_RESULT_OFFSET = 0;
|
|
62
70
|
/** Whole-byte offset accepted by the result representation documented in
|
|
63
71
|
* documentation/meta-tools.md#result-representation. */
|
|
@@ -101,17 +109,9 @@ export function alignEndToCharBoundary(bytes, offset, end, total) {
|
|
|
101
109
|
}
|
|
102
110
|
return e;
|
|
103
111
|
}
|
|
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+):`);
|
|
112
|
+
/** Every readable stash carries host-admitted bindings; older entries fail closed. */
|
|
113
|
+
const RESULT_ENVELOPE_V4 = "connecta-result-v4:";
|
|
114
|
+
const RESULT_ENVELOPE_V4_HEADER = /^connecta-result-v4:([A-Za-z0-9+/=]+):(\d+):(\d+):(\d+):/;
|
|
115
115
|
/**
|
|
116
116
|
* Smallest chunk of result text stored under one key. A multiple of three so
|
|
117
117
|
* every chunk's base64 stands alone and a byte offset inside it lands on a
|
|
@@ -122,7 +122,7 @@ const RESULT_CHUNK_BYTES = 49_152;
|
|
|
122
122
|
/**
|
|
123
123
|
* Keys one stashed result may occupy. Chunking trades write count for read
|
|
124
124
|
* count, and both are real: every chunk is a storage write at stash time, and
|
|
125
|
-
*
|
|
125
|
+
* a SQL store commits one row per write. Above roughly 1.5 MB the
|
|
126
126
|
* chunks widen instead of multiplying, so a result costs a bounded number of
|
|
127
127
|
* writes and a page still reads a small fraction of it.
|
|
128
128
|
*/
|
|
@@ -145,6 +145,23 @@ function base64Of(bytes) {
|
|
|
145
145
|
}
|
|
146
146
|
/** Opens a write's notice, ahead of any paging instruction. */
|
|
147
147
|
const WRITE_ALREADY_RAN = "This write already ran: do not call it again to see its result.";
|
|
148
|
+
/** UTF-8 cannot represent lone UTF-16 surrogates without changing them. */
|
|
149
|
+
function isWellFormedText(text) {
|
|
150
|
+
const native = String.prototype.isWellFormed;
|
|
151
|
+
if (native)
|
|
152
|
+
return native.call(text);
|
|
153
|
+
for (let at = 0; at < text.length; at++) {
|
|
154
|
+
const unit = text.charCodeAt(at);
|
|
155
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
156
|
+
const next = text.charCodeAt(++at);
|
|
157
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff)
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
148
165
|
/**
|
|
149
166
|
* Offers a read the route meta-tools.md prefers for finding something in it:
|
|
150
167
|
* repeat the call inside a program and reduce it there. Paging comes second,
|
|
@@ -156,18 +173,20 @@ const WRITE_ALREADY_RAN = "This write already ran: do not call it again to see i
|
|
|
156
173
|
const READ_REDUCE_FIRST = "To find something specific, repeat this read inside execute_code with connecta.call and filter or search the result there, returning only what matters";
|
|
157
174
|
function pagingHint(results, totalBytes, preview) {
|
|
158
175
|
if (!results.write) {
|
|
176
|
+
if (results.binding.program)
|
|
177
|
+
return "filter/map/slice data inside execute_code and return only what you need; page this completed return with connecta.result using nextAction.";
|
|
159
178
|
const [shown, full] = preview.kind === "prefix"
|
|
160
179
|
? [`Bytes 0-${preview.bytes} of ${totalBytes} follow.`, "to read it in full, page the rest"]
|
|
161
180
|
: preview.kind === "text-of-envelope"
|
|
162
181
|
? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
|
|
163
182
|
: [`The result is ${totalBytes} bytes.`, "to read it in full, page it"];
|
|
164
|
-
return `${shown} ${READ_REDUCE_FIRST}; ${full} with
|
|
183
|
+
return `${shown} ${READ_REDUCE_FIRST}; ${full} with connecta.result using nextAction.`;
|
|
165
184
|
}
|
|
166
185
|
const body = preview.kind === "prefix"
|
|
167
|
-
? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with
|
|
186
|
+
? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with connecta.result using nextAction.`
|
|
168
187
|
: 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
|
|
188
|
+
? `Its text follows; page the full ${totalBytes}-byte content array as JSON with connecta.result using nextAction.`
|
|
189
|
+
: `Page all ${totalBytes} bytes with connecta.result using nextAction.`;
|
|
171
190
|
return `${WRITE_ALREADY_RAN} ${body}`;
|
|
172
191
|
}
|
|
173
192
|
/**
|
|
@@ -180,16 +199,32 @@ function pagingHint(results, totalBytes, preview) {
|
|
|
180
199
|
* documentation/meta-tools.md#result-representation). It is one compact JSON
|
|
181
200
|
* line with no raw newline, so the preview starts after the first `\n`.
|
|
182
201
|
*/
|
|
183
|
-
async function stashResult(bytes, results, preview) {
|
|
202
|
+
async function stashResult(bytes, results, preview, rawText) {
|
|
184
203
|
const totalBytes = bytes.length;
|
|
204
|
+
// Direct pages promise offsets into the original UTF-8 text. JSON-escaping
|
|
205
|
+
// raw text would change that contract; never stash the encoder's replacement
|
|
206
|
+
// characters as if they were the completed call's output.
|
|
207
|
+
if (rawText !== undefined && !isWellFormedText(rawText))
|
|
208
|
+
return {
|
|
209
|
+
truncated: true,
|
|
210
|
+
totalBytes,
|
|
211
|
+
hint: `${results.write ? WRITE_ALREADY_RAN + " " : ""}Paging is unavailable: text contains unpaired surrogates and can't be paged as text; request the value as JSON (value mode or a JSON-returning call) to recover it.`,
|
|
212
|
+
};
|
|
213
|
+
if (!results.pageable)
|
|
214
|
+
return {
|
|
215
|
+
truncated: true,
|
|
216
|
+
totalBytes,
|
|
217
|
+
hint: `${WRITE_ALREADY_RAN} Paging is unavailable for write results on read-only pools.`,
|
|
218
|
+
};
|
|
185
219
|
const id = crypto.randomUUID();
|
|
186
220
|
try {
|
|
187
221
|
// Base64 permits byte-range decoding, and splitting the envelope across
|
|
188
222
|
// 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
|
|
223
|
+
// the whole result (issue #540). Chunk 0 carries the header; the connecta.result
|
|
190
224
|
// reader below maps a byte offset back to chunk index and base64 quad.
|
|
191
225
|
const chunkBytes = resultChunkBytes(totalBytes);
|
|
192
|
-
const
|
|
226
|
+
const binding = base64Of(enc.encode(JSON.stringify(results.binding)));
|
|
227
|
+
const chunks = [`${RESULT_ENVELOPE_V4}${binding}:${totalBytes}:${chunkBytes}:${results.cap}:`];
|
|
193
228
|
for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
|
|
194
229
|
const chunk = base64Of(bytes.subarray(offset, offset + chunkBytes));
|
|
195
230
|
if (offset === 0)
|
|
@@ -197,7 +232,7 @@ async function stashResult(bytes, results, preview) {
|
|
|
197
232
|
else
|
|
198
233
|
chunks.push(chunk);
|
|
199
234
|
}
|
|
200
|
-
if (!await results.set(
|
|
235
|
+
if (!(await results.set(id, chunks, RESULT_TTL_SECONDS))) {
|
|
201
236
|
throw new Error("Result stash capacity exhausted");
|
|
202
237
|
}
|
|
203
238
|
}
|
|
@@ -223,11 +258,84 @@ async function stashResult(bytes, results, preview) {
|
|
|
223
258
|
resultId: id,
|
|
224
259
|
totalBytes,
|
|
225
260
|
hint: pagingHint(results, totalBytes, preview),
|
|
261
|
+
nextOffset: preview.kind === "prefix" ? preview.bytes : 0,
|
|
226
262
|
nextAction: {
|
|
227
|
-
tool: "
|
|
228
|
-
|
|
263
|
+
tool: "execute_code",
|
|
264
|
+
// A byte may cost six JSON characters when escaped. Leave room for the
|
|
265
|
+
// result id, page counters, and continuation metadata in the returned page.
|
|
266
|
+
arguments: {
|
|
267
|
+
code: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${Math.floor((MAX_EXECUTE_RESULT_CHARS - 1024) / 6)} })`,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/** Program calls use the direct-call envelope, ledger, expiry and authority. */
|
|
273
|
+
export async function pageProgramResult(registry, baseUrl, resolved, value, format, secrets, trust, requestScope, wireBytes = PROGRAM_RESULT_WIRE_BYTES) {
|
|
274
|
+
const data = secrets.redact(value);
|
|
275
|
+
const wire = JSON.stringify({ data, format });
|
|
276
|
+
if (enc.encode(wire).length <= wireBytes)
|
|
277
|
+
return { data, format };
|
|
278
|
+
const text = secrets.text(format === "text" ? String(data) : serializeResultText(data));
|
|
279
|
+
const bytes = enc.encode(text);
|
|
280
|
+
const write = resolved.definition.classification !== "read";
|
|
281
|
+
const notice = await stashResult(bytes, {
|
|
282
|
+
pageable: !write || trust === "trusted",
|
|
283
|
+
binding: {
|
|
284
|
+
identity: registry.resultIdentity(),
|
|
285
|
+
baseUrl,
|
|
286
|
+
connector: resolved.connector.id,
|
|
287
|
+
tool: resolved.definition.name,
|
|
288
|
+
classification: write ? "write" : "read",
|
|
289
|
+
},
|
|
290
|
+
write,
|
|
291
|
+
cap: resolveMaxResultBytes(resolved.connector.maxResultBytes, registry.maxResultBytes),
|
|
292
|
+
set: (id, chunks, ttl) => registry.stashResult(id, chunks, ttl),
|
|
293
|
+
warn: () => logFailure(registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger, "result paging unavailable", failureRecord({ connector: resolved.connector.id, tool: resolved.definition })),
|
|
294
|
+
}, { kind: "none" }, format === "text" ? text : undefined);
|
|
295
|
+
return {
|
|
296
|
+
format: "paged",
|
|
297
|
+
valueFormat: format,
|
|
298
|
+
...(notice.resultId ? { resultId: notice.resultId } : {}),
|
|
299
|
+
totalBytes: bytes.length,
|
|
300
|
+
truncated: true,
|
|
301
|
+
hint: notice.resultId
|
|
302
|
+
? `${write ? WRITE_ALREADY_RAN + " " : ""}Page this result with connecta.result(handle); do not repeat the call for output.`
|
|
303
|
+
: notice.hint,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/** Program returns share the direct stash, with all host-admitted call dependencies. */
|
|
307
|
+
export async function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, trust, logger, maxChars, completedWrite) {
|
|
308
|
+
const serialized = serializeResultText(value);
|
|
309
|
+
if (serialized.length <= maxChars)
|
|
310
|
+
return value;
|
|
311
|
+
const text = secrets.text(serialized);
|
|
312
|
+
const bytes = enc.encode(text);
|
|
313
|
+
const write = dependencies.some((dependency) => dependency.classification === "write");
|
|
314
|
+
const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
|
|
315
|
+
const results = {
|
|
316
|
+
secrets,
|
|
317
|
+
pageable: !write || trust === "trusted",
|
|
318
|
+
binding: {
|
|
319
|
+
identity: registry.resultIdentity(),
|
|
320
|
+
baseUrl,
|
|
321
|
+
classification: write ? "write" : "read",
|
|
322
|
+
program: dependencies,
|
|
229
323
|
},
|
|
324
|
+
write: completedWrite,
|
|
325
|
+
cap,
|
|
326
|
+
set: (id, chunks, ttl) => registry.stashResult(id, chunks, ttl),
|
|
327
|
+
warn: () => logFailure(logger, "result paging unavailable", failureRecord({})),
|
|
230
328
|
};
|
|
329
|
+
const notice = await stashResult(bytes, results, { kind: "none" }, text);
|
|
330
|
+
// Put the entire recovery route ahead of the preview in both response forms.
|
|
331
|
+
// A client retaining only the first 2,000 characters still sees the action.
|
|
332
|
+
if (notice.resultId) {
|
|
333
|
+
const base = { ...notice, totalChars: text.length, preview: "" };
|
|
334
|
+
const budget = Math.max(0, maxChars - JSON.stringify(base).length);
|
|
335
|
+
return { ...base, preview: escapedHeadOf(bytes, Math.min(512, budget)) };
|
|
336
|
+
}
|
|
337
|
+
const fallback = guardExecuteResultValue(secrets.redact(value), maxChars, notice.hint);
|
|
338
|
+
return { truncated: true, totalChars: fallback.totalChars, hint: fallback.hint, preview: fallback.preview };
|
|
231
339
|
}
|
|
232
340
|
/** The first `cap` bytes of `bytes`, ending on a character boundary. */
|
|
233
341
|
function headOf(bytes, cap) {
|
|
@@ -235,11 +343,13 @@ function headOf(bytes, cap) {
|
|
|
235
343
|
}
|
|
236
344
|
/** One text block: the notice line, then the preview. */
|
|
237
345
|
function noticeFirst(notice, preview) {
|
|
238
|
-
return {
|
|
346
|
+
return {
|
|
347
|
+
content: [{ type: "text", text: `${JSON.stringify(notice)}\n${preview}` }],
|
|
348
|
+
};
|
|
239
349
|
}
|
|
240
350
|
/**
|
|
241
351
|
* 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
|
|
352
|
+
* full text and return a JSON truncation notice pointing at connecta.result,
|
|
243
353
|
* followed by the first `cap` bytes. The preview is a byte prefix of what
|
|
244
354
|
* pages, so the notice's next action continues where it stops. `bytes` is
|
|
245
355
|
* `text` already encoded, so a caller that had to measure it to make this
|
|
@@ -252,6 +362,10 @@ async function guardEncoded(text, bytes, results, cap) {
|
|
|
252
362
|
truncated: false,
|
|
253
363
|
};
|
|
254
364
|
}
|
|
365
|
+
if (!isWellFormedText(text)) {
|
|
366
|
+
const notice = await stashResult(bytes, results, { kind: "none" }, text);
|
|
367
|
+
return { result: noticeFirst(notice, ""), truncated: true };
|
|
368
|
+
}
|
|
255
369
|
const head = headOf(bytes, cap);
|
|
256
370
|
const notice = await stashResult(bytes, results, {
|
|
257
371
|
kind: "prefix",
|
|
@@ -266,21 +380,25 @@ async function guardText(text, results, cap) {
|
|
|
266
380
|
// Normalizing at the door means the size check below always measures exactly
|
|
267
381
|
// the text that is emitted, and no future caller can launder a non-string
|
|
268
382
|
// through it the way issue #42 describes.
|
|
269
|
-
const
|
|
383
|
+
const serialized = typeof text === "string" ? text : serializeResultText(text);
|
|
384
|
+
const body = results.secrets?.text(serialized) ?? serialized;
|
|
270
385
|
return guardEncoded(body, enc.encode(body), results, cap);
|
|
271
386
|
}
|
|
272
387
|
/** Store an oversized JSON value and replace it with a page handle. */
|
|
273
388
|
async function guardValue(value, results, cap) {
|
|
274
|
-
const
|
|
389
|
+
const serialized = serializeResultText(value);
|
|
390
|
+
const text = results.secrets?.text(serialized) ?? serialized;
|
|
275
391
|
const bytes = enc.encode(text);
|
|
276
392
|
if (bytes.length <= cap)
|
|
277
|
-
return { result: value, truncated: false };
|
|
393
|
+
return { result: results.secrets?.redact(value) ?? value, truncated: false };
|
|
278
394
|
const notice = await stashResult(bytes, results, { kind: "none" });
|
|
279
395
|
return {
|
|
280
|
-
result: notice.resultId
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
396
|
+
result: notice.resultId
|
|
397
|
+
? notice
|
|
398
|
+
: {
|
|
399
|
+
...notice,
|
|
400
|
+
preview: escapedHeadOf(bytes, cap),
|
|
401
|
+
},
|
|
284
402
|
truncated: true,
|
|
285
403
|
};
|
|
286
404
|
}
|
|
@@ -317,12 +435,13 @@ function escapedHeadOf(bytes, cap) {
|
|
|
317
435
|
*
|
|
318
436
|
* Over the cap, what a client gets depends on whether a preview is usable.
|
|
319
437
|
* Several text blocks preview as their text joined by newlines, while
|
|
320
|
-
*
|
|
438
|
+
* connecta.result pages the envelope from offset 0, where the block boundaries
|
|
321
439
|
* live. An envelope carrying non-text blocks is replaced by the notice alone:
|
|
322
440
|
* 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 `
|
|
441
|
+
* the full envelope is stashed and pages through `connecta.result`.
|
|
324
442
|
*/
|
|
325
443
|
async function guardContent(content, results, cap) {
|
|
444
|
+
content = results.secrets?.redact({ content }).content ?? content;
|
|
326
445
|
const only = content.length === 1 ? content[0] : undefined;
|
|
327
446
|
if (only?.type === "text" && typeof only.text === "string") {
|
|
328
447
|
const bytes = enc.encode(only.text);
|
|
@@ -333,7 +452,8 @@ async function guardContent(content, results, cap) {
|
|
|
333
452
|
}
|
|
334
453
|
let text;
|
|
335
454
|
try {
|
|
336
|
-
|
|
455
|
+
const serialized = JSON.stringify(content);
|
|
456
|
+
text = results.secrets?.text(serialized) ?? serialized;
|
|
337
457
|
}
|
|
338
458
|
catch {
|
|
339
459
|
// A block carrying a BigInt or a cycle cannot be serialized, so it cannot
|
|
@@ -362,13 +482,6 @@ async function guardContent(content, results, cap) {
|
|
|
362
482
|
truncated: true,
|
|
363
483
|
};
|
|
364
484
|
}
|
|
365
|
-
/**
|
|
366
|
-
* The sentence that closes the OAuth handoff, telling the operator's agent how
|
|
367
|
-
* to confirm the flow landed through the one surface it can call.
|
|
368
|
-
*/
|
|
369
|
-
function oauthFollowUp(connectorId) {
|
|
370
|
-
return `Then retry the original call; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside execute_code confirms the catalog now loads.`;
|
|
371
|
-
}
|
|
372
485
|
/**
|
|
373
486
|
* Every meta-tool handler over a registry, one per registered tool. Exported for
|
|
374
487
|
* direct testing; registerMetaTools() wires the six explicit tools onto an
|
|
@@ -385,40 +498,56 @@ function oauthFollowUp(connectorId) {
|
|
|
385
498
|
* override each have one runtime source of truth.
|
|
386
499
|
*/
|
|
387
500
|
export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
501
|
+
return agentOutputOperations((requestScope) => metaToolsForRequest(registry, baseUrl, { ...opts, requestScope }), opts.requestScope);
|
|
502
|
+
}
|
|
503
|
+
function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
388
504
|
// Already normalized and warned about at registry construction.
|
|
389
505
|
const globalCap = registry.maxResultBytes;
|
|
390
|
-
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
|
|
506
|
+
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs) ?? CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
|
|
391
507
|
const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
392
508
|
const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
|
|
393
509
|
// createMetaTools() is called once per inbound MCP request. Sharing this
|
|
394
510
|
// identity lets remote connectors reuse one downstream client inside that
|
|
395
511
|
// request without leaking request-bound I/O into the next one.
|
|
396
|
-
const requestScope = {};
|
|
512
|
+
const requestScope = opts.requestScope ?? {};
|
|
513
|
+
if (opts.canManageAuth || opts.oauthConnectUrl || opts.credentialHandoffUrl || opts.oauthConnectUnavailable) {
|
|
514
|
+
bindAuthorizationHandoffs(requestScope, (connector) => authorizationHandoff(registry, baseUrl, requestScope, opts, { connector }));
|
|
515
|
+
}
|
|
516
|
+
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
517
|
+
const skills = new SkillsRegistry(registry, baseUrl, {
|
|
518
|
+
requestScope,
|
|
519
|
+
requestSignal: opts.requestSignal,
|
|
520
|
+
probeTimeoutMs,
|
|
521
|
+
defer: opts.defer,
|
|
522
|
+
});
|
|
397
523
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
398
524
|
requestScope,
|
|
399
525
|
probeTimeoutMs,
|
|
400
526
|
concurrency: discoveryConcurrency,
|
|
401
527
|
defer: opts.defer,
|
|
402
528
|
requestSignal: opts.requestSignal,
|
|
403
|
-
approval: opts.approval,
|
|
404
529
|
// searchRoute keeps its top-level default. In-program callers use a
|
|
405
530
|
// separate CatalogService configured for connecta.search.
|
|
406
531
|
});
|
|
407
532
|
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
408
533
|
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
409
|
-
async function runCall(call, source
|
|
534
|
+
async function runCall(call, source) {
|
|
410
535
|
const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
|
|
411
536
|
const outcome = await invocation.invoke(call.address, call.args ?? {}, {
|
|
412
537
|
source,
|
|
413
|
-
|
|
414
|
-
? { allowDestructive: options.allowDestructive }
|
|
415
|
-
: {}),
|
|
538
|
+
sentSecrets,
|
|
416
539
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
417
|
-
...(opts.requestSignal !== undefined
|
|
418
|
-
? { requestSignal: opts.requestSignal }
|
|
419
|
-
: {}),
|
|
540
|
+
...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
|
|
420
541
|
unwrapResult: call.resultMode === "value",
|
|
421
|
-
|
|
542
|
+
...(opts.downstreamInput
|
|
543
|
+
? {
|
|
544
|
+
processInputRequired: async (result, resolved, secrets) => {
|
|
545
|
+
await captureDownstreamInput(requestScope, resolved.connector.id, `${resolved.connector.id}.${resolved.toolName}`, result, secrets);
|
|
546
|
+
return { toolResult: { content: [] } };
|
|
547
|
+
},
|
|
548
|
+
}
|
|
549
|
+
: {}),
|
|
550
|
+
processResult: async (result, resolved, secrets, format) => {
|
|
422
551
|
// Result-size cap for THIS call: the connector's own override wins,
|
|
423
552
|
// then the deployment-wide value, then the built-in default (already
|
|
424
553
|
// folded into `globalCap`). Resolved per call so one request can
|
|
@@ -427,13 +556,21 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
427
556
|
// here, so the connector simply inherits `globalCap`.
|
|
428
557
|
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
|
|
429
558
|
const results = {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
559
|
+
secrets,
|
|
560
|
+
pageable: resolved.definition.classification === "read" || opts.trust === "trusted",
|
|
561
|
+
binding: {
|
|
562
|
+
identity: registry.resultIdentity(),
|
|
563
|
+
baseUrl,
|
|
434
564
|
connector: resolved.connector.id,
|
|
435
|
-
tool: resolved.
|
|
436
|
-
|
|
565
|
+
tool: resolved.definition.name,
|
|
566
|
+
classification: resolved.definition.classification === "read" ? "read" : "write",
|
|
567
|
+
},
|
|
568
|
+
write: resolved.definition.classification !== "read",
|
|
569
|
+
cap,
|
|
570
|
+
set: (id, value, ttlSeconds) => registry.stashResult(id, value, ttlSeconds),
|
|
571
|
+
// The catalog entry, never `toolName`: a record names a tool
|
|
572
|
+
// only through the grammar check (src/operator-record.ts).
|
|
573
|
+
warn: () => logFailure(registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger, "result paging unavailable", failureRecord({ connector: resolved.connector.id, tool: resolved.definition })),
|
|
437
574
|
};
|
|
438
575
|
const processed = (toolResult, truncated, value) => ({
|
|
439
576
|
toolResult,
|
|
@@ -444,23 +581,29 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
444
581
|
let value = result;
|
|
445
582
|
const guarded = await guardValue(value, results, cap);
|
|
446
583
|
value = guarded.result;
|
|
447
|
-
return processed(jsonResult({ ok: true, data: value }), guarded.truncated, { value });
|
|
584
|
+
return processed(jsonResult({ ok: true, data: value, format }), guarded.truncated, { value });
|
|
448
585
|
}
|
|
449
586
|
if (resolved.connector.kind === "mcp") {
|
|
450
587
|
const mcpResult = result;
|
|
451
588
|
let content = mcpResult?.content ?? [];
|
|
452
|
-
if (!content.some((block) => block.type === "text") &&
|
|
453
|
-
|
|
454
|
-
|
|
589
|
+
if (!content.some((block) => block.type === "text") && mcpResult?.structuredContent !== undefined) {
|
|
590
|
+
content = [
|
|
591
|
+
...content,
|
|
592
|
+
{
|
|
455
593
|
type: "text",
|
|
456
594
|
text: JSON.stringify(mcpResult.structuredContent),
|
|
457
|
-
}
|
|
595
|
+
},
|
|
596
|
+
];
|
|
458
597
|
}
|
|
459
598
|
const guarded = await guardContent(content, results, cap);
|
|
599
|
+
// Native blocks and bounded previews are the result. A format-only
|
|
600
|
+
// structuredContent hides them in clients that prefer that field.
|
|
601
|
+
guarded.result._meta = { "dev.connecta/format": format };
|
|
460
602
|
return processed(guarded.result, guarded.truncated);
|
|
461
603
|
}
|
|
462
604
|
const value = result;
|
|
463
605
|
const guarded = await guardText(serializeResultText(value), results, cap);
|
|
606
|
+
guarded.result._meta = { "dev.connecta/format": format };
|
|
464
607
|
return processed(guarded.result, guarded.truncated, { value });
|
|
465
608
|
},
|
|
466
609
|
activityFriction: (processed) => processed.friction,
|
|
@@ -473,23 +616,17 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
473
616
|
outcome.error.details !== undefined ||
|
|
474
617
|
// So does a conflict's `current`: where things stand is the retry.
|
|
475
618
|
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);
|
|
619
|
+
["auth_required", "invalid_args", "input_required_unsupported"].includes(outcome.error.code);
|
|
620
|
+
// Every mode carries the complete repair envelope in both MCP forms.
|
|
621
|
+
const makeFailedResult = () => jsonResult({
|
|
622
|
+
ok: false,
|
|
623
|
+
error: outcome.error,
|
|
624
|
+
durationMs: outcome.durationMs,
|
|
625
|
+
attempts: outcome.attempts,
|
|
626
|
+
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
627
|
+
});
|
|
491
628
|
let failedResult = makeFailedResult();
|
|
492
|
-
//
|
|
629
|
+
// Errors repeat in text and structuredContent. Account for
|
|
493
630
|
// both copies and JSON escaping when the bounded provider reason is large.
|
|
494
631
|
if (!recoveryRequired) {
|
|
495
632
|
const cap = resolveMaxResultBytes(outcome.resolved?.connector.maxResultBytes, globalCap);
|
|
@@ -500,9 +637,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
500
637
|
failedResult = makeFailedResult();
|
|
501
638
|
}
|
|
502
639
|
}
|
|
503
|
-
|
|
504
|
-
failedResult.isError = true;
|
|
505
|
-
}
|
|
640
|
+
failedResult.isError = true;
|
|
506
641
|
return {
|
|
507
642
|
toolResult: failedResult,
|
|
508
643
|
durationMs: outcome.durationMs,
|
|
@@ -515,6 +650,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
515
650
|
? jsonResult({
|
|
516
651
|
ok: true,
|
|
517
652
|
data: outcome.value.value,
|
|
653
|
+
format: outcome.format,
|
|
518
654
|
durationMs: outcome.durationMs,
|
|
519
655
|
attempts: outcome.attempts,
|
|
520
656
|
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
@@ -525,37 +661,37 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
525
661
|
durationMs: outcome.durationMs,
|
|
526
662
|
attempts: outcome.attempts,
|
|
527
663
|
timing: outcome.timing,
|
|
528
|
-
...(Object.prototype.hasOwnProperty.call(outcome.value, "value")
|
|
529
|
-
? { value: outcome.value.value }
|
|
530
|
-
: {}),
|
|
664
|
+
...(Object.prototype.hasOwnProperty.call(outcome.value, "value") ? { value: outcome.value.value } : {}),
|
|
531
665
|
};
|
|
532
666
|
}
|
|
533
667
|
return {
|
|
534
668
|
async skills(args = {}) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
{
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
.join("\n"),
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
669
|
+
try {
|
|
670
|
+
if (!args.name) {
|
|
671
|
+
const listing = await skills.summaries();
|
|
672
|
+
return {
|
|
673
|
+
structuredContent: { skills: listing },
|
|
674
|
+
content: [
|
|
675
|
+
{
|
|
676
|
+
type: "text",
|
|
677
|
+
text: 'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
|
|
678
|
+
listing.map((skill) => `- \`${skill.name}\` — ${skill.description}`).join("\n"),
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const text = await skills.text(args.name);
|
|
684
|
+
return { content: [{ type: "text", text }], structuredContent: { name: args.name, format: "text", text } };
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
return errorResult(error instanceof ConnectorCallError ? error.message : "Skills are unavailable.");
|
|
548
688
|
}
|
|
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
689
|
},
|
|
554
690
|
async searchTools(args) {
|
|
555
691
|
if (args.connector !== undefined && enc.encode(args.connector).length > 512) {
|
|
556
692
|
return discoveryErrorResult(new DiscoveryPolicyError("invalid_args", "connector must be at most 512 UTF-8 bytes."));
|
|
557
693
|
}
|
|
558
|
-
return discoveryResult(async () =>
|
|
694
|
+
return discoveryResult(async () => flatSearchResult(await catalog.search({
|
|
559
695
|
...args,
|
|
560
696
|
includeSchemaKeys: args.includeSchemas !== undefined,
|
|
561
697
|
})), "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
|
|
@@ -566,16 +702,12 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
566
702
|
async callDestructiveTool(args) {
|
|
567
703
|
// `reason` is read by the host's approval view and stops there — runCall
|
|
568
704
|
// forwards only the call arguments, so it never reaches the connector.
|
|
569
|
-
return (await runCall(args, "call_destructive_tool"
|
|
705
|
+
return (await runCall(args, "call_destructive_tool")).toolResult;
|
|
570
706
|
},
|
|
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)) {
|
|
707
|
+
async readResult(args, options = {}) {
|
|
708
|
+
// Defense for in-process callers too: guest paging validates these domains
|
|
709
|
+
// before calling here, while internal consumers may call readResult directly.
|
|
710
|
+
if (args.maxBytes !== undefined && !isValidMaxResultBytes(args.maxBytes)) {
|
|
579
711
|
return errorResult(`Invalid maxBytes ${args.maxBytes}: must be a whole number of bytes ` +
|
|
580
712
|
`>= ${MIN_MAX_RESULT_BYTES}. Omit it to use the deployment default.`);
|
|
581
713
|
}
|
|
@@ -583,6 +715,9 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
583
715
|
return errorResult(`Invalid offset ${args.offset}: must be a whole number of bytes ` +
|
|
584
716
|
`>= ${MIN_RESULT_OFFSET}. Omit it to start at the beginning.`);
|
|
585
717
|
}
|
|
718
|
+
if (args.page !== undefined && (!Number.isSafeInteger(args.page) || args.page < 0 || args.offset !== undefined)) {
|
|
719
|
+
return errorResult("Use a whole page >= 0, without offset.");
|
|
720
|
+
}
|
|
586
721
|
const results = registry.resultsStorage();
|
|
587
722
|
const unavailableResult = () => ({
|
|
588
723
|
...jsonResult({
|
|
@@ -598,52 +733,124 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
598
733
|
// is simply gone. Every key a page touches answers the same way, so a
|
|
599
734
|
// backend that dies halfway through a multi-chunk page says so.
|
|
600
735
|
const read = async (key) => {
|
|
736
|
+
options.signal?.throwIfAborted();
|
|
737
|
+
let value;
|
|
601
738
|
try {
|
|
602
|
-
|
|
739
|
+
value = (await results.get(key)) ?? null;
|
|
603
740
|
}
|
|
604
741
|
catch {
|
|
605
|
-
|
|
742
|
+
value = false;
|
|
606
743
|
}
|
|
744
|
+
// A storage driver may finish its pending read after cancellation. No
|
|
745
|
+
// subsequent chunk read or decode may start when that happens (INV-7).
|
|
746
|
+
options.signal?.throwIfAborted();
|
|
747
|
+
return value;
|
|
607
748
|
};
|
|
608
|
-
const stored = await read(
|
|
749
|
+
const stored = await read(resultKeys.chunk(args.id, 0));
|
|
609
750
|
if (stored === false)
|
|
610
751
|
return unavailableResult();
|
|
611
752
|
if (stored === null) {
|
|
612
753
|
return errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
|
|
613
754
|
}
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
755
|
+
const denied = () => errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
|
|
756
|
+
const header = RESULT_ENVELOPE_V4_HEADER.exec(stored);
|
|
757
|
+
if (!header)
|
|
758
|
+
return denied();
|
|
759
|
+
let binding;
|
|
760
|
+
try {
|
|
761
|
+
binding = JSON.parse(dec.decode(Uint8Array.from(atob(header[1]), (char) => char.charCodeAt(0))));
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
return denied();
|
|
765
|
+
}
|
|
766
|
+
const identity = registry.resultIdentity();
|
|
767
|
+
if (!binding ||
|
|
768
|
+
binding.baseUrl !== baseUrl ||
|
|
769
|
+
!binding.identity ||
|
|
770
|
+
binding.identity.subject !== identity.subject ||
|
|
771
|
+
binding.identity.principal !== identity.principal ||
|
|
772
|
+
binding.identity.endpoint !== identity.endpoint ||
|
|
773
|
+
binding.identity.origin !== identity.origin ||
|
|
774
|
+
(binding.program === undefined
|
|
775
|
+
? typeof binding.connector !== "string" || typeof binding.tool !== "string"
|
|
776
|
+
: !Array.isArray(binding.program) ||
|
|
777
|
+
binding.program.some((dependency) => !dependency ||
|
|
778
|
+
typeof dependency.connector !== "string" ||
|
|
779
|
+
(dependency.tool !== undefined && typeof dependency.tool !== "string") ||
|
|
780
|
+
(dependency.access !== undefined &&
|
|
781
|
+
(dependency.access !== "catalog" || dependency.classification !== "read")) ||
|
|
782
|
+
!["read", "write"].includes(dependency.classification))) ||
|
|
783
|
+
!["read", "write"].includes(binding.classification))
|
|
784
|
+
return denied();
|
|
785
|
+
const allowed = async () => {
|
|
786
|
+
options.signal?.throwIfAborted();
|
|
787
|
+
try {
|
|
788
|
+
if (binding.program &&
|
|
789
|
+
!(await registry.recheckResultAccess(undefined, binding.classification, options.signal)))
|
|
790
|
+
return false;
|
|
791
|
+
if (binding.classification === "write" && opts.trust !== "trusted")
|
|
792
|
+
return false;
|
|
793
|
+
const dependencies = binding.program ?? [binding];
|
|
794
|
+
for (const dependency of dependencies) {
|
|
795
|
+
const address = dependency.tool === undefined ? dependency.connector : `${dependency.connector}.${dependency.tool}`;
|
|
796
|
+
if (!(await registry.recheckResultAccess(address, dependency.classification, options.signal, dependency.access)))
|
|
797
|
+
return false;
|
|
798
|
+
if (dependency.tool === undefined) {
|
|
799
|
+
if (dependency.access === "catalog"
|
|
800
|
+
? !registry.getConnector(dependency.connector)
|
|
801
|
+
: !registry.getResourceConnector(dependency.connector))
|
|
802
|
+
return false;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (!registry.getConnector(dependency.connector))
|
|
806
|
+
return false;
|
|
807
|
+
const tools = await registry.getTools(dependency.connector, baseUrl, requestScope, options.signal ? { signal: options.signal } : {});
|
|
808
|
+
const tool = tools.find((tool) => tool.name === dependency.tool);
|
|
809
|
+
if (!tool ||
|
|
810
|
+
!(dependency.access === "catalog" ||
|
|
811
|
+
(dependency.classification === "read" && tool.classification === "read") ||
|
|
812
|
+
opts.trust === "trusted"))
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
options.signal?.throwIfAborted();
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
if (!(await allowed()))
|
|
823
|
+
return denied();
|
|
824
|
+
opts.onResultRead?.(binding.program ?? [binding]);
|
|
825
|
+
let requestedOffset = args.offset ?? 0;
|
|
826
|
+
const cap = Number(header[4]);
|
|
827
|
+
if (!isValidMaxResultBytes(cap))
|
|
828
|
+
return denied();
|
|
632
829
|
const maxBytes = Math.min(args.maxBytes ?? cap, cap);
|
|
633
830
|
let bytes;
|
|
634
831
|
let total;
|
|
635
832
|
let start = 0;
|
|
636
|
-
|
|
637
|
-
total = Number(header[
|
|
638
|
-
const chunkBytes =
|
|
833
|
+
{
|
|
834
|
+
total = Number(header[2]);
|
|
835
|
+
const chunkBytes = Number(header[3]);
|
|
836
|
+
if (!Number.isSafeInteger(total) ||
|
|
837
|
+
total < 0 ||
|
|
838
|
+
!Number.isSafeInteger(chunkBytes) ||
|
|
839
|
+
chunkBytes < 3 ||
|
|
840
|
+
chunkBytes % 3 !== 0)
|
|
841
|
+
return denied();
|
|
842
|
+
if (args.page !== undefined) {
|
|
843
|
+
// At least one complete UTF-8 codepoint per nominal window. Adjacent
|
|
844
|
+
// pages align both edges backwards, so no character repeats or vanishes.
|
|
845
|
+
const width = Math.max(4, maxBytes);
|
|
846
|
+
requestedOffset = Math.min(total, Math.min(args.page, Math.ceil(total / width)) * width);
|
|
847
|
+
}
|
|
639
848
|
start = Math.floor(Math.max(0, Math.min(requestedOffset, total) - 3) / 3) * 3;
|
|
640
849
|
const end = Math.min(total, requestedOffset + maxBytes + 4);
|
|
641
850
|
bytes = new Uint8Array(Math.max(0, end - start));
|
|
642
851
|
const lastChunk = Math.floor(Math.max(end - 1, start) / chunkBytes);
|
|
643
852
|
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}`);
|
|
853
|
+
const encoded = index === 0 ? stored.slice(header[0].length) : await read(resultKeys.chunk(args.id, index));
|
|
647
854
|
if (encoded === false)
|
|
648
855
|
return unavailableResult();
|
|
649
856
|
if (encoded === null) {
|
|
@@ -656,16 +863,12 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
656
863
|
// alignment because every chunk boundary is a multiple of three.
|
|
657
864
|
const from = Math.max(start, chunkStart) - chunkStart;
|
|
658
865
|
const to = Math.min(end, chunkStart + chunkBytes, total) - chunkStart;
|
|
659
|
-
const binary = atob(encoded.slice(from / 3 * 4, Math.ceil(to / 3) * 4));
|
|
866
|
+
const binary = atob(encoded.slice((from / 3) * 4, Math.ceil(to / 3) * 4));
|
|
660
867
|
for (let at = from; at < to; at++) {
|
|
661
868
|
bytes[chunkStart + at - start] = binary.charCodeAt(at - from);
|
|
662
869
|
}
|
|
663
870
|
}
|
|
664
871
|
}
|
|
665
|
-
else {
|
|
666
|
-
bytes = enc.encode(stored);
|
|
667
|
-
total = bytes.length;
|
|
668
|
-
}
|
|
669
872
|
// Validated above, so no coercion is needed here — only alignment. A
|
|
670
873
|
// client that computes its own offsets can land inside a multi-byte
|
|
671
874
|
// character, which would decode as U+FFFD; the offset actually served is
|
|
@@ -678,117 +881,45 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
678
881
|
// never split across pages (which would emit U+FFFD on both sides).
|
|
679
882
|
// `nextOffset` is this aligned end, so it is a valid boundary for the
|
|
680
883
|
// next call and paging reassembles the original byte-for-byte.
|
|
681
|
-
const end =
|
|
884
|
+
const end = args.page === undefined
|
|
885
|
+
? start + alignEndToCharBoundary(bytes, offset - start, offset - start + maxBytes, total - start)
|
|
886
|
+
: start +
|
|
887
|
+
alignStartToCharBoundary(bytes, Math.min(total - start, requestedOffset - start + Math.max(4, maxBytes)));
|
|
682
888
|
const slice = dec.decode(bytes.subarray(offset - start, end - start));
|
|
683
889
|
const hasMore = end < total;
|
|
684
890
|
// The same notice-first shape as a truncated call: one line of header,
|
|
685
891
|
// then the page as raw text. A JSON `text` field escaped every quote and
|
|
686
892
|
// newline in the page — a second layer over JSON payloads — which
|
|
687
893
|
// inflated pages past the size clients accept and made them hard to read.
|
|
688
|
-
|
|
894
|
+
const pageHeader = {
|
|
689
895
|
resultId: args.id,
|
|
690
896
|
offset,
|
|
691
897
|
bytes: Math.max(0, end - offset),
|
|
692
898
|
totalBytes: total,
|
|
693
899
|
hasMore,
|
|
694
|
-
...(hasMore
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
900
|
+
...(hasMore ? { nextOffset: end } : {}),
|
|
901
|
+
};
|
|
902
|
+
if (!(await allowed()))
|
|
903
|
+
return denied();
|
|
904
|
+
const page = noticeFirst(pageHeader, slice);
|
|
905
|
+
page.structuredContent = { ...pageHeader, text: slice, format: "text" };
|
|
906
|
+
return page;
|
|
698
907
|
},
|
|
699
908
|
async authorizeConnector(args) {
|
|
700
|
-
const connector = registry.getConnector(args.connector);
|
|
701
|
-
if (!connector) {
|
|
702
|
-
return errorResult(`Unknown connector "${boundedEchoText(args.connector)}"`);
|
|
703
|
-
}
|
|
704
|
-
if (!connector.startAuth) {
|
|
705
|
-
if (!connector.credential) {
|
|
706
|
-
return jsonResult({
|
|
707
|
-
connector: connector.id,
|
|
708
|
-
recovery: "unavailable",
|
|
709
|
-
message: `Connector "${connector.id}" declares neither downstream OAuth ` +
|
|
710
|
-
"nor an operator-managed credential slot. Update the connector " +
|
|
711
|
-
"or deployment configuration before retrying.",
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
|
|
715
|
-
if (!ctx.credential || !opts.credentialHandoffUrl) {
|
|
716
|
-
return jsonResult({
|
|
717
|
-
connector: connector.id,
|
|
718
|
-
recovery: "unavailable",
|
|
719
|
-
message: "Credential recovery needs both a vault and the optional UI. Configure " +
|
|
720
|
-
"vault and ui in deployment code, then call " +
|
|
721
|
-
"authorize_connector again.",
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
const fields = connector.credential.fields?.map((field) => ({
|
|
725
|
-
name: field.name,
|
|
726
|
-
guidance: field.description ?? field.label,
|
|
727
|
-
})) ?? [
|
|
728
|
-
{
|
|
729
|
-
name: "value",
|
|
730
|
-
guidance: connector.credential.description ??
|
|
731
|
-
connector.credential.label,
|
|
732
|
-
},
|
|
733
|
-
];
|
|
734
|
-
return jsonResult({
|
|
735
|
-
connector: connector.id,
|
|
736
|
-
recovery: "operator_config",
|
|
737
|
-
credential: {
|
|
738
|
-
label: connector.credential.label,
|
|
739
|
-
fields,
|
|
740
|
-
},
|
|
741
|
-
operatorUrl: opts.credentialHandoffUrl,
|
|
742
|
-
instructions: "Have the operator open operatorUrl, set and test the credential, " +
|
|
743
|
-
"then retry the original call. No redeploy is needed. " +
|
|
744
|
-
(connector.authScope === "personal"
|
|
745
|
-
? "Credential mutation requires the signed-in principal who owns this connection."
|
|
746
|
-
: "Shared credential mutation requires a signed-in human with access to this connector."),
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
if (!opts.canManageAuth?.(connector.id))
|
|
750
|
-
return jsonResult({ connector: connector.id, recovery: "unavailable", message: "Your identity is not permitted to manage authentication for this connection." });
|
|
751
|
-
const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
|
|
752
909
|
try {
|
|
753
|
-
|
|
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
|
-
return jsonResult({
|
|
762
|
-
connector: connector.id,
|
|
763
|
-
recovery: "oauth",
|
|
764
|
-
status: status.state,
|
|
765
|
-
...(status.authorizationUrl
|
|
766
|
-
? {
|
|
767
|
-
authorizationUrl: status.authorizationUrl,
|
|
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 } : {}),
|
|
773
|
-
});
|
|
910
|
+
return jsonResult(await authorizationHandoff(registry, baseUrl, requestScope, opts, args));
|
|
774
911
|
}
|
|
775
912
|
catch (err) {
|
|
776
913
|
return errorResult(msg(err));
|
|
777
914
|
}
|
|
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
915
|
},
|
|
784
916
|
};
|
|
785
917
|
}
|
|
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 =
|
|
918
|
+
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".`;
|
|
919
|
+
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
920
|
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.';
|
|
921
|
+
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.";
|
|
922
|
+
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
923
|
/**
|
|
793
924
|
* Sentences appended to a meta-tool description only when this connection
|
|
794
925
|
* actually has connector guides. Tool descriptions are always-loaded context,
|
|
@@ -804,9 +935,7 @@ const GUIDE_NOTES = {
|
|
|
804
935
|
};
|
|
805
936
|
/** `base`, plus its guide note when any VISIBLE connector carries a guide. */
|
|
806
937
|
function describedFor(registry, base, note) {
|
|
807
|
-
return hasConnectorGuides(registry.listConnectors())
|
|
808
|
-
? base + GUIDE_NOTES[note]
|
|
809
|
-
: base;
|
|
938
|
+
return hasConnectorGuides(registry.listConnectors()) ? base + GUIDE_NOTES[note] : base;
|
|
810
939
|
}
|
|
811
940
|
/**
|
|
812
941
|
* Connecta refuses downstream tools that are not explicitly annotated
|
|
@@ -827,7 +956,7 @@ const READ_ONLY_LOCAL = {
|
|
|
827
956
|
openWorldHint: false,
|
|
828
957
|
};
|
|
829
958
|
const CALL_INPUT_SCHEMA = {
|
|
830
|
-
address: z.string(),
|
|
959
|
+
address: z.string().meta({ "x-mcp-header": "Address" }),
|
|
831
960
|
args: z.record(z.string(), z.unknown()).optional(),
|
|
832
961
|
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
833
962
|
timeoutMs: z.number().int().positive().optional(),
|
|
@@ -860,18 +989,9 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
|
|
|
860
989
|
connector: z.string(),
|
|
861
990
|
force: z.boolean().optional(),
|
|
862
991
|
}));
|
|
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
992
|
/**
|
|
873
|
-
* Register the
|
|
874
|
-
* `registerExecuteTool` adds the
|
|
993
|
+
* Register the five explicit meta-tools onto an McpServer instance.
|
|
994
|
+
* `registerExecuteTool` adds the sixth, `execute_code`. Broad discovery and
|
|
875
995
|
* multi-call work uses discovery and ordinary JavaScript promises inside a
|
|
876
996
|
* program, which `execute_code` builds over the same
|
|
877
997
|
* `CatalogService` and `InvocationService` these handlers use — one shared
|
|
@@ -879,33 +999,56 @@ const GET_RESULT_INPUT = advertisedSchema(z.object({
|
|
|
879
999
|
*/
|
|
880
1000
|
export function registerMetaTools(server, registry, ctx) {
|
|
881
1001
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1002
|
+
downstreamInput: Boolean(ctx.authElicitation),
|
|
1003
|
+
requestScope: ctx.requestScope,
|
|
1004
|
+
trust: ctx.trust,
|
|
882
1005
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
883
1006
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
884
1007
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
885
1008
|
activity: ctx.activity,
|
|
1009
|
+
client: ctx.client,
|
|
886
1010
|
canManageAuth: ctx.canManageAuth,
|
|
887
1011
|
credentialHandoffUrl: ctx.credentialHandoffUrl,
|
|
1012
|
+
oauthConnectUrl: ctx.oauthConnectUrl,
|
|
1013
|
+
oauthConnectUnavailable: ctx.oauthConnectUnavailable,
|
|
888
1014
|
requestSignal: ctx.requestSignal,
|
|
889
1015
|
defer: ctx.defer,
|
|
890
|
-
approval: ctx.approval,
|
|
891
1016
|
});
|
|
1017
|
+
const bindRequest = (request) => {
|
|
1018
|
+
if (ctx.client)
|
|
1019
|
+
bindMcpClient(request, ctx.client, ctx.activity);
|
|
1020
|
+
};
|
|
892
1021
|
server.registerTool("skills", {
|
|
893
1022
|
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
894
1023
|
inputSchema: SKILLS_INPUT,
|
|
1024
|
+
outputSchema: SKILLS_OUTPUT,
|
|
895
1025
|
annotations: READ_ONLY_LOCAL,
|
|
896
|
-
}, async (args) =>
|
|
1026
|
+
}, async (args, request) => {
|
|
1027
|
+
bindRequest(request);
|
|
1028
|
+
return mt.skills(args);
|
|
1029
|
+
});
|
|
897
1030
|
server.registerTool("search_tools", {
|
|
898
1031
|
description: describedFor(registry, SEARCH_DESC, "search"),
|
|
899
1032
|
inputSchema: SEARCH_INPUT,
|
|
1033
|
+
outputSchema: SEARCH_OUTPUT,
|
|
900
1034
|
annotations: READ_ONLY_REMOTE,
|
|
901
|
-
}, async (args) =>
|
|
1035
|
+
}, async (args, request) => {
|
|
1036
|
+
bindRequest(request);
|
|
1037
|
+
return mt.searchTools(args);
|
|
1038
|
+
});
|
|
902
1039
|
server.registerTool("call_tool", {
|
|
903
1040
|
description: CALL_DESC,
|
|
904
1041
|
inputSchema: CALL_INPUT,
|
|
1042
|
+
// Default mode returns native content, so no structured result is promised.
|
|
905
1043
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
906
1044
|
// anything else is refused and routed to call_destructive_tool.
|
|
907
1045
|
annotations: READ_ONLY_REMOTE,
|
|
908
|
-
}, async (args) =>
|
|
1046
|
+
}, async (args, request) => {
|
|
1047
|
+
bindRequest(request);
|
|
1048
|
+
return ctx.authElicitation
|
|
1049
|
+
? ctx.authElicitation.run("call_tool", args, request, () => mt.callTool(args))
|
|
1050
|
+
: mt.callTool(args);
|
|
1051
|
+
});
|
|
909
1052
|
server.registerTool("call_destructive_tool", {
|
|
910
1053
|
description: describedFor(registry, CALL_DESTRUCTIVE_DESC, "destructive"),
|
|
911
1054
|
inputSchema: CALL_DESTRUCTIVE_INPUT,
|
|
@@ -914,18 +1057,22 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
914
1057
|
readOnlyHint: false,
|
|
915
1058
|
openWorldHint: true,
|
|
916
1059
|
},
|
|
917
|
-
}, async (args) => {
|
|
1060
|
+
}, async (args, request) => {
|
|
1061
|
+
bindRequest(request);
|
|
918
1062
|
// `reason` is the host's to display and connecta's to keep out of the
|
|
919
1063
|
// downstream call, so this destructuring is the whole of its handling:
|
|
920
1064
|
// nothing below reads it. Dropping it is also what makes an empty or
|
|
921
1065
|
// whitespace-only one "absent" rather than a validation failure — there
|
|
922
1066
|
// is no field left for it to be absent from.
|
|
923
1067
|
const { reason: _hostContext, ...call } = args;
|
|
924
|
-
return
|
|
1068
|
+
return ctx.authElicitation
|
|
1069
|
+
? ctx.authElicitation.run("call_destructive_tool", call, request, () => mt.callDestructiveTool(call))
|
|
1070
|
+
: mt.callDestructiveTool(call);
|
|
925
1071
|
});
|
|
926
1072
|
server.registerTool("authorize_connector", {
|
|
927
1073
|
description: AUTHORIZE_DESC,
|
|
928
1074
|
inputSchema: AUTHORIZE_INPUT,
|
|
1075
|
+
outputSchema: AUTHORIZE_OUTPUT,
|
|
929
1076
|
// Starts (or with force, resets) a downstream OAuth flow — it changes
|
|
930
1077
|
// stored connector auth state, so it is deliberately not read-only.
|
|
931
1078
|
annotations: {
|
|
@@ -933,10 +1080,10 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
933
1080
|
destructiveHint: false,
|
|
934
1081
|
openWorldHint: true,
|
|
935
1082
|
},
|
|
936
|
-
}, async (args) =>
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
1083
|
+
}, async (args, request) => {
|
|
1084
|
+
bindRequest(request);
|
|
1085
|
+
return ctx.authElicitation
|
|
1086
|
+
? ctx.authElicitation.run("authorize_connector", args, request, () => mt.authorizeConnector(args))
|
|
1087
|
+
: mt.authorizeConnector(args);
|
|
1088
|
+
});
|
|
942
1089
|
}
|