@zackbart/connecta 0.15.1 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +40 -20
- package/CHANGELOG.md +653 -0
- package/README.md +25 -5
- package/bin/connecta.mjs +44 -3
- package/dist/access-tokens.d.ts +0 -1
- package/dist/access-tokens.js +0 -1
- package/dist/activity.d.ts +50 -2
- package/dist/activity.js +34 -1
- package/dist/apps-shell.d.ts +0 -1
- package/dist/apps-shell.js +0 -1
- package/dist/auth/bearer.d.ts +0 -1
- package/dist/auth/bearer.js +0 -1
- package/dist/auth/clerk.d.ts +0 -1
- package/dist/auth/clerk.js +45 -9
- package/dist/auth/downstream-oauth.d.ts +0 -1
- package/dist/auth/downstream-oauth.js +0 -1
- package/dist/call-admission.d.ts +0 -1
- package/dist/call-admission.js +0 -1
- package/dist/catalog-drift.d.ts +99 -0
- package/dist/catalog-drift.js +251 -0
- package/dist/catalog-fingerprint.d.ts +0 -1
- package/dist/catalog-fingerprint.js +0 -1
- package/dist/catalog-limits.d.ts +0 -1
- package/dist/catalog-limits.js +0 -1
- package/dist/catalog-service.d.ts +0 -1
- package/dist/catalog-service.js +41 -5
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +20 -1
- package/dist/concurrency.d.ts +0 -1
- package/dist/concurrency.js +0 -1
- package/dist/connector-scope.d.ts +0 -1
- package/dist/connector-scope.js +0 -1
- package/dist/connectors/api.d.ts +29 -21
- package/dist/connectors/api.js +45 -19
- package/dist/connectors/guarded-fetch.d.ts +106 -0
- package/dist/connectors/guarded-fetch.js +289 -0
- package/dist/connectors/remote-mcp.d.ts +0 -1
- package/dist/connectors/remote-mcp.js +0 -1
- package/dist/credentials.d.ts +0 -1
- package/dist/credentials.js +0 -1
- package/dist/errors.d.ts +30 -3
- package/dist/errors.js +1 -1
- package/dist/execute.d.ts +0 -1
- package/dist/execute.js +0 -1
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +28 -1
- package/dist/executor-result.d.ts +0 -1
- package/dist/executor-result.js +0 -1
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-child.js +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -1
- package/dist/executors/quickjs-protocol.js +0 -1
- package/dist/executors/quickjs-runtime.d.ts +0 -1
- package/dist/executors/quickjs-runtime.js +0 -1
- package/dist/executors/quickjs.d.ts +0 -1
- package/dist/executors/quickjs.js +2 -1
- package/dist/favicon.d.ts +0 -1
- package/dist/favicon.js +0 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +24 -7
- package/dist/invocation.d.ts +0 -1
- package/dist/invocation.js +0 -1
- package/dist/json-schema.d.ts +0 -1
- package/dist/json-schema.js +0 -1
- package/dist/mcp-result.d.ts +0 -1
- package/dist/mcp-result.js +0 -1
- package/dist/meta-tools.d.ts +0 -1
- package/dist/meta-tools.js +0 -1
- package/dist/node.d.ts +0 -1
- package/dist/node.js +0 -1
- package/dist/operator-ui/generated.d.ts +2 -3
- package/dist/operator-ui/generated.js +3 -4
- package/dist/operator-ui/model.d.ts +10 -1
- package/dist/operator-ui/model.js +0 -1
- package/dist/operator-ui/view.d.ts +167 -0
- package/dist/operator-ui/view.js +270 -0
- package/dist/providers/cloudflare.d.ts +0 -1
- package/dist/providers/cloudflare.js +261 -299
- package/dist/providers/linear.d.ts +26 -5
- package/dist/providers/linear.js +32 -61
- package/dist/providers/mixpanel.d.ts +35 -4
- package/dist/providers/mixpanel.js +53 -79
- package/dist/providers/notion.d.ts +0 -1
- package/dist/providers/notion.js +87 -42
- package/dist/providers/stripe.d.ts +13 -1
- package/dist/providers/stripe.js +31 -60
- package/dist/registry.d.ts +34 -2
- package/dist/registry.js +85 -5
- package/dist/retired-toolkits.d.ts +0 -1
- package/dist/retired-toolkits.js +0 -1
- package/dist/routes/access-tokens.d.ts +0 -1
- package/dist/routes/access-tokens.js +0 -1
- package/dist/routes/activity.d.ts +0 -1
- package/dist/routes/activity.js +0 -1
- package/dist/routes/credentials.d.ts +0 -1
- package/dist/routes/credentials.js +0 -1
- package/dist/routes/mcp.d.ts +0 -1
- package/dist/routes/mcp.js +0 -1
- package/dist/routes/oauth.d.ts +0 -1
- package/dist/routes/oauth.js +0 -1
- package/dist/routes/shared.d.ts +2 -1
- package/dist/routes/shared.js +0 -1
- package/dist/routes/ui.d.ts +0 -1
- package/dist/routes/ui.js +0 -1
- package/dist/server.d.ts +0 -1
- package/dist/server.js +11 -1
- package/dist/skills.d.ts +0 -1
- package/dist/skills.js +0 -1
- package/dist/storage/file.d.ts +0 -1
- package/dist/storage/file.js +0 -1
- package/dist/storage/memory.d.ts +0 -1
- package/dist/storage/memory.js +0 -1
- package/dist/timeout.d.ts +0 -1
- package/dist/timeout.js +0 -1
- package/dist/tool-safety.d.ts +0 -1
- package/dist/tool-safety.js +0 -1
- package/dist/types.d.ts +45 -1
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +0 -1
- package/dist/ui.js +13 -137
- package/dist/validate.d.ts +13 -15
- package/dist/validate.js +24 -11
- package/dist/version.d.ts +1 -2
- package/dist/version.js +1 -2
- package/documentation/architecture.md +186 -5
- package/documentation/auth.md +11 -0
- package/documentation/call-admission.md +142 -6
- package/documentation/cloudflare.md +92 -10
- package/documentation/code-first-exploration.md +2 -2
- package/documentation/code-mode.md +14 -14
- package/documentation/connector-guides.md +172 -5
- package/documentation/connectors.md +89 -0
- package/documentation/linear.md +20 -2
- package/documentation/mcp-ui-design.md +2 -2
- package/documentation/meta-tools.md +7 -1
- package/documentation/mixpanel.md +42 -9
- package/documentation/notion.md +23 -2
- package/documentation/operations.md +325 -5
- package/documentation/operator-ui.md +120 -5
- package/documentation/provider-audit.md +160 -0
- package/documentation/provider-conventions.md +667 -0
- package/documentation/request-admission.md +167 -5
- package/documentation/rich-output-design.md +1 -2
- package/documentation/stripe.md +15 -0
- package/documentation/upgrading.md +454 -0
- package/ethos.md +14 -9
- package/examples/worker/README.md +102 -12
- package/examples/worker/src/index.ts +46 -1
- package/examples/worker/wrangler.jsonc +14 -1
- package/package.json +10 -6
- package/templates/node/.dockerignore +10 -0
- package/templates/node/.env.example +40 -3
- package/templates/node/AGENTS.md +16 -1
- package/templates/node/Dockerfile +49 -0
- package/templates/node/README.md +117 -2
- package/templates/node/docker-compose.yml +39 -0
- package/templates/node/package.json +1 -1
- package/templates/node/src/file-activity.ts +182 -0
- package/templates/node/src/index.ts +80 -4
- package/assets/connecta-clay-hero.png +0 -0
- package/dist/access-tokens.d.ts.map +0 -1
- package/dist/access-tokens.js.map +0 -1
- package/dist/activity.d.ts.map +0 -1
- package/dist/activity.js.map +0 -1
- package/dist/apps-shell.d.ts.map +0 -1
- package/dist/apps-shell.js.map +0 -1
- package/dist/auth/bearer.d.ts.map +0 -1
- package/dist/auth/bearer.js.map +0 -1
- package/dist/auth/clerk.d.ts.map +0 -1
- package/dist/auth/clerk.js.map +0 -1
- package/dist/auth/downstream-oauth.d.ts.map +0 -1
- package/dist/auth/downstream-oauth.js.map +0 -1
- package/dist/call-admission.d.ts.map +0 -1
- package/dist/call-admission.js.map +0 -1
- package/dist/catalog-fingerprint.d.ts.map +0 -1
- package/dist/catalog-fingerprint.js.map +0 -1
- package/dist/catalog-limits.d.ts.map +0 -1
- package/dist/catalog-limits.js.map +0 -1
- package/dist/catalog-service.d.ts.map +0 -1
- package/dist/catalog-service.js.map +0 -1
- package/dist/catalog.d.ts.map +0 -1
- package/dist/catalog.js.map +0 -1
- package/dist/concurrency.d.ts.map +0 -1
- package/dist/concurrency.js.map +0 -1
- package/dist/connector-scope.d.ts.map +0 -1
- package/dist/connector-scope.js.map +0 -1
- package/dist/connectors/api.d.ts.map +0 -1
- package/dist/connectors/api.js.map +0 -1
- package/dist/connectors/remote-mcp.d.ts.map +0 -1
- package/dist/connectors/remote-mcp.js.map +0 -1
- package/dist/credentials.d.ts.map +0 -1
- package/dist/credentials.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js.map +0 -1
- package/dist/executor-admission.d.ts.map +0 -1
- package/dist/executor-admission.js.map +0 -1
- package/dist/executor-result.d.ts.map +0 -1
- package/dist/executor-result.js.map +0 -1
- package/dist/executors/quickjs-child.d.ts.map +0 -1
- package/dist/executors/quickjs-child.js.map +0 -1
- package/dist/executors/quickjs-protocol.d.ts.map +0 -1
- package/dist/executors/quickjs-protocol.js.map +0 -1
- package/dist/executors/quickjs-runtime.d.ts.map +0 -1
- package/dist/executors/quickjs-runtime.js.map +0 -1
- package/dist/executors/quickjs.d.ts.map +0 -1
- package/dist/executors/quickjs.js.map +0 -1
- package/dist/favicon.d.ts.map +0 -1
- package/dist/favicon.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invocation.d.ts.map +0 -1
- package/dist/invocation.js.map +0 -1
- package/dist/json-schema.d.ts.map +0 -1
- package/dist/json-schema.js.map +0 -1
- package/dist/mcp-result.d.ts.map +0 -1
- package/dist/mcp-result.js.map +0 -1
- package/dist/meta-tools.d.ts.map +0 -1
- package/dist/meta-tools.js.map +0 -1
- package/dist/node.d.ts.map +0 -1
- package/dist/node.js.map +0 -1
- package/dist/operator-ui/generated.d.ts.map +0 -1
- package/dist/operator-ui/generated.js.map +0 -1
- package/dist/operator-ui/model.d.ts.map +0 -1
- package/dist/operator-ui/model.js.map +0 -1
- package/dist/providers/cloudflare.d.ts.map +0 -1
- package/dist/providers/cloudflare.js.map +0 -1
- package/dist/providers/linear.d.ts.map +0 -1
- package/dist/providers/linear.js.map +0 -1
- package/dist/providers/mixpanel.d.ts.map +0 -1
- package/dist/providers/mixpanel.js.map +0 -1
- package/dist/providers/notion.d.ts.map +0 -1
- package/dist/providers/notion.js.map +0 -1
- package/dist/providers/stripe.d.ts.map +0 -1
- package/dist/providers/stripe.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/retired-toolkits.d.ts.map +0 -1
- package/dist/retired-toolkits.js.map +0 -1
- package/dist/routes/access-tokens.d.ts.map +0 -1
- package/dist/routes/access-tokens.js.map +0 -1
- package/dist/routes/activity.d.ts.map +0 -1
- package/dist/routes/activity.js.map +0 -1
- package/dist/routes/credentials.d.ts.map +0 -1
- package/dist/routes/credentials.js.map +0 -1
- package/dist/routes/mcp.d.ts.map +0 -1
- package/dist/routes/mcp.js.map +0 -1
- package/dist/routes/oauth.d.ts.map +0 -1
- package/dist/routes/oauth.js.map +0 -1
- package/dist/routes/shared.d.ts.map +0 -1
- package/dist/routes/shared.js.map +0 -1
- package/dist/routes/ui.d.ts.map +0 -1
- package/dist/routes/ui.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/storage/file.d.ts.map +0 -1
- package/dist/storage/file.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/timeout.d.ts.map +0 -1
- package/dist/timeout.js.map +0 -1
- package/dist/tool-safety.d.ts.map +0 -1
- package/dist/tool-safety.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/ui.d.ts.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/examples/node/README.md +0 -52
- package/examples/node/src/index.ts +0 -71
- package/src/access-tokens.ts +0 -289
- package/src/activity.ts +0 -234
- package/src/apps-shell.ts +0 -370
- package/src/auth/bearer.ts +0 -61
- package/src/auth/clerk.ts +0 -553
- package/src/auth/downstream-oauth.ts +0 -575
- package/src/call-admission.ts +0 -521
- package/src/catalog-fingerprint.ts +0 -52
- package/src/catalog-limits.ts +0 -11
- package/src/catalog-service.ts +0 -1153
- package/src/catalog.ts +0 -790
- package/src/concurrency.ts +0 -39
- package/src/connector-scope.ts +0 -66
- package/src/connectors/api.ts +0 -159
- package/src/connectors/remote-mcp.ts +0 -1090
- package/src/credentials.ts +0 -428
- package/src/errors.ts +0 -364
- package/src/execute.ts +0 -1406
- package/src/executor-admission.ts +0 -328
- package/src/executor-result.ts +0 -107
- package/src/executors/quickjs-child.ts +0 -172
- package/src/executors/quickjs-protocol.ts +0 -99
- package/src/executors/quickjs-runtime.ts +0 -470
- package/src/executors/quickjs.ts +0 -776
- package/src/favicon.ts +0 -53
- package/src/index.ts +0 -666
- package/src/invocation.ts +0 -655
- package/src/json-schema.ts +0 -11
- package/src/mcp-result.ts +0 -43
- package/src/meta-tools.ts +0 -1607
- package/src/node.ts +0 -206
- package/src/operator-ui/browser.css +0 -423
- package/src/operator-ui/browser.ts +0 -1281
- package/src/operator-ui/generated.ts +0 -4
- package/src/operator-ui/model.ts +0 -111
- package/src/providers/cloudflare.ts +0 -3999
- package/src/providers/linear.ts +0 -302
- package/src/providers/mixpanel.ts +0 -229
- package/src/providers/notion.ts +0 -1879
- package/src/providers/stripe.ts +0 -307
- package/src/registry.ts +0 -1085
- package/src/retired-toolkits.ts +0 -14
- package/src/routes/access-tokens.ts +0 -115
- package/src/routes/activity.ts +0 -215
- package/src/routes/credentials.ts +0 -254
- package/src/routes/mcp.ts +0 -429
- package/src/routes/oauth.ts +0 -322
- package/src/routes/shared.ts +0 -247
- package/src/routes/ui.ts +0 -152
- package/src/server.ts +0 -195
- package/src/skills.ts +0 -271
- package/src/storage/file.ts +0 -123
- package/src/storage/memory.ts +0 -44
- package/src/timeout.ts +0 -84
- package/src/tool-safety.ts +0 -15
- package/src/types.ts +0 -523
- package/src/ui.ts +0 -744
- package/src/validate.ts +0 -387
- package/src/version.ts +0 -7
package/src/meta-tools.ts
DELETED
|
@@ -1,1607 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import type {
|
|
4
|
-
ActivityCallSource,
|
|
5
|
-
ActivityRequestContext,
|
|
6
|
-
} from "./activity.js";
|
|
7
|
-
import {
|
|
8
|
-
boundedDiscoveryText,
|
|
9
|
-
CatalogService,
|
|
10
|
-
DEFAULT_SEARCH_LIMIT,
|
|
11
|
-
DiscoveryPolicyError,
|
|
12
|
-
groupedSearchResult,
|
|
13
|
-
MAX_DESCRIBE_ADDRESSES,
|
|
14
|
-
MAX_DISCOVERY_RESULT_BYTES,
|
|
15
|
-
MAX_SEARCH_LIMIT,
|
|
16
|
-
} from "./catalog-service.js";
|
|
17
|
-
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
18
|
-
import type { CallErrorDetails } from "./errors.js";
|
|
19
|
-
import {
|
|
20
|
-
InvocationService,
|
|
21
|
-
MAX_RETRY_BACKOFF_MS,
|
|
22
|
-
retryBackoffMs,
|
|
23
|
-
type InvocationTiming,
|
|
24
|
-
} from "./invocation.js";
|
|
25
|
-
import {
|
|
26
|
-
isValidMaxResultBytes,
|
|
27
|
-
MIN_MAX_RESULT_BYTES,
|
|
28
|
-
resolveMaxResultBytes,
|
|
29
|
-
type RegistryView,
|
|
30
|
-
} from "./registry.js";
|
|
31
|
-
import {
|
|
32
|
-
hasConnectorGuides,
|
|
33
|
-
listSkills,
|
|
34
|
-
resolveSkill,
|
|
35
|
-
} from "./skills.js";
|
|
36
|
-
import {
|
|
37
|
-
DEFAULT_PROBE_TIMEOUT_MS,
|
|
38
|
-
normalizeTimeoutMs,
|
|
39
|
-
} from "./timeout.js";
|
|
40
|
-
import type { KVStorage } from "./types.js";
|
|
41
|
-
|
|
42
|
-
export {
|
|
43
|
-
MAX_DESCRIBE_ADDRESSES,
|
|
44
|
-
MAX_DISCOVERY_RESULT_BYTES,
|
|
45
|
-
MAX_RETRY_BACKOFF_MS,
|
|
46
|
-
MAX_SEARCH_LIMIT,
|
|
47
|
-
retryBackoffMs,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
interface TextContent {
|
|
51
|
-
type: "text";
|
|
52
|
-
text: string;
|
|
53
|
-
}
|
|
54
|
-
export interface ToolResult {
|
|
55
|
-
content: TextContent[];
|
|
56
|
-
isError?: boolean;
|
|
57
|
-
structuredContent?: Record<string, unknown>;
|
|
58
|
-
[x: string]: unknown;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const RESULT_TTL_SECONDS = 900;
|
|
62
|
-
const enc = new TextEncoder();
|
|
63
|
-
const dec = new TextDecoder();
|
|
64
|
-
|
|
65
|
-
export function jsonResult(obj: unknown): ToolResult {
|
|
66
|
-
return {
|
|
67
|
-
content: [{ type: "text", text: JSON.stringify(obj) }],
|
|
68
|
-
...(obj !== null && typeof obj === "object" && !Array.isArray(obj)
|
|
69
|
-
? { structuredContent: obj as Record<string, unknown> }
|
|
70
|
-
: {}),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function errorResult(message: string): ToolResult {
|
|
75
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function msg(err: unknown): string {
|
|
79
|
-
return err instanceof Error ? err.message : String(err);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
|
|
83
|
-
const result = jsonResult({
|
|
84
|
-
error: {
|
|
85
|
-
code: error.code,
|
|
86
|
-
message: error.message,
|
|
87
|
-
retryable: false,
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
result.isError = true;
|
|
91
|
-
return result;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function discoveryResult(
|
|
95
|
-
operation: () => unknown | Promise<unknown>,
|
|
96
|
-
hint: string,
|
|
97
|
-
): Promise<ToolResult> {
|
|
98
|
-
try {
|
|
99
|
-
const value = await operation();
|
|
100
|
-
const text = boundedDiscoveryText(value, hint);
|
|
101
|
-
return {
|
|
102
|
-
content: [{ type: "text", text }],
|
|
103
|
-
...(value !== null && typeof value === "object" && !Array.isArray(value)
|
|
104
|
-
? { structuredContent: value as Record<string, unknown> }
|
|
105
|
-
: {}),
|
|
106
|
-
};
|
|
107
|
-
} catch (err) {
|
|
108
|
-
if (err instanceof DiscoveryPolicyError) {
|
|
109
|
-
return discoveryErrorResult(err);
|
|
110
|
-
}
|
|
111
|
-
throw err;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
|
|
116
|
-
function isContinuationByte(b: number | undefined): boolean {
|
|
117
|
-
return b !== undefined && (b & 0xc0) === 0x80;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Smallest accepted `get_result` byte offset. */
|
|
121
|
-
const MIN_RESULT_OFFSET = 0;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* The one definition of a usable `get_result` offset: a whole number of bytes
|
|
125
|
-
* at or past {@link MIN_RESULT_OFFSET}. Shared by the registered zod schema and
|
|
126
|
-
* the handler's own check, the way `isValidMaxResultBytes` is shared across the
|
|
127
|
-
* cap's intake points (issue #32) — so a value valid at the wire is valid in
|
|
128
|
-
* process, and the two cannot drift.
|
|
129
|
-
*
|
|
130
|
-
* Everything else is rejected rather than coerced, because coercion is how an
|
|
131
|
-
* out-of-domain offset used to void a result silently: `Math.max(0, NaN)` is
|
|
132
|
-
* `NaN`, which slices to nothing, serializes as `"offset": null`, and reports
|
|
133
|
-
* no `nextOffset` — a caller sees a successful, empty result instead of an
|
|
134
|
-
* error. An offset past the end of the payload stays legal: it is a whole
|
|
135
|
-
* number of bytes, and it answers with an empty final page.
|
|
136
|
-
*/
|
|
137
|
-
function isValidResultOffset(value: number): boolean {
|
|
138
|
-
return Number.isInteger(value) && value >= MIN_RESULT_OFFSET;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Move a byte `offset` back to the nearest UTF-8 codepoint boundary in
|
|
143
|
-
* `[0, offset]`, so decoding from it never starts mid-character (which emits
|
|
144
|
-
* U+FFFD for the severed tail).
|
|
145
|
-
*
|
|
146
|
-
* Backwards, never forwards: re-serving a few bytes the caller already has is
|
|
147
|
-
* recoverable, silently skipping the rest of a character is not. Offsets the
|
|
148
|
-
* server itself produced (`nextOffset`) are already boundaries and come back
|
|
149
|
-
* unchanged, so this only moves an offset a client computed on its own
|
|
150
|
-
* (issue #38). An offset at or past `bytes.length` is left alone — there is no
|
|
151
|
-
* character there to split.
|
|
152
|
-
*/
|
|
153
|
-
export function alignStartToCharBoundary(
|
|
154
|
-
bytes: Uint8Array,
|
|
155
|
-
offset: number,
|
|
156
|
-
): number {
|
|
157
|
-
let o = offset;
|
|
158
|
-
while (o > 0 && isContinuationByte(bytes[o])) o--;
|
|
159
|
-
return o;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Move a byte `end` back to the nearest UTF-8 codepoint boundary in
|
|
164
|
-
* `(offset, total]`, so decoding `bytes[offset, end)` never splits a codepoint
|
|
165
|
-
* (which would emit U+FFFD and break byte-exact reassembly). If backing up
|
|
166
|
-
* would make no progress — a single codepoint wider than the window — extend
|
|
167
|
-
* forward to the end of that codepoint instead so paging always advances.
|
|
168
|
-
* Assumes `offset` is itself a codepoint boundary (offsets are the prior
|
|
169
|
-
* `nextOffset`, which this function guarantees, and 0 is always a boundary).
|
|
170
|
-
*
|
|
171
|
-
* The return is always `> offset` while `offset < total`, whatever `end` is
|
|
172
|
-
* asked for. That is the belt-and-braces half of issue #32: cap validation
|
|
173
|
-
* keeps an empty window from arising in the first place, and this keeps an
|
|
174
|
-
* empty window from turning into a `nextOffset === offset` paging loop if one
|
|
175
|
-
* ever does. Exported for direct testing of that invariant.
|
|
176
|
-
*/
|
|
177
|
-
export function alignEndToCharBoundary(
|
|
178
|
-
bytes: Uint8Array,
|
|
179
|
-
offset: number,
|
|
180
|
-
end: number,
|
|
181
|
-
total: number,
|
|
182
|
-
): number {
|
|
183
|
-
if (end >= total) return total;
|
|
184
|
-
// A window that reaches no further than `offset` yields no bytes and no
|
|
185
|
-
// progress; widen it to one byte and let the codepoint walk below finish it.
|
|
186
|
-
const wanted = Math.max(end, offset + 1);
|
|
187
|
-
let e = wanted;
|
|
188
|
-
while (e > offset && isContinuationByte(bytes[e])) e--;
|
|
189
|
-
if (e === offset) {
|
|
190
|
-
// Window is narrower than the codepoint at `offset`; take the whole thing.
|
|
191
|
-
e = wanted;
|
|
192
|
-
while (e < total && isContinuationByte(bytes[e])) e++;
|
|
193
|
-
}
|
|
194
|
-
return e;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// --- fields selection (feature 2) -----------------------------------------
|
|
198
|
-
|
|
199
|
-
/** Resolve a dot-path (segments) against a value; `key[]` maps the tail over an array. */
|
|
200
|
-
function resolvePath(value: unknown, segments: string[]): unknown {
|
|
201
|
-
const seg = segments[0];
|
|
202
|
-
if (seg === undefined) return value;
|
|
203
|
-
const rest = segments.slice(1);
|
|
204
|
-
const isArr = seg.endsWith("[]");
|
|
205
|
-
const key = isArr ? seg.slice(0, -2) : seg;
|
|
206
|
-
let next: unknown = value;
|
|
207
|
-
if (key !== "") {
|
|
208
|
-
if (value === null || typeof value !== "object") return undefined;
|
|
209
|
-
next = (value as Record<string, unknown>)[key];
|
|
210
|
-
}
|
|
211
|
-
if (isArr) {
|
|
212
|
-
if (!Array.isArray(next)) return undefined;
|
|
213
|
-
return next.map((el) => resolvePath(el, rest));
|
|
214
|
-
}
|
|
215
|
-
return resolvePath(next, rest);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const MAX_PROJECTION_AVAILABLE_FIELDS = 20;
|
|
219
|
-
const MAX_PROJECTION_SCHEMA_DEPTH = 12;
|
|
220
|
-
const MAX_PROJECTION_SCHEMA_NODES = 200;
|
|
221
|
-
const MAX_PROJECTION_SCHEMA_PATHS = 100;
|
|
222
|
-
const MAX_PROJECTION_PATH_CHARS = 256;
|
|
223
|
-
const MAX_PROJECTION_PATH_BYTES = 512;
|
|
224
|
-
const MAX_PROJECTION_TOTAL_PATH_CHARS = 512;
|
|
225
|
-
const MAX_PROJECTION_TOTAL_PATH_BYTES = 768;
|
|
226
|
-
const JSON_SCHEMA_TYPES = new Set([
|
|
227
|
-
"array",
|
|
228
|
-
"boolean",
|
|
229
|
-
"integer",
|
|
230
|
-
"null",
|
|
231
|
-
"number",
|
|
232
|
-
"object",
|
|
233
|
-
"string",
|
|
234
|
-
]);
|
|
235
|
-
const NON_SEMANTIC_REF_SIBLINGS = new Set([
|
|
236
|
-
"$anchor",
|
|
237
|
-
"$comment",
|
|
238
|
-
"$defs",
|
|
239
|
-
"$id",
|
|
240
|
-
"$ref",
|
|
241
|
-
"$schema",
|
|
242
|
-
"default",
|
|
243
|
-
"definitions",
|
|
244
|
-
"deprecated",
|
|
245
|
-
"description",
|
|
246
|
-
"examples",
|
|
247
|
-
"readOnly",
|
|
248
|
-
"title",
|
|
249
|
-
"writeOnly",
|
|
250
|
-
]);
|
|
251
|
-
|
|
252
|
-
interface FieldProjection {
|
|
253
|
-
data: Record<string, unknown>;
|
|
254
|
-
unmatchedFields: string[];
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
interface ProjectionFeedback {
|
|
258
|
-
data: Record<string, unknown>;
|
|
259
|
-
$connecta: {
|
|
260
|
-
type: "field_projection";
|
|
261
|
-
unmatchedFields: string[];
|
|
262
|
-
hint?: string;
|
|
263
|
-
schemaDeclared?: true;
|
|
264
|
-
schemaCoverage?: "complete" | "partial";
|
|
265
|
-
invalidFields?: string[];
|
|
266
|
-
availableFields?: string[];
|
|
267
|
-
availableFieldsTruncated?: true;
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
interface SchemaFieldAnalysis {
|
|
272
|
-
paths: string[];
|
|
273
|
-
complete: boolean;
|
|
274
|
-
truncated: boolean;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function localSchemaRef(root: unknown, ref: string): unknown | undefined {
|
|
278
|
-
if (ref === "#") return root;
|
|
279
|
-
if (
|
|
280
|
-
!ref.startsWith("#/") ||
|
|
281
|
-
ref.length > 2_048 ||
|
|
282
|
-
/~(?![01])/.test(ref)
|
|
283
|
-
) {
|
|
284
|
-
return undefined;
|
|
285
|
-
}
|
|
286
|
-
const segments = ref.slice(2).split("/");
|
|
287
|
-
if (segments.length > MAX_PROJECTION_SCHEMA_DEPTH) return undefined;
|
|
288
|
-
let current = root;
|
|
289
|
-
for (const encoded of segments) {
|
|
290
|
-
if (current === null || typeof current !== "object") return undefined;
|
|
291
|
-
const key = encoded.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
292
|
-
if (!Object.prototype.hasOwnProperty.call(current, key)) return undefined;
|
|
293
|
-
current = (current as Record<string, unknown>)[key];
|
|
294
|
-
}
|
|
295
|
-
return current;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function hasSchemaType(
|
|
299
|
-
schema: Record<string, unknown>,
|
|
300
|
-
wanted: string,
|
|
301
|
-
): boolean {
|
|
302
|
-
return (
|
|
303
|
-
schema.type === wanted ||
|
|
304
|
-
(Array.isArray(schema.type) && schema.type.includes(wanted))
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function selectableFieldName(name: string): boolean {
|
|
309
|
-
return name.length > 0 && !name.includes(".") && !name.endsWith("[]");
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Collect selectable output paths without trusting a schema more than JSON
|
|
314
|
-
* Schema permits. Traversal is iterative and budgeted before sorting or
|
|
315
|
-
* rendering, so a cyclic, extremely deep, or extremely broad downstream
|
|
316
|
-
* schema cannot turn projection feedback into unbounded host work.
|
|
317
|
-
*/
|
|
318
|
-
function analyzeSchemaFields(root: unknown): SchemaFieldAnalysis {
|
|
319
|
-
interface PendingSchema {
|
|
320
|
-
schema: unknown;
|
|
321
|
-
prefix: string;
|
|
322
|
-
depth: number;
|
|
323
|
-
ancestors: Set<unknown>;
|
|
324
|
-
}
|
|
325
|
-
const pending: PendingSchema[] = [
|
|
326
|
-
{ schema: root, prefix: "", depth: 0, ancestors: new Set() },
|
|
327
|
-
];
|
|
328
|
-
const paths = new Set<string>();
|
|
329
|
-
let nodes = 0;
|
|
330
|
-
let totalPathChars = 0;
|
|
331
|
-
let totalPathBytes = 0;
|
|
332
|
-
let complete = true;
|
|
333
|
-
let truncated = false;
|
|
334
|
-
|
|
335
|
-
const addPath = (path: string): boolean => {
|
|
336
|
-
if (paths.has(path)) return true;
|
|
337
|
-
// Check UTF-16 length before encoding, so a hostile multi-megabyte key
|
|
338
|
-
// never causes a same-sized temporary allocation merely to reject it.
|
|
339
|
-
if (
|
|
340
|
-
path.length > MAX_PROJECTION_PATH_CHARS ||
|
|
341
|
-
totalPathChars + path.length > MAX_PROJECTION_TOTAL_PATH_CHARS
|
|
342
|
-
) {
|
|
343
|
-
complete = false;
|
|
344
|
-
truncated = true;
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
const pathBytes = enc.encode(path).length;
|
|
348
|
-
if (
|
|
349
|
-
pathBytes > MAX_PROJECTION_PATH_BYTES ||
|
|
350
|
-
totalPathBytes + pathBytes > MAX_PROJECTION_TOTAL_PATH_BYTES ||
|
|
351
|
-
paths.size >= MAX_PROJECTION_SCHEMA_PATHS
|
|
352
|
-
) {
|
|
353
|
-
complete = false;
|
|
354
|
-
truncated = true;
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
paths.add(path);
|
|
358
|
-
totalPathChars += path.length;
|
|
359
|
-
totalPathBytes += pathBytes;
|
|
360
|
-
return true;
|
|
361
|
-
};
|
|
362
|
-
const enqueue = (item: PendingSchema): boolean => {
|
|
363
|
-
if (nodes + pending.length >= MAX_PROJECTION_SCHEMA_NODES) {
|
|
364
|
-
complete = false;
|
|
365
|
-
truncated = true;
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
pending.push(item);
|
|
369
|
-
return true;
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
while (pending.length > 0) {
|
|
373
|
-
const item = pending.pop()!;
|
|
374
|
-
if (item.depth > MAX_PROJECTION_SCHEMA_DEPTH) {
|
|
375
|
-
complete = false;
|
|
376
|
-
truncated = true;
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
if (++nodes > MAX_PROJECTION_SCHEMA_NODES) {
|
|
380
|
-
complete = false;
|
|
381
|
-
truncated = true;
|
|
382
|
-
break;
|
|
383
|
-
}
|
|
384
|
-
if (item.schema === false) continue;
|
|
385
|
-
if (
|
|
386
|
-
item.schema === true ||
|
|
387
|
-
item.schema === null ||
|
|
388
|
-
typeof item.schema !== "object" ||
|
|
389
|
-
item.ancestors.has(item.schema)
|
|
390
|
-
) {
|
|
391
|
-
complete = false;
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const schema = item.schema as Record<string, unknown>;
|
|
396
|
-
const ancestors = new Set(item.ancestors).add(item.schema);
|
|
397
|
-
let recognized = false;
|
|
398
|
-
if (
|
|
399
|
-
schema.type !== undefined &&
|
|
400
|
-
!(
|
|
401
|
-
(typeof schema.type === "string" &&
|
|
402
|
-
JSON_SCHEMA_TYPES.has(schema.type)) ||
|
|
403
|
-
(Array.isArray(schema.type) &&
|
|
404
|
-
schema.type.length > 0 &&
|
|
405
|
-
schema.type.every(
|
|
406
|
-
(type) =>
|
|
407
|
-
typeof type === "string" && JSON_SCHEMA_TYPES.has(type),
|
|
408
|
-
))
|
|
409
|
-
)
|
|
410
|
-
) {
|
|
411
|
-
complete = false;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (schema.$ref !== undefined) {
|
|
415
|
-
recognized = true;
|
|
416
|
-
let hasSemanticSiblings = false;
|
|
417
|
-
for (const key in schema) {
|
|
418
|
-
if (
|
|
419
|
-
Object.prototype.hasOwnProperty.call(schema, key) &&
|
|
420
|
-
!NON_SEMANTIC_REF_SIBLINGS.has(key)
|
|
421
|
-
) {
|
|
422
|
-
hasSemanticSiblings = true;
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (hasSemanticSiblings) {
|
|
427
|
-
// Modern JSON Schema applies $ref siblings as an intersection. A
|
|
428
|
-
// compact field walker cannot prove that intersection's selectable
|
|
429
|
-
// paths, so do not publish paths from either half as available.
|
|
430
|
-
complete = false;
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
const target =
|
|
434
|
-
typeof schema.$ref === "string"
|
|
435
|
-
? localSchemaRef(root, schema.$ref)
|
|
436
|
-
: undefined;
|
|
437
|
-
if (target === undefined) complete = false;
|
|
438
|
-
else {
|
|
439
|
-
enqueue({
|
|
440
|
-
schema: target,
|
|
441
|
-
prefix: item.prefix,
|
|
442
|
-
depth: item.depth + 1,
|
|
443
|
-
ancestors,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
449
|
-
const variants = schema[keyword];
|
|
450
|
-
if (!Array.isArray(variants)) continue;
|
|
451
|
-
recognized = true;
|
|
452
|
-
// Combining schemas can close or conditionally expose fields in ways
|
|
453
|
-
// this compact recovery walker intentionally does not prove.
|
|
454
|
-
complete = false;
|
|
455
|
-
for (const variant of variants) {
|
|
456
|
-
if (
|
|
457
|
-
!enqueue({
|
|
458
|
-
schema: variant,
|
|
459
|
-
prefix: item.prefix,
|
|
460
|
-
depth: item.depth + 1,
|
|
461
|
-
ancestors,
|
|
462
|
-
})
|
|
463
|
-
) {
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (truncated) break;
|
|
469
|
-
|
|
470
|
-
const properties = schema.properties;
|
|
471
|
-
const propertyRecord =
|
|
472
|
-
properties !== null &&
|
|
473
|
-
typeof properties === "object" &&
|
|
474
|
-
!Array.isArray(properties)
|
|
475
|
-
? (properties as Record<string, unknown>)
|
|
476
|
-
: undefined;
|
|
477
|
-
if (properties !== undefined && propertyRecord === undefined) {
|
|
478
|
-
complete = false;
|
|
479
|
-
}
|
|
480
|
-
const objectShape =
|
|
481
|
-
hasSchemaType(schema, "object") || propertyRecord !== undefined;
|
|
482
|
-
if (objectShape) {
|
|
483
|
-
recognized = true;
|
|
484
|
-
const patterns = schema.patternProperties;
|
|
485
|
-
let hasPatterns = false;
|
|
486
|
-
if (
|
|
487
|
-
patterns !== undefined &&
|
|
488
|
-
(patterns === null ||
|
|
489
|
-
typeof patterns !== "object" ||
|
|
490
|
-
Array.isArray(patterns))
|
|
491
|
-
) {
|
|
492
|
-
complete = false;
|
|
493
|
-
} else if (patterns !== undefined) {
|
|
494
|
-
for (const key in patterns as Record<string, unknown>) {
|
|
495
|
-
if (Object.prototype.hasOwnProperty.call(patterns, key)) {
|
|
496
|
-
hasPatterns = true;
|
|
497
|
-
break;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
if (schema.additionalProperties !== false || hasPatterns) {
|
|
502
|
-
complete = false;
|
|
503
|
-
}
|
|
504
|
-
if (propertyRecord) {
|
|
505
|
-
for (const key in propertyRecord) {
|
|
506
|
-
if (!Object.prototype.hasOwnProperty.call(propertyRecord, key)) {
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
if (++nodes > MAX_PROJECTION_SCHEMA_NODES) {
|
|
510
|
-
complete = false;
|
|
511
|
-
truncated = true;
|
|
512
|
-
break;
|
|
513
|
-
}
|
|
514
|
-
if (key.length > MAX_PROJECTION_PATH_CHARS) {
|
|
515
|
-
complete = false;
|
|
516
|
-
truncated = true;
|
|
517
|
-
break;
|
|
518
|
-
}
|
|
519
|
-
if (!selectableFieldName(key)) {
|
|
520
|
-
complete = false;
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
const child = propertyRecord[key];
|
|
524
|
-
// A false property schema forbids the property; advertising its name
|
|
525
|
-
// as selectable would turn an impossible value into a valid hint.
|
|
526
|
-
if (child === false) continue;
|
|
527
|
-
const path = item.prefix ? `${item.prefix}.${key}` : key;
|
|
528
|
-
if (!addPath(path)) break;
|
|
529
|
-
if (
|
|
530
|
-
!enqueue({
|
|
531
|
-
schema: child,
|
|
532
|
-
prefix: path,
|
|
533
|
-
depth: item.depth + 1,
|
|
534
|
-
ancestors,
|
|
535
|
-
})
|
|
536
|
-
) {
|
|
537
|
-
break;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
if (truncated) break;
|
|
543
|
-
|
|
544
|
-
const arrayShape =
|
|
545
|
-
hasSchemaType(schema, "array") ||
|
|
546
|
-
schema.items !== undefined ||
|
|
547
|
-
schema.prefixItems !== undefined;
|
|
548
|
-
if (arrayShape) {
|
|
549
|
-
recognized = true;
|
|
550
|
-
const arrayPath = `${item.prefix}[]`;
|
|
551
|
-
if (addPath(arrayPath)) {
|
|
552
|
-
if (Array.isArray(schema.prefixItems)) {
|
|
553
|
-
complete = false;
|
|
554
|
-
for (const child of schema.prefixItems) {
|
|
555
|
-
if (
|
|
556
|
-
!enqueue({
|
|
557
|
-
schema: child,
|
|
558
|
-
prefix: arrayPath,
|
|
559
|
-
depth: item.depth + 1,
|
|
560
|
-
ancestors,
|
|
561
|
-
})
|
|
562
|
-
) {
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
if (schema.items === undefined) {
|
|
568
|
-
if (!Array.isArray(schema.prefixItems)) complete = false;
|
|
569
|
-
} else if (schema.items === true || Array.isArray(schema.items)) {
|
|
570
|
-
complete = false;
|
|
571
|
-
const children = Array.isArray(schema.items)
|
|
572
|
-
? schema.items
|
|
573
|
-
: [];
|
|
574
|
-
for (const child of children) {
|
|
575
|
-
if (
|
|
576
|
-
!enqueue({
|
|
577
|
-
schema: child,
|
|
578
|
-
prefix: arrayPath,
|
|
579
|
-
depth: item.depth + 1,
|
|
580
|
-
ancestors,
|
|
581
|
-
})
|
|
582
|
-
) {
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
} else if (schema.items !== false) {
|
|
587
|
-
enqueue({
|
|
588
|
-
schema: schema.items,
|
|
589
|
-
prefix: arrayPath,
|
|
590
|
-
depth: item.depth + 1,
|
|
591
|
-
ancestors,
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const types = Array.isArray(schema.type)
|
|
598
|
-
? schema.type
|
|
599
|
-
: schema.type === undefined
|
|
600
|
-
? []
|
|
601
|
-
: [schema.type];
|
|
602
|
-
const primitiveOnly =
|
|
603
|
-
types.length > 0 &&
|
|
604
|
-
types.every(
|
|
605
|
-
(type) =>
|
|
606
|
-
type === "string" ||
|
|
607
|
-
type === "number" ||
|
|
608
|
-
type === "integer" ||
|
|
609
|
-
type === "boolean" ||
|
|
610
|
-
type === "null",
|
|
611
|
-
);
|
|
612
|
-
if (!recognized && !primitiveOnly) complete = false;
|
|
613
|
-
if (truncated) break;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return {
|
|
617
|
-
paths: [...paths].sort(),
|
|
618
|
-
complete,
|
|
619
|
-
truncated,
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function schemaProjectionFeedback(
|
|
624
|
-
outputSchema: unknown,
|
|
625
|
-
unmatchedFields: string[],
|
|
626
|
-
): Omit<ProjectionFeedback["$connecta"], "type" | "unmatchedFields"> {
|
|
627
|
-
const analysis = analyzeSchemaFields(outputSchema);
|
|
628
|
-
const available = new Set(analysis.paths);
|
|
629
|
-
const invalidFields = analysis.complete
|
|
630
|
-
? unmatchedFields.filter((field) => !available.has(field))
|
|
631
|
-
: [];
|
|
632
|
-
const missingArrayMarker = unmatchedFields.some((field) =>
|
|
633
|
-
analysis.paths.some(
|
|
634
|
-
(availableField) =>
|
|
635
|
-
availableField.includes("[]") &&
|
|
636
|
-
availableField.replaceAll("[]", "") === field,
|
|
637
|
-
),
|
|
638
|
-
);
|
|
639
|
-
return {
|
|
640
|
-
...(missingArrayMarker
|
|
641
|
-
? {
|
|
642
|
-
hint:
|
|
643
|
-
'Traverse arrays with [] after the array field name, for example "results[].id".',
|
|
644
|
-
}
|
|
645
|
-
: {}),
|
|
646
|
-
schemaDeclared: true,
|
|
647
|
-
schemaCoverage: analysis.complete ? "complete" : "partial",
|
|
648
|
-
...(invalidFields.length > 0 ? { invalidFields } : {}),
|
|
649
|
-
...(analysis.paths.length > 0
|
|
650
|
-
? {
|
|
651
|
-
availableFields: analysis.paths.slice(
|
|
652
|
-
0,
|
|
653
|
-
MAX_PROJECTION_AVAILABLE_FIELDS,
|
|
654
|
-
),
|
|
655
|
-
}
|
|
656
|
-
: {}),
|
|
657
|
-
...(analysis.truncated ||
|
|
658
|
-
analysis.paths.length > MAX_PROJECTION_AVAILABLE_FIELDS
|
|
659
|
-
? { availableFieldsTruncated: true as const }
|
|
660
|
-
: {}),
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/** Select the given dot-paths, retaining both matches and exact misses. */
|
|
665
|
-
function applyFields(
|
|
666
|
-
value: unknown,
|
|
667
|
-
fields: string[],
|
|
668
|
-
): FieldProjection {
|
|
669
|
-
const out: Record<string, unknown> = {};
|
|
670
|
-
const unmatchedFields: string[] = [];
|
|
671
|
-
for (const path of fields) {
|
|
672
|
-
const resolved = resolvePath(value, path.split("."));
|
|
673
|
-
if (resolved === undefined) unmatchedFields.push(path);
|
|
674
|
-
else out[path] = resolved;
|
|
675
|
-
}
|
|
676
|
-
return { data: out, unmatchedFields };
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/**
|
|
680
|
-
* Keep the historical flat projection when every path resolves. A miss gets a
|
|
681
|
-
* wrapper with a reserved discriminator so neither `{}` nor downstream fields
|
|
682
|
-
* named `data` / `projection` can be mistaken for projection feedback.
|
|
683
|
-
*/
|
|
684
|
-
function projectionValue(
|
|
685
|
-
value: unknown,
|
|
686
|
-
fields: string[],
|
|
687
|
-
outputSchema?: unknown,
|
|
688
|
-
): Record<string, unknown> | ProjectionFeedback {
|
|
689
|
-
const projected = applyFields(value, fields);
|
|
690
|
-
// `$connecta` is reserved at the top level of a projection. Even a fully
|
|
691
|
-
// matched downstream field with that exact name is escaped below `data`, so
|
|
692
|
-
// no user-controlled value can impersonate Connecta's discriminator.
|
|
693
|
-
const reservedCollision = Object.prototype.hasOwnProperty.call(
|
|
694
|
-
projected.data,
|
|
695
|
-
"$connecta",
|
|
696
|
-
);
|
|
697
|
-
if (projected.unmatchedFields.length === 0 && !reservedCollision) {
|
|
698
|
-
return projected.data;
|
|
699
|
-
}
|
|
700
|
-
return {
|
|
701
|
-
data: projected.data,
|
|
702
|
-
$connecta: {
|
|
703
|
-
type: "field_projection",
|
|
704
|
-
unmatchedFields: projected.unmatchedFields,
|
|
705
|
-
...(outputSchema
|
|
706
|
-
? schemaProjectionFeedback(outputSchema, projected.unmatchedFields)
|
|
707
|
-
: {}),
|
|
708
|
-
},
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/** Apply fields to each JSON-parseable text block; non-JSON blocks pass through. */
|
|
713
|
-
function applyFieldsToContent(
|
|
714
|
-
content: TextContent[],
|
|
715
|
-
fields: string[],
|
|
716
|
-
outputSchema?: unknown,
|
|
717
|
-
): TextContent[] {
|
|
718
|
-
return content.map((b) => {
|
|
719
|
-
if (b.type !== "text") return b;
|
|
720
|
-
let parsed: unknown;
|
|
721
|
-
try {
|
|
722
|
-
parsed = JSON.parse(b.text);
|
|
723
|
-
} catch {
|
|
724
|
-
return b;
|
|
725
|
-
}
|
|
726
|
-
return {
|
|
727
|
-
...b,
|
|
728
|
-
text: JSON.stringify(projectionValue(parsed, fields, outputSchema)),
|
|
729
|
-
};
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// --- result-size guard + get_result (feature 1) ---------------------------
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* The one serialization every result guard measures, stashes, and pages: JSON
|
|
737
|
-
* text for whatever JSON can represent, and `String(value)` for the returns
|
|
738
|
-
* JSON renders as `undefined` — a handler that returns nothing, a function, or
|
|
739
|
-
* a Symbol. `JSON.stringify` is *typed* as returning `string` while actually
|
|
740
|
-
* returning `undefined` for those, which is how a handler returning `undefined`
|
|
741
|
-
* reached clients as a `{"type":"text"}` block carrying no `text` at all: the
|
|
742
|
-
* size guard measured `enc.encode(undefined)` — the empty string, per the
|
|
743
|
-
* WebIDL default — and emitted the non-string unchanged (issue #42). `null`
|
|
744
|
-
* needs no special case; JSON renders it as `"null"`.
|
|
745
|
-
*
|
|
746
|
-
* Shared by `guardText`, `guardValue`, and execute_code's `guardResultValue` so
|
|
747
|
-
* the three give one answer to the same question. A value JSON cannot serialize
|
|
748
|
-
* at all (a BigInt) still throws, as before, and is reported as a failure.
|
|
749
|
-
*/
|
|
750
|
-
function serializeResultText(value: unknown): string {
|
|
751
|
-
const serialized = JSON.stringify(value);
|
|
752
|
-
return serialized === undefined ? String(value) : serialized;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
/**
|
|
756
|
-
* Stash `text` under `result:<uuid>` (ttl 900s) and describe it as the
|
|
757
|
-
* truncation notice every over-cap path hands back.
|
|
758
|
-
*/
|
|
759
|
-
async function stashResult(
|
|
760
|
-
text: string,
|
|
761
|
-
results: KVStorage,
|
|
762
|
-
totalBytes: number,
|
|
763
|
-
): Promise<{
|
|
764
|
-
truncated: true;
|
|
765
|
-
resultId: string;
|
|
766
|
-
totalBytes: number;
|
|
767
|
-
hint: string;
|
|
768
|
-
nextAction: {
|
|
769
|
-
tool: "get_result";
|
|
770
|
-
arguments: { id: string; offset: 0 };
|
|
771
|
-
};
|
|
772
|
-
}> {
|
|
773
|
-
const id = crypto.randomUUID();
|
|
774
|
-
await results.set(`result:${id}`, text, { ttlSeconds: RESULT_TTL_SECONDS });
|
|
775
|
-
return {
|
|
776
|
-
truncated: true,
|
|
777
|
-
resultId: id,
|
|
778
|
-
totalBytes,
|
|
779
|
-
hint: "use get_result {id, offset} to page, or re-call with fields to select less",
|
|
780
|
-
nextAction: {
|
|
781
|
-
tool: "get_result",
|
|
782
|
-
arguments: { id, offset: 0 },
|
|
783
|
-
},
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
interface GuardedResult<T> {
|
|
788
|
-
result: T;
|
|
789
|
-
truncated: boolean;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* Return `text` as a single content block; if it exceeds `cap` bytes, stash the
|
|
794
|
-
* full text and return the first `cap` bytes followed by a JSON truncation
|
|
795
|
-
* notice pointing at get_result. `bytes` is `text` already encoded, so a caller
|
|
796
|
-
* that had to measure it to make this decision doesn't encode it twice.
|
|
797
|
-
*/
|
|
798
|
-
async function guardEncoded(
|
|
799
|
-
text: string,
|
|
800
|
-
bytes: Uint8Array,
|
|
801
|
-
results: KVStorage,
|
|
802
|
-
cap: number,
|
|
803
|
-
): Promise<GuardedResult<ToolResult>> {
|
|
804
|
-
if (bytes.length <= cap) {
|
|
805
|
-
return {
|
|
806
|
-
result: { content: [{ type: "text", text }] },
|
|
807
|
-
truncated: false,
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
const notice = await stashResult(text, results, bytes.length);
|
|
811
|
-
const head = dec.decode(
|
|
812
|
-
bytes.slice(0, alignEndToCharBoundary(bytes, 0, cap, bytes.length)),
|
|
813
|
-
);
|
|
814
|
-
return {
|
|
815
|
-
result: {
|
|
816
|
-
content: [{ type: "text", text: `${head}\n${JSON.stringify(notice)}` }],
|
|
817
|
-
},
|
|
818
|
-
truncated: true,
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/** {@link guardEncoded} over a string that has not been measured yet. */
|
|
823
|
-
async function guardText(
|
|
824
|
-
text: string,
|
|
825
|
-
results: KVStorage,
|
|
826
|
-
cap: number,
|
|
827
|
-
): Promise<GuardedResult<ToolResult>> {
|
|
828
|
-
// `JSON.stringify`'s type says `string` where its behavior says `string |
|
|
829
|
-
// undefined`, so TypeScript alone does not keep a non-string out of here.
|
|
830
|
-
// Normalizing at the door means the size check below always measures exactly
|
|
831
|
-
// the text that is emitted, and no future caller can launder a non-string
|
|
832
|
-
// through it the way issue #42 describes.
|
|
833
|
-
const body: string =
|
|
834
|
-
typeof text === "string" ? text : serializeResultText(text);
|
|
835
|
-
return guardEncoded(body, enc.encode(body), results, cap);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/** Store an oversized JSON value and replace it with a page handle. */
|
|
839
|
-
async function guardValue(
|
|
840
|
-
value: unknown,
|
|
841
|
-
results: KVStorage,
|
|
842
|
-
cap: number,
|
|
843
|
-
): Promise<GuardedResult<unknown>> {
|
|
844
|
-
const text = serializeResultText(value);
|
|
845
|
-
const bytes = enc.encode(text);
|
|
846
|
-
if (bytes.length <= cap) return { result: value, truncated: false };
|
|
847
|
-
return {
|
|
848
|
-
result: await stashResult(text, results, bytes.length),
|
|
849
|
-
truncated: true,
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* Bound a downstream MCP `content` array by `cap`, measuring the serialized
|
|
855
|
-
* envelope — the same string that gets stashed and paged, and the one that
|
|
856
|
-
* counts every block rather than only the text ones.
|
|
857
|
-
*
|
|
858
|
-
* Both halves matter (issue #43). Measuring only text blocks meant an oversized
|
|
859
|
-
* all-image result scored zero bytes and was returned inline unbounded, with no
|
|
860
|
-
* `resultId` to page from; and measuring one string while truncating another
|
|
861
|
-
* left `totalBytes` and the served head describing something the cap was never
|
|
862
|
-
* compared against.
|
|
863
|
-
*
|
|
864
|
-
* Over the cap, what a client gets depends on whether a prefix is usable. An
|
|
865
|
-
* all-text envelope keeps the historical head + notice — a JSON prefix is still
|
|
866
|
-
* readable. An envelope carrying non-text blocks is replaced by the notice
|
|
867
|
-
* alone: the head of a half-written base64 image is of no use to anyone, and
|
|
868
|
-
* cutting one leaves unparseable block structure behind. Either way the full
|
|
869
|
-
* envelope is stashed and pages through `get_result`.
|
|
870
|
-
*/
|
|
871
|
-
async function guardContent(
|
|
872
|
-
content: TextContent[],
|
|
873
|
-
results: KVStorage,
|
|
874
|
-
cap: number,
|
|
875
|
-
): Promise<GuardedResult<ToolResult>> {
|
|
876
|
-
let text: string;
|
|
877
|
-
try {
|
|
878
|
-
text = JSON.stringify(content);
|
|
879
|
-
} catch {
|
|
880
|
-
// A block carrying a BigInt or a cycle cannot be serialized, so it cannot
|
|
881
|
-
// be measured, stashed, or paged either — there is nothing this guard could
|
|
882
|
-
// do with it. Pass it through as the old text-only measure did, rather than
|
|
883
|
-
// turning a call that used to succeed into result_processing_failed.
|
|
884
|
-
return { result: { content }, truncated: false };
|
|
885
|
-
}
|
|
886
|
-
const bytes = enc.encode(text);
|
|
887
|
-
// Under the cap the downstream blocks pass through untouched, non-text ones
|
|
888
|
-
// included, in their original order.
|
|
889
|
-
if (bytes.length <= cap) {
|
|
890
|
-
return { result: { content }, truncated: false };
|
|
891
|
-
}
|
|
892
|
-
if (content.every((b) => b.type === "text")) {
|
|
893
|
-
return guardEncoded(text, bytes, results, cap);
|
|
894
|
-
}
|
|
895
|
-
const notice = await stashResult(text, results, bytes.length);
|
|
896
|
-
return {
|
|
897
|
-
result: { content: [{ type: "text", text: JSON.stringify(notice) }] },
|
|
898
|
-
truncated: true,
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// --- compact schema rendering (feature 3a) --------------------------------
|
|
903
|
-
|
|
904
|
-
// --- argument shapes -------------------------------------------------------
|
|
905
|
-
|
|
906
|
-
export interface SearchArgs {
|
|
907
|
-
query?: string;
|
|
908
|
-
connector?: string;
|
|
909
|
-
safety?: "readOnly" | "approvalRequired" | "all";
|
|
910
|
-
limit?: number;
|
|
911
|
-
offset?: number;
|
|
912
|
-
fullDescriptions?: boolean;
|
|
913
|
-
includeSchemas?: "compact" | "json";
|
|
914
|
-
}
|
|
915
|
-
type ResultMode = "mcp" | "value";
|
|
916
|
-
export interface CallArgs {
|
|
917
|
-
address: string;
|
|
918
|
-
args?: Record<string, unknown>;
|
|
919
|
-
fields?: string[];
|
|
920
|
-
resultMode?: ResultMode;
|
|
921
|
-
timeoutMs?: number;
|
|
922
|
-
/** Retries after the first attempt; honored only for safely annotated tools. */
|
|
923
|
-
maxRetries?: number;
|
|
924
|
-
/** Include connector/catalog/result-processing timing segments. */
|
|
925
|
-
diagnostics?: boolean;
|
|
926
|
-
}
|
|
927
|
-
export interface DestructiveCallArgs extends CallArgs {
|
|
928
|
-
/** Short model-authored context for the host's approval UI; never downstream input. */
|
|
929
|
-
reason?: string;
|
|
930
|
-
}
|
|
931
|
-
export interface GetResultArgs {
|
|
932
|
-
id: string;
|
|
933
|
-
/**
|
|
934
|
-
* Byte offset to page from; a whole number >= 0, aligned back to the nearest
|
|
935
|
-
* character boundary and reported as the response's `offset`. Defaults to 0.
|
|
936
|
-
*/
|
|
937
|
-
offset?: number;
|
|
938
|
-
/** Page size in bytes; a whole number >= 1. Defaults to the deployment cap. */
|
|
939
|
-
maxBytes?: number;
|
|
940
|
-
}
|
|
941
|
-
export interface AuthorizeArgs {
|
|
942
|
-
connector: string;
|
|
943
|
-
force?: boolean;
|
|
944
|
-
}
|
|
945
|
-
export interface SkillArgs {
|
|
946
|
-
name?: string;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
/**
|
|
950
|
-
* The sentence that closes the OAuth handoff, telling the operator's agent how
|
|
951
|
-
* to confirm the flow landed through the one surface it can call.
|
|
952
|
-
*/
|
|
953
|
-
function oauthFollowUp(connectorId: string): string {
|
|
954
|
-
return `Then retry the original call; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside execute_code confirms the catalog now loads.`;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
/**
|
|
958
|
-
* Every meta-tool handler over a registry, one per registered tool. Exported for
|
|
959
|
-
* direct testing; registerMetaTools() wires the six explicit tools onto an
|
|
960
|
-
* McpServer. `opts.defaultToolTimeoutMs` supplies a deadline for calls that
|
|
961
|
-
* don't carry one. (execute_code is registered separately by
|
|
962
|
-
* registerExecuteTool, and builds its own services over the same registry.)
|
|
963
|
-
*
|
|
964
|
-
* What execute_code shares with these handlers is the services layer beneath
|
|
965
|
-
* them — `CatalogService` and `InvocationService` — not the handlers, which no
|
|
966
|
-
* in-program path calls.
|
|
967
|
-
*
|
|
968
|
-
* Deployment-wide result-size caps are read off the registry view rather than
|
|
969
|
-
* passed in: `ConnectaConfig.calls.maxResultBytes` and its per-connector
|
|
970
|
-
* override each have one runtime source of truth.
|
|
971
|
-
*/
|
|
972
|
-
export function createMetaTools(
|
|
973
|
-
registry: RegistryView,
|
|
974
|
-
baseUrl: string,
|
|
975
|
-
opts: {
|
|
976
|
-
/** Deadline applied when a call passes no `timeoutMs`. Off when unset. */
|
|
977
|
-
defaultToolTimeoutMs?: number;
|
|
978
|
-
/** Per-connector deadline for the search/describe probe fan-out. Default 30_000. */
|
|
979
|
-
probeTimeoutMs?: number;
|
|
980
|
-
/** Maximum simultaneous connector discovery operations. Default 4. */
|
|
981
|
-
discoveryConcurrency?: number;
|
|
982
|
-
activity?: ActivityRequestContext;
|
|
983
|
-
/** Inbound request cancellation shared by every call this request makes. */
|
|
984
|
-
requestSignal?: AbortSignal;
|
|
985
|
-
} = {},
|
|
986
|
-
) {
|
|
987
|
-
// Already normalized and warned about at registry construction.
|
|
988
|
-
const globalCap = registry.maxResultBytes;
|
|
989
|
-
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
|
|
990
|
-
const probeTimeoutMs =
|
|
991
|
-
normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
992
|
-
const discoveryConcurrency = resolveDiscoveryConcurrency(
|
|
993
|
-
opts.discoveryConcurrency,
|
|
994
|
-
);
|
|
995
|
-
// createMetaTools() is called once per inbound MCP request. Sharing this
|
|
996
|
-
// identity lets remote connectors reuse one downstream client inside that
|
|
997
|
-
// request without leaking request-bound I/O into the next one.
|
|
998
|
-
const requestScope = {};
|
|
999
|
-
const catalog = new CatalogService(registry, baseUrl, {
|
|
1000
|
-
requestScope,
|
|
1001
|
-
probeTimeoutMs,
|
|
1002
|
-
concurrency: discoveryConcurrency,
|
|
1003
|
-
// searchRoute keeps its top-level default. In-program callers use a
|
|
1004
|
-
// separate CatalogService configured for connecta.search.
|
|
1005
|
-
});
|
|
1006
|
-
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
1007
|
-
|
|
1008
|
-
interface RunCallOutcome {
|
|
1009
|
-
toolResult: ToolResult;
|
|
1010
|
-
durationMs: number;
|
|
1011
|
-
attempts: number;
|
|
1012
|
-
timing: InvocationTiming;
|
|
1013
|
-
value?: unknown;
|
|
1014
|
-
error?: CallErrorDetails;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
interface ProcessedCallResult {
|
|
1018
|
-
toolResult: ToolResult;
|
|
1019
|
-
value?: unknown;
|
|
1020
|
-
/**
|
|
1021
|
-
* Friction on a call that *succeeded*. It travels as a friction class, not
|
|
1022
|
-
* as an `errorCode`, so persistence keyed on "this row has an error code"
|
|
1023
|
-
* keeps counting failures rather than truncations.
|
|
1024
|
-
*/
|
|
1025
|
-
friction?: "result_too_large";
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
1029
|
-
async function runCall(
|
|
1030
|
-
call: CallArgs,
|
|
1031
|
-
source: ActivityCallSource,
|
|
1032
|
-
options: { allowDestructive?: boolean } = {},
|
|
1033
|
-
): Promise<RunCallOutcome> {
|
|
1034
|
-
const results = registry.resultsStorage();
|
|
1035
|
-
const fields = call.fields && call.fields.length > 0 ? call.fields : null;
|
|
1036
|
-
const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
|
|
1037
|
-
const outcome = await invocation.invoke<ProcessedCallResult>(
|
|
1038
|
-
call.address,
|
|
1039
|
-
call.args ?? {},
|
|
1040
|
-
{
|
|
1041
|
-
source,
|
|
1042
|
-
...(options.allowDestructive !== undefined
|
|
1043
|
-
? { allowDestructive: options.allowDestructive }
|
|
1044
|
-
: {}),
|
|
1045
|
-
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
1046
|
-
...(call.maxRetries !== undefined
|
|
1047
|
-
? { maxRetries: call.maxRetries }
|
|
1048
|
-
: {}),
|
|
1049
|
-
...(opts.requestSignal !== undefined
|
|
1050
|
-
? { requestSignal: opts.requestSignal }
|
|
1051
|
-
: {}),
|
|
1052
|
-
unwrapResult: call.resultMode === "value",
|
|
1053
|
-
processResult: async (result, resolved) => {
|
|
1054
|
-
// Result-size cap for THIS call: the connector's own override wins,
|
|
1055
|
-
// then the deployment-wide value, then the built-in default (already
|
|
1056
|
-
// folded into `globalCap`). Resolved per call so one request can
|
|
1057
|
-
// mix a tight-capped connector with siblings on the global cap. An
|
|
1058
|
-
// override the registry already warned about at startup is dropped
|
|
1059
|
-
// here, so the connector simply inherits `globalCap`.
|
|
1060
|
-
const cap = resolveMaxResultBytes(
|
|
1061
|
-
resolved.connector.maxResultBytes,
|
|
1062
|
-
globalCap,
|
|
1063
|
-
);
|
|
1064
|
-
if (call.resultMode === "value") {
|
|
1065
|
-
let value = fields
|
|
1066
|
-
? projectionValue(
|
|
1067
|
-
result,
|
|
1068
|
-
fields,
|
|
1069
|
-
resolved.definition.outputSchema,
|
|
1070
|
-
)
|
|
1071
|
-
: result;
|
|
1072
|
-
const guarded = await guardValue(value, results, cap);
|
|
1073
|
-
value = guarded.result;
|
|
1074
|
-
return {
|
|
1075
|
-
toolResult: jsonResult({ ok: true, data: value }),
|
|
1076
|
-
value,
|
|
1077
|
-
...(guarded.truncated
|
|
1078
|
-
? { friction: "result_too_large" as const }
|
|
1079
|
-
: {}),
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
if (resolved.connector.kind === "mcp") {
|
|
1083
|
-
const mcpResult = result as { content?: TextContent[] };
|
|
1084
|
-
let content = mcpResult?.content ?? [];
|
|
1085
|
-
if (fields) {
|
|
1086
|
-
content = applyFieldsToContent(
|
|
1087
|
-
content,
|
|
1088
|
-
fields,
|
|
1089
|
-
resolved.definition.outputSchema,
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
const guarded = await guardContent(content, results, cap);
|
|
1093
|
-
return {
|
|
1094
|
-
toolResult: guarded.result,
|
|
1095
|
-
...(guarded.truncated
|
|
1096
|
-
? { friction: "result_too_large" as const }
|
|
1097
|
-
: {}),
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
const value = fields
|
|
1101
|
-
? projectionValue(
|
|
1102
|
-
result,
|
|
1103
|
-
fields,
|
|
1104
|
-
resolved.definition.outputSchema,
|
|
1105
|
-
)
|
|
1106
|
-
: result;
|
|
1107
|
-
const guarded = await guardText(
|
|
1108
|
-
serializeResultText(value),
|
|
1109
|
-
results,
|
|
1110
|
-
cap,
|
|
1111
|
-
);
|
|
1112
|
-
return {
|
|
1113
|
-
toolResult: guarded.result,
|
|
1114
|
-
value,
|
|
1115
|
-
...(guarded.truncated
|
|
1116
|
-
? { friction: "result_too_large" as const }
|
|
1117
|
-
: {}),
|
|
1118
|
-
};
|
|
1119
|
-
},
|
|
1120
|
-
activityFriction: (processed) => processed.friction,
|
|
1121
|
-
},
|
|
1122
|
-
);
|
|
1123
|
-
if (!outcome.ok) {
|
|
1124
|
-
const structuredRecovery = outcome.error.nextAction !== undefined;
|
|
1125
|
-
const failedResult =
|
|
1126
|
-
structuredRecovery ||
|
|
1127
|
-
outcome.error.code === "auth_required" ||
|
|
1128
|
-
outcome.error.code === "invalid_args" ||
|
|
1129
|
-
outcome.error.code === "input_required_unsupported" ||
|
|
1130
|
-
call.resultMode === "value"
|
|
1131
|
-
? jsonResult({
|
|
1132
|
-
ok: false,
|
|
1133
|
-
error: outcome.error,
|
|
1134
|
-
durationMs: outcome.durationMs,
|
|
1135
|
-
attempts: outcome.attempts,
|
|
1136
|
-
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
1137
|
-
})
|
|
1138
|
-
: errorResult(outcome.error.message);
|
|
1139
|
-
if (
|
|
1140
|
-
structuredRecovery ||
|
|
1141
|
-
outcome.error.code === "auth_required" ||
|
|
1142
|
-
outcome.error.code === "invalid_args" ||
|
|
1143
|
-
outcome.error.code === "input_required_unsupported"
|
|
1144
|
-
) {
|
|
1145
|
-
failedResult.isError = true;
|
|
1146
|
-
}
|
|
1147
|
-
return {
|
|
1148
|
-
toolResult: failedResult,
|
|
1149
|
-
durationMs: outcome.durationMs,
|
|
1150
|
-
attempts: outcome.attempts,
|
|
1151
|
-
timing: outcome.timing,
|
|
1152
|
-
error: outcome.error,
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
const valueModeResult =
|
|
1156
|
-
call.resultMode === "value"
|
|
1157
|
-
? jsonResult({
|
|
1158
|
-
ok: true,
|
|
1159
|
-
data: outcome.value.value,
|
|
1160
|
-
durationMs: outcome.durationMs,
|
|
1161
|
-
attempts: outcome.attempts,
|
|
1162
|
-
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
1163
|
-
})
|
|
1164
|
-
: outcome.value.toolResult;
|
|
1165
|
-
return {
|
|
1166
|
-
toolResult: valueModeResult,
|
|
1167
|
-
durationMs: outcome.durationMs,
|
|
1168
|
-
attempts: outcome.attempts,
|
|
1169
|
-
timing: outcome.timing,
|
|
1170
|
-
...(Object.prototype.hasOwnProperty.call(outcome.value, "value")
|
|
1171
|
-
? { value: outcome.value.value }
|
|
1172
|
-
: {}),
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
return {
|
|
1177
|
-
async skills(args: SkillArgs = {}): Promise<ToolResult> {
|
|
1178
|
-
const connectors = registry.listConnectors();
|
|
1179
|
-
if (!args.name) {
|
|
1180
|
-
return {
|
|
1181
|
-
content: [
|
|
1182
|
-
{
|
|
1183
|
-
type: "text",
|
|
1184
|
-
text:
|
|
1185
|
-
'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
|
|
1186
|
-
listSkills(connectors)
|
|
1187
|
-
.map((skill) => `- \`${skill.name}\` — ${skill.description}`)
|
|
1188
|
-
.join("\n"),
|
|
1189
|
-
},
|
|
1190
|
-
],
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
const skill = resolveSkill(args.name, connectors);
|
|
1194
|
-
if (!skill.found) return errorResult(skill.message);
|
|
1195
|
-
return { content: [{ type: "text", text: skill.content }] };
|
|
1196
|
-
},
|
|
1197
|
-
|
|
1198
|
-
async searchTools(args: SearchArgs): Promise<ToolResult> {
|
|
1199
|
-
return discoveryResult(
|
|
1200
|
-
async () =>
|
|
1201
|
-
groupedSearchResult(
|
|
1202
|
-
await catalog.search({
|
|
1203
|
-
...args,
|
|
1204
|
-
includeSchemaKeys: args.includeSchemas !== undefined,
|
|
1205
|
-
}),
|
|
1206
|
-
),
|
|
1207
|
-
"Request a smaller limit, omit fullDescriptions, or use compact schemas.",
|
|
1208
|
-
);
|
|
1209
|
-
},
|
|
1210
|
-
|
|
1211
|
-
async callTool(args: CallArgs): Promise<ToolResult> {
|
|
1212
|
-
return (await runCall(args, "call_tool")).toolResult;
|
|
1213
|
-
},
|
|
1214
|
-
|
|
1215
|
-
async callDestructiveTool(args: DestructiveCallArgs): Promise<ToolResult> {
|
|
1216
|
-
// `reason` is read by the host's approval view and stops there — runCall
|
|
1217
|
-
// forwards only the call fields, so it never reaches the connector.
|
|
1218
|
-
return (
|
|
1219
|
-
await runCall(args, "call_destructive_tool", { allowDestructive: true })
|
|
1220
|
-
).toolResult;
|
|
1221
|
-
},
|
|
1222
|
-
|
|
1223
|
-
async getResult(args: GetResultArgs): Promise<ToolResult> {
|
|
1224
|
-
// Client-supplied page size and offset: normal input-validation errors,
|
|
1225
|
-
// not clamps. Callers arriving over MCP are rejected earlier by the
|
|
1226
|
-
// registered zod schema and never reach these branches, so they exist for
|
|
1227
|
-
// in-process callers of createMetaTools — which have no schema in front
|
|
1228
|
-
// of them — and to keep the rules true of the handler on its own terms.
|
|
1229
|
-
if (
|
|
1230
|
-
args.maxBytes !== undefined &&
|
|
1231
|
-
!isValidMaxResultBytes(args.maxBytes)
|
|
1232
|
-
) {
|
|
1233
|
-
return errorResult(
|
|
1234
|
-
`Invalid maxBytes ${args.maxBytes}: must be a whole number of bytes ` +
|
|
1235
|
-
`>= ${MIN_MAX_RESULT_BYTES}. Omit it to use the deployment default.`,
|
|
1236
|
-
);
|
|
1237
|
-
}
|
|
1238
|
-
if (args.offset !== undefined && !isValidResultOffset(args.offset)) {
|
|
1239
|
-
return errorResult(
|
|
1240
|
-
`Invalid offset ${args.offset}: must be a whole number of bytes ` +
|
|
1241
|
-
`>= ${MIN_RESULT_OFFSET}. Omit it to start at the beginning.`,
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
|
-
const results = registry.resultsStorage();
|
|
1245
|
-
const stored = await results.get(`result:${args.id}`);
|
|
1246
|
-
if (stored === null || stored === undefined) {
|
|
1247
|
-
return errorResult(`Unknown or expired result id "${args.id}"`);
|
|
1248
|
-
}
|
|
1249
|
-
const bytes = enc.encode(stored);
|
|
1250
|
-
const total = bytes.length;
|
|
1251
|
-
// Validated above, so no coercion is needed here — only alignment. A
|
|
1252
|
-
// client that computes its own offsets can land inside a multi-byte
|
|
1253
|
-
// character, which would decode as U+FFFD; the offset actually served is
|
|
1254
|
-
// the boundary at or before it, and it is what the response reports back
|
|
1255
|
-
// as `offset` (issue #38).
|
|
1256
|
-
const offset = alignStartToCharBoundary(bytes, args.offset ?? 0);
|
|
1257
|
-
// Page size only: a stashed result carries no connector identity, so
|
|
1258
|
-
// get_result keeps the deployment-wide default when none is requested.
|
|
1259
|
-
// Both sides are validated by now — the argument above, `globalCap` at
|
|
1260
|
-
// intake — so `offset + maxBytes` always reaches past `offset`.
|
|
1261
|
-
const maxBytes = args.maxBytes ?? globalCap;
|
|
1262
|
-
// Align the slice end to a codepoint boundary so a multi-byte char is
|
|
1263
|
-
// never split across pages (which would emit U+FFFD on both sides).
|
|
1264
|
-
// `nextOffset` is this aligned end, so it is a valid boundary for the
|
|
1265
|
-
// next call and paging reassembles the original byte-for-byte.
|
|
1266
|
-
const end = alignEndToCharBoundary(
|
|
1267
|
-
bytes,
|
|
1268
|
-
offset,
|
|
1269
|
-
offset + maxBytes,
|
|
1270
|
-
total,
|
|
1271
|
-
);
|
|
1272
|
-
const slice = dec.decode(bytes.slice(offset, end));
|
|
1273
|
-
const nextOffset = end < total ? end : undefined;
|
|
1274
|
-
return jsonResult({
|
|
1275
|
-
offset,
|
|
1276
|
-
...(nextOffset !== undefined ? { nextOffset } : {}),
|
|
1277
|
-
totalBytes: total,
|
|
1278
|
-
text: slice,
|
|
1279
|
-
});
|
|
1280
|
-
},
|
|
1281
|
-
|
|
1282
|
-
async authorizeConnector(args: AuthorizeArgs): Promise<ToolResult> {
|
|
1283
|
-
const connector = registry.getConnector(args.connector);
|
|
1284
|
-
if (!connector) {
|
|
1285
|
-
return errorResult(`Unknown connector "${args.connector}"`);
|
|
1286
|
-
}
|
|
1287
|
-
if (!connector.startAuth) {
|
|
1288
|
-
if (!connector.credential) {
|
|
1289
|
-
return jsonResult({
|
|
1290
|
-
connector: connector.id,
|
|
1291
|
-
recovery: "unavailable",
|
|
1292
|
-
message:
|
|
1293
|
-
`Connector "${connector.id}" declares neither downstream OAuth ` +
|
|
1294
|
-
"nor an operator-managed credential slot. Update the connector " +
|
|
1295
|
-
"or deployment configuration before retrying.",
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
const ctx = registry.contextFor(
|
|
1299
|
-
connector.id,
|
|
1300
|
-
baseUrl,
|
|
1301
|
-
requestScope,
|
|
1302
|
-
);
|
|
1303
|
-
if (!ctx.credential) {
|
|
1304
|
-
return jsonResult({
|
|
1305
|
-
connector: connector.id,
|
|
1306
|
-
recovery: "unavailable",
|
|
1307
|
-
message:
|
|
1308
|
-
"Credential storage is not configured. Configure " +
|
|
1309
|
-
"credentials.encryptionKey, redeploy, then call " +
|
|
1310
|
-
"authorize_connector again.",
|
|
1311
|
-
});
|
|
1312
|
-
}
|
|
1313
|
-
const fields = connector.credential.fields?.map((field) => ({
|
|
1314
|
-
name: field.name,
|
|
1315
|
-
guidance: field.description ?? field.label,
|
|
1316
|
-
})) ?? [
|
|
1317
|
-
{
|
|
1318
|
-
name: "value",
|
|
1319
|
-
guidance:
|
|
1320
|
-
connector.credential.description ??
|
|
1321
|
-
connector.credential.label,
|
|
1322
|
-
},
|
|
1323
|
-
];
|
|
1324
|
-
return jsonResult({
|
|
1325
|
-
connector: connector.id,
|
|
1326
|
-
recovery: "operator_config",
|
|
1327
|
-
credential: {
|
|
1328
|
-
label: connector.credential.label,
|
|
1329
|
-
fields,
|
|
1330
|
-
},
|
|
1331
|
-
operatorUrl: new URL("/credentials", baseUrl).toString(),
|
|
1332
|
-
instructions:
|
|
1333
|
-
"Have the operator open operatorUrl, set and test the credential, " +
|
|
1334
|
-
"then retry the original call. No redeploy is needed. Credential " +
|
|
1335
|
-
"mutation requires a Clerk-authenticated operator.",
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
|
|
1339
|
-
try {
|
|
1340
|
-
const status = await connector.startAuth(
|
|
1341
|
-
ctx,
|
|
1342
|
-
args.force !== undefined ? { force: args.force } : {},
|
|
1343
|
-
);
|
|
1344
|
-
if (status.state === "auth_required" && !status.authorizationUrl) {
|
|
1345
|
-
// auth_required with nothing to open is a dead end for the operator.
|
|
1346
|
-
return errorResult(
|
|
1347
|
-
`Connector "${connector.id}": authorization required but no URL is available — retry authorize_connector.`,
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
return jsonResult({
|
|
1351
|
-
connector: connector.id,
|
|
1352
|
-
recovery: "oauth",
|
|
1353
|
-
status: status.state,
|
|
1354
|
-
...(status.authorizationUrl
|
|
1355
|
-
? {
|
|
1356
|
-
authorizationUrl: status.authorizationUrl,
|
|
1357
|
-
instructions:
|
|
1358
|
-
"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. " +
|
|
1359
|
-
oauthFollowUp(connector.id),
|
|
1360
|
-
}
|
|
1361
|
-
: {}),
|
|
1362
|
-
...(status.message ? { message: status.message } : {}),
|
|
1363
|
-
});
|
|
1364
|
-
} catch (err) {
|
|
1365
|
-
return errorResult(msg(err));
|
|
1366
|
-
} finally {
|
|
1367
|
-
// Auth state may have changed — even on a throw or a half-wiped force —
|
|
1368
|
-
// so don't serve a stale tool list.
|
|
1369
|
-
await registry.invalidateStored(connector.id);
|
|
1370
|
-
}
|
|
1371
|
-
},
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
const SEARCH_DESC = `Use top-level search only for exactly one unreduced read, then call_tool, or for write-capable work, then call_destructive_tool. For read-only reduction, dependent or multiple calls, never search here: make one execute_code program that searches and calls. Use 2–4 distinctive action/object terms, not the full request; set connector to the obvious integration id to load one catalog instead of all; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}), page to ${MAX_SEARCH_LIMIT} if needed. safety="readOnly" returns only calls available to call_tool/code; "approvalRequired" returns the rest; omitted/"all" returns all. This filters results, not authority. includeSchemas="compact" adds the input and any declared output shape, bounded; plain objects expose inputKeys, requiredInputKeys, and outputKeys; truncation flags mark incomplete shapes; matches also carry declared annotations. Require purpose/address fit plus compatible inputs, truncation, safety, and outputs — never the first lexical match. Empty or whitespace-only query browses all; non-empty input with no ASCII terms returns no match.`;
|
|
1376
|
-
const CALL_DESC =
|
|
1377
|
-
'Use for ONE tool explicitly annotated readOnlyHint: true — the cheapest path for a single cold call. For two or more calls, dependent steps, loops, joins, or data reduction use execute_code, whose connecta.call and connecta.batch reach the same tools. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths; traverse arrays with [] (for example results[].id). Misses return data plus `$connecta` feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
1378
|
-
const CALL_DESTRUCTIVE_DESC =
|
|
1379
|
-
"Invoke any tool that is not explicitly annotated readOnlyHint: true, including unannotated, write-capable, or destructive tools. Include a short reason explaining the intended consequence for the human reviewer; it grants no authority and is never passed downstream. The MCP destructiveHint on this meta-tool lets the host request human approval before execution. Use only after reviewing the downstream tool schema and consequences.";
|
|
1380
|
-
const GET_RESULT_DESC =
|
|
1381
|
-
"Page a truncated result stashed by call_tool or call_destructive_tool; a program's oversized return is not paged, so reduce it in code instead. Input { id, offset?, maxBytes? } → { text, offset, nextOffset?, totalBytes } sliced by byte offset. maxBytes is a whole number of bytes >= 1 (omit for the deployment default) and offset a whole number of bytes >= 0; an offset inside a multi-byte character is moved back to that character's first byte and the offset served is returned. Unknown/expired id is an error.";
|
|
1382
|
-
const AUTHORIZE_DESC =
|
|
1383
|
-
"Use after auth_required. Returns an OAuth or operator-credential handoff, or reports required deployment configuration. force=true restarts OAuth only; this tool never accepts credentials.";
|
|
1384
|
-
const SKILLS_DESC =
|
|
1385
|
-
'List or fetch concise guidance for choosing among Connecta meta-tools. Call skills({ name: "usage" }) once when the routing workflow is unfamiliar; do not refetch it in the same task.';
|
|
1386
|
-
|
|
1387
|
-
const SEARCH_WITH_DESCRIBE_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
|
|
1388
|
-
|
|
1389
|
-
/**
|
|
1390
|
-
* Sentences appended to a meta-tool description only when this connection
|
|
1391
|
-
* actually has connector guides. Tool descriptions are always-loaded context,
|
|
1392
|
-
* so a deployment with no guides gets every base description unchanged rather
|
|
1393
|
-
* than paying for text about a feature it does not use.
|
|
1394
|
-
*
|
|
1395
|
-
* Registration is per connection and reads the configured connector set.
|
|
1396
|
-
*/
|
|
1397
|
-
const GUIDE_NOTES = {
|
|
1398
|
-
skills:
|
|
1399
|
-
" skills({}) also lists this deployment's scoped connector guides; fetch only an exact name listed there or carried by discovery, never one inferred from a connector id.",
|
|
1400
|
-
search:
|
|
1401
|
-
" A result carrying `guide` also carries a bounded `guideSummary`. `guideRequired: true` is a hard stop: fetch that exact guide before calling. `guideRequiredReasons` explains why — `connector_required` and `approval_required` stand however you expand the schema; `schema_truncated` clears once describe returns the exact one. Otherwise fetch only when the summary names a connector convention relevant to the task. A complete, unambiguous read-only schema needs no otherwise-irrelevant guide fetch.",
|
|
1402
|
-
destructive:
|
|
1403
|
-
" Before a consequential call, inspect the address through discovery or describe and fetch any connector guide it names.",
|
|
1404
|
-
} as const;
|
|
1405
|
-
|
|
1406
|
-
/** `base`, plus its guide note when any VISIBLE connector carries a guide. */
|
|
1407
|
-
function describedFor(
|
|
1408
|
-
registry: RegistryView,
|
|
1409
|
-
base: string,
|
|
1410
|
-
note: keyof typeof GUIDE_NOTES,
|
|
1411
|
-
): string {
|
|
1412
|
-
return hasConnectorGuides(registry.listConnectors())
|
|
1413
|
-
? base + GUIDE_NOTES[note]
|
|
1414
|
-
: base;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
/**
|
|
1418
|
-
* Connecta refuses downstream tools that are not explicitly annotated
|
|
1419
|
-
* read-only, so its own meta-tools must carry the same hints — otherwise a
|
|
1420
|
-
* host that gates on annotations prompts for every search, and a connecta
|
|
1421
|
-
* aggregated behind another connecta would be refused by its own policy.
|
|
1422
|
-
*/
|
|
1423
|
-
const READ_ONLY_REMOTE = {
|
|
1424
|
-
readOnlyHint: true,
|
|
1425
|
-
destructiveHint: false,
|
|
1426
|
-
openWorldHint: true,
|
|
1427
|
-
} as const;
|
|
1428
|
-
|
|
1429
|
-
/** Read-only and served entirely from connecta's own storage. */
|
|
1430
|
-
const READ_ONLY_LOCAL = {
|
|
1431
|
-
readOnlyHint: true,
|
|
1432
|
-
destructiveHint: false,
|
|
1433
|
-
idempotentHint: true,
|
|
1434
|
-
openWorldHint: false,
|
|
1435
|
-
} as const;
|
|
1436
|
-
|
|
1437
|
-
const CALL_INPUT_SCHEMA = {
|
|
1438
|
-
address: z.string(),
|
|
1439
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1440
|
-
fields: z.array(z.string()).optional(),
|
|
1441
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1442
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1443
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1444
|
-
diagnostics: z.boolean().optional(),
|
|
1445
|
-
};
|
|
1446
|
-
|
|
1447
|
-
/**
|
|
1448
|
-
* Register the six explicit meta-tools onto an McpServer instance.
|
|
1449
|
-
* `registerExecuteTool` adds the seventh, `execute_code`. Broad discovery and
|
|
1450
|
-
* multi-call work is reached through `connecta.search` / `connecta.describe` /
|
|
1451
|
-
* `connecta.batch` inside a program, which `execute_code` builds over the same
|
|
1452
|
-
* `CatalogService` and `InvocationService` these handlers use — one shared
|
|
1453
|
-
* services layer, two adapters above it.
|
|
1454
|
-
*/
|
|
1455
|
-
export function registerMetaTools(
|
|
1456
|
-
server: McpServer,
|
|
1457
|
-
registry: RegistryView,
|
|
1458
|
-
ctx: {
|
|
1459
|
-
baseUrl: string;
|
|
1460
|
-
defaultToolTimeoutMs?: number;
|
|
1461
|
-
probeTimeoutMs?: number;
|
|
1462
|
-
discoveryConcurrency?: number;
|
|
1463
|
-
activity?: ActivityRequestContext;
|
|
1464
|
-
requestSignal?: AbortSignal;
|
|
1465
|
-
},
|
|
1466
|
-
): void {
|
|
1467
|
-
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1468
|
-
...(ctx.defaultToolTimeoutMs !== undefined
|
|
1469
|
-
? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
|
|
1470
|
-
: {}),
|
|
1471
|
-
...(ctx.probeTimeoutMs !== undefined
|
|
1472
|
-
? { probeTimeoutMs: ctx.probeTimeoutMs }
|
|
1473
|
-
: {}),
|
|
1474
|
-
...(ctx.discoveryConcurrency !== undefined
|
|
1475
|
-
? { discoveryConcurrency: ctx.discoveryConcurrency }
|
|
1476
|
-
: {}),
|
|
1477
|
-
...(ctx.activity !== undefined ? { activity: ctx.activity } : {}),
|
|
1478
|
-
...(ctx.requestSignal !== undefined
|
|
1479
|
-
? { requestSignal: ctx.requestSignal }
|
|
1480
|
-
: {}),
|
|
1481
|
-
});
|
|
1482
|
-
|
|
1483
|
-
server.registerTool(
|
|
1484
|
-
"skills",
|
|
1485
|
-
{
|
|
1486
|
-
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
1487
|
-
inputSchema: z.object({ name: z.string().optional() }),
|
|
1488
|
-
annotations: READ_ONLY_LOCAL,
|
|
1489
|
-
_meta: { ui: { visibility: ["model"] } },
|
|
1490
|
-
},
|
|
1491
|
-
async (args) => mt.skills(args as SkillArgs),
|
|
1492
|
-
);
|
|
1493
|
-
|
|
1494
|
-
server.registerTool(
|
|
1495
|
-
"search_tools",
|
|
1496
|
-
{
|
|
1497
|
-
description: describedFor(
|
|
1498
|
-
registry,
|
|
1499
|
-
SEARCH_WITH_DESCRIBE_DESC,
|
|
1500
|
-
"search",
|
|
1501
|
-
),
|
|
1502
|
-
inputSchema: z.object({
|
|
1503
|
-
query: z.string().optional(),
|
|
1504
|
-
connector: z.string().optional(),
|
|
1505
|
-
safety: z
|
|
1506
|
-
.enum(["readOnly", "approvalRequired", "all"])
|
|
1507
|
-
.optional(),
|
|
1508
|
-
limit: z.number().int().positive().max(MAX_SEARCH_LIMIT).optional(),
|
|
1509
|
-
offset: z.number().int().nonnegative().optional(),
|
|
1510
|
-
fullDescriptions: z.boolean().optional(),
|
|
1511
|
-
includeSchemas: z.enum(["compact", "json"]).optional(),
|
|
1512
|
-
}),
|
|
1513
|
-
annotations: READ_ONLY_REMOTE,
|
|
1514
|
-
_meta: { ui: { visibility: ["model"] } },
|
|
1515
|
-
},
|
|
1516
|
-
async (args) => mt.searchTools(args as SearchArgs),
|
|
1517
|
-
);
|
|
1518
|
-
|
|
1519
|
-
server.registerTool(
|
|
1520
|
-
"call_tool",
|
|
1521
|
-
{
|
|
1522
|
-
description: CALL_DESC,
|
|
1523
|
-
inputSchema: z.object(CALL_INPUT_SCHEMA),
|
|
1524
|
-
// call_tool admits only tools that are themselves explicitly read-only;
|
|
1525
|
-
// anything else is refused and routed to call_destructive_tool.
|
|
1526
|
-
annotations: READ_ONLY_REMOTE,
|
|
1527
|
-
// The trusted program-view shell delegates bounded named reads here.
|
|
1528
|
-
// It is already one of the seven model tools; app visibility adds no
|
|
1529
|
-
// tool and this handler repeats ordinary fail-closed read admission.
|
|
1530
|
-
_meta: { ui: { visibility: ["model", "app"] } },
|
|
1531
|
-
},
|
|
1532
|
-
async (args) => mt.callTool(args as CallArgs),
|
|
1533
|
-
);
|
|
1534
|
-
|
|
1535
|
-
server.registerTool(
|
|
1536
|
-
"call_destructive_tool",
|
|
1537
|
-
{
|
|
1538
|
-
description: describedFor(
|
|
1539
|
-
registry,
|
|
1540
|
-
CALL_DESTRUCTIVE_DESC,
|
|
1541
|
-
"destructive",
|
|
1542
|
-
),
|
|
1543
|
-
inputSchema: z.object({
|
|
1544
|
-
...CALL_INPUT_SCHEMA,
|
|
1545
|
-
// Bounded above, but with no lower bound: a model that sends `""` or
|
|
1546
|
-
// whitespace has written no reason, and failing an entire consequential
|
|
1547
|
-
// call over a cosmetic field the host merely displays is the wrong
|
|
1548
|
-
// trade. It is normalized to absent below instead.
|
|
1549
|
-
reason: z.string().max(500).optional(),
|
|
1550
|
-
}),
|
|
1551
|
-
annotations: {
|
|
1552
|
-
destructiveHint: true,
|
|
1553
|
-
readOnlyHint: false,
|
|
1554
|
-
openWorldHint: true,
|
|
1555
|
-
},
|
|
1556
|
-
_meta: { ui: { visibility: ["model"] } },
|
|
1557
|
-
},
|
|
1558
|
-
async (args) => {
|
|
1559
|
-
// `reason` is the host's to display and connecta's to keep out of the
|
|
1560
|
-
// downstream call, so this destructuring is the whole of its handling:
|
|
1561
|
-
// nothing below reads it. Dropping it is also what makes an empty or
|
|
1562
|
-
// whitespace-only one "absent" rather than a validation failure — there
|
|
1563
|
-
// is no field left for it to be absent from.
|
|
1564
|
-
const { reason: _hostContext, ...call } = args as DestructiveCallArgs;
|
|
1565
|
-
return mt.callDestructiveTool(call);
|
|
1566
|
-
},
|
|
1567
|
-
);
|
|
1568
|
-
|
|
1569
|
-
server.registerTool(
|
|
1570
|
-
"authorize_connector",
|
|
1571
|
-
{
|
|
1572
|
-
description: AUTHORIZE_DESC,
|
|
1573
|
-
inputSchema: z.object({
|
|
1574
|
-
connector: z.string(),
|
|
1575
|
-
force: z.boolean().optional(),
|
|
1576
|
-
}),
|
|
1577
|
-
// Starts (or with force, resets) a downstream OAuth flow — it changes
|
|
1578
|
-
// stored connector auth state, so it is deliberately not read-only.
|
|
1579
|
-
annotations: {
|
|
1580
|
-
readOnlyHint: false,
|
|
1581
|
-
destructiveHint: false,
|
|
1582
|
-
openWorldHint: true,
|
|
1583
|
-
},
|
|
1584
|
-
_meta: { ui: { visibility: ["model"] } },
|
|
1585
|
-
},
|
|
1586
|
-
async (args) => mt.authorizeConnector(args as AuthorizeArgs),
|
|
1587
|
-
);
|
|
1588
|
-
|
|
1589
|
-
server.registerTool(
|
|
1590
|
-
"get_result",
|
|
1591
|
-
{
|
|
1592
|
-
description: GET_RESULT_DESC,
|
|
1593
|
-
inputSchema: z.object({
|
|
1594
|
-
id: z.string(),
|
|
1595
|
-
// Both bounds are the shared rules (isValidResultOffset,
|
|
1596
|
-
// isValidMaxResultBytes) expressed for the wire: spelling them against
|
|
1597
|
-
// the same constants keeps the schema from drifting away from the
|
|
1598
|
-
// in-handler checks if either floor ever moves.
|
|
1599
|
-
offset: z.number().int().min(MIN_RESULT_OFFSET).optional(),
|
|
1600
|
-
maxBytes: z.number().int().min(MIN_MAX_RESULT_BYTES).optional(),
|
|
1601
|
-
}),
|
|
1602
|
-
annotations: READ_ONLY_LOCAL,
|
|
1603
|
-
_meta: { ui: { visibility: ["model"] } },
|
|
1604
|
-
},
|
|
1605
|
-
async (args) => mt.getResult(args as GetResultArgs),
|
|
1606
|
-
);
|
|
1607
|
-
}
|