@zackbart/connecta 0.10.5 → 0.11.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/AGENTS.md +8 -6
- package/CHANGELOG.md +150 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +0 -7
- package/dist/activity.d.ts +11 -1
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +44 -3
- package/dist/activity.js.map +1 -1
- package/dist/catalog-service.d.ts +24 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +68 -9
- package/dist/catalog-service.js.map +1 -1
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +1 -1
- package/dist/errors.d.ts +49 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +68 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +73 -3
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +161 -29
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +28 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -37
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +9 -2
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +61 -31
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +24 -59
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +107 -359
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +1 -1
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/registry.d.ts +12 -10
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -17
- package/dist/registry.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +19 -21
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/shared.d.ts +9 -11
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/server.js +5 -4
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +8 -18
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +13 -60
- package/dist/skills.js.map +1 -1
- package/dist/types.d.ts +6 -20
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-first-exploration.md +16 -16
- package/documentation/code-mode.md +137 -63
- package/documentation/connectors.md +1 -1
- package/documentation/meta-tools.md +96 -33
- package/documentation/rich-output-design.md +212 -0
- package/ethos.md +17 -19
- package/examples/node/README.md +1 -2
- package/examples/node/src/index.ts +1 -3
- package/examples/worker/README.md +19 -16
- package/examples/worker/src/d1-activity-row.ts +40 -0
- package/examples/worker/src/d1-activity.ts +3 -2
- package/examples/worker/src/index.ts +6 -14
- package/examples/worker/wrangler.jsonc +3 -6
- package/package.json +1 -1
- package/src/activity.ts +69 -3
- package/src/catalog-service.ts +113 -20
- package/src/connectors/api.ts +2 -2
- package/src/connectors/remote-mcp.ts +1 -1
- package/src/errors.ts +104 -3
- package/src/execute.ts +237 -37
- package/src/index.ts +60 -67
- package/src/invocation.ts +61 -19
- package/src/meta-tools.ts +136 -482
- package/src/operator-ui/browser.ts +10 -2
- package/src/operator-ui/generated.ts +1 -1
- package/src/registry.ts +7 -35
- package/src/routes/mcp.ts +19 -21
- package/src/routes/shared.ts +8 -11
- package/src/server.ts +7 -7
- package/src/skills.ts +11 -74
- package/src/types.ts +6 -21
- package/src/version.ts +1 -1
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +1 -1
package/src/meta-tools.ts
CHANGED
|
@@ -14,19 +14,8 @@ import {
|
|
|
14
14
|
MAX_DISCOVERY_RESULT_BYTES,
|
|
15
15
|
MAX_SEARCH_LIMIT,
|
|
16
16
|
} from "./catalog-service.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
resolveDiscoveryConcurrency,
|
|
20
|
-
} from "./concurrency.js";
|
|
21
|
-
import {
|
|
22
|
-
closeConnectorScope,
|
|
23
|
-
type DeferredWork,
|
|
24
|
-
} from "./connector-scope.js";
|
|
25
|
-
import {
|
|
26
|
-
classifyCallError,
|
|
27
|
-
messageLooksRetryable,
|
|
28
|
-
type CallErrorDetails,
|
|
29
|
-
} from "./errors.js";
|
|
17
|
+
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
18
|
+
import type { CallErrorDetails } from "./errors.js";
|
|
30
19
|
import {
|
|
31
20
|
InvocationService,
|
|
32
21
|
MAX_RETRY_BACKOFF_MS,
|
|
@@ -47,13 +36,8 @@ import {
|
|
|
47
36
|
import {
|
|
48
37
|
DEFAULT_PROBE_TIMEOUT_MS,
|
|
49
38
|
normalizeTimeoutMs,
|
|
50
|
-
withAbortableTimeout,
|
|
51
39
|
} from "./timeout.js";
|
|
52
|
-
import type {
|
|
53
|
-
ConnectaSurface,
|
|
54
|
-
ConnectorStatus,
|
|
55
|
-
KVStorage,
|
|
56
|
-
} from "./types.js";
|
|
40
|
+
import type { KVStorage } from "./types.js";
|
|
57
41
|
|
|
58
42
|
export {
|
|
59
43
|
MAX_DESCRIBE_ADDRESSES,
|
|
@@ -95,10 +79,6 @@ function msg(err: unknown): string {
|
|
|
95
79
|
return err instanceof Error ? err.message : String(err);
|
|
96
80
|
}
|
|
97
81
|
|
|
98
|
-
function errorDetails(code: string, message: string): CallErrorDetails {
|
|
99
|
-
return { code, message, retryable: messageLooksRetryable(message) };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
82
|
function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
|
|
103
83
|
const result = jsonResult({
|
|
104
84
|
error: {
|
|
@@ -132,7 +112,6 @@ async function discoveryResult(
|
|
|
132
112
|
}
|
|
133
113
|
}
|
|
134
114
|
|
|
135
|
-
|
|
136
115
|
/** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
|
|
137
116
|
function isContinuationByte(b: number | undefined): boolean {
|
|
138
117
|
return b !== undefined && (b & 0xc0) === 0x80;
|
|
@@ -786,6 +765,10 @@ async function stashResult(
|
|
|
786
765
|
resultId: string;
|
|
787
766
|
totalBytes: number;
|
|
788
767
|
hint: string;
|
|
768
|
+
nextAction: {
|
|
769
|
+
tool: "get_result";
|
|
770
|
+
arguments: { id: string; offset: 0 };
|
|
771
|
+
};
|
|
789
772
|
}> {
|
|
790
773
|
const id = crypto.randomUUID();
|
|
791
774
|
await results.set(`result:${id}`, text, { ttlSeconds: RESULT_TTL_SECONDS });
|
|
@@ -794,16 +777,16 @@ async function stashResult(
|
|
|
794
777
|
resultId: id,
|
|
795
778
|
totalBytes,
|
|
796
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
|
+
},
|
|
797
784
|
};
|
|
798
785
|
}
|
|
799
786
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const maxBytes = 512;
|
|
804
|
-
if (bytes.length <= maxBytes) return value;
|
|
805
|
-
const end = alignEndToCharBoundary(bytes, 0, maxBytes, bytes.length);
|
|
806
|
-
return `${dec.decode(bytes.slice(0, end))}…`;
|
|
787
|
+
interface GuardedResult<T> {
|
|
788
|
+
result: T;
|
|
789
|
+
truncated: boolean;
|
|
807
790
|
}
|
|
808
791
|
|
|
809
792
|
/**
|
|
@@ -817,16 +800,22 @@ async function guardEncoded(
|
|
|
817
800
|
bytes: Uint8Array,
|
|
818
801
|
results: KVStorage,
|
|
819
802
|
cap: number,
|
|
820
|
-
): Promise<ToolResult
|
|
803
|
+
): Promise<GuardedResult<ToolResult>> {
|
|
821
804
|
if (bytes.length <= cap) {
|
|
822
|
-
return {
|
|
805
|
+
return {
|
|
806
|
+
result: { content: [{ type: "text", text }] },
|
|
807
|
+
truncated: false,
|
|
808
|
+
};
|
|
823
809
|
}
|
|
824
810
|
const notice = await stashResult(text, results, bytes.length);
|
|
825
811
|
const head = dec.decode(
|
|
826
812
|
bytes.slice(0, alignEndToCharBoundary(bytes, 0, cap, bytes.length)),
|
|
827
813
|
);
|
|
828
814
|
return {
|
|
829
|
-
|
|
815
|
+
result: {
|
|
816
|
+
content: [{ type: "text", text: `${head}\n${JSON.stringify(notice)}` }],
|
|
817
|
+
},
|
|
818
|
+
truncated: true,
|
|
830
819
|
};
|
|
831
820
|
}
|
|
832
821
|
|
|
@@ -835,7 +824,7 @@ async function guardText(
|
|
|
835
824
|
text: string,
|
|
836
825
|
results: KVStorage,
|
|
837
826
|
cap: number,
|
|
838
|
-
): Promise<ToolResult
|
|
827
|
+
): Promise<GuardedResult<ToolResult>> {
|
|
839
828
|
// `JSON.stringify`'s type says `string` where its behavior says `string |
|
|
840
829
|
// undefined`, so TypeScript alone does not keep a non-string out of here.
|
|
841
830
|
// Normalizing at the door means the size check below always measures exactly
|
|
@@ -851,11 +840,14 @@ async function guardValue(
|
|
|
851
840
|
value: unknown,
|
|
852
841
|
results: KVStorage,
|
|
853
842
|
cap: number,
|
|
854
|
-
): Promise<unknown
|
|
843
|
+
): Promise<GuardedResult<unknown>> {
|
|
855
844
|
const text = serializeResultText(value);
|
|
856
845
|
const bytes = enc.encode(text);
|
|
857
|
-
if (bytes.length <= cap) return value;
|
|
858
|
-
return
|
|
846
|
+
if (bytes.length <= cap) return { result: value, truncated: false };
|
|
847
|
+
return {
|
|
848
|
+
result: await stashResult(text, results, bytes.length),
|
|
849
|
+
truncated: true,
|
|
850
|
+
};
|
|
859
851
|
}
|
|
860
852
|
|
|
861
853
|
/**
|
|
@@ -880,7 +872,7 @@ async function guardContent(
|
|
|
880
872
|
content: TextContent[],
|
|
881
873
|
results: KVStorage,
|
|
882
874
|
cap: number,
|
|
883
|
-
): Promise<ToolResult
|
|
875
|
+
): Promise<GuardedResult<ToolResult>> {
|
|
884
876
|
let text: string;
|
|
885
877
|
try {
|
|
886
878
|
text = JSON.stringify(content);
|
|
@@ -889,17 +881,22 @@ async function guardContent(
|
|
|
889
881
|
// be measured, stashed, or paged either — there is nothing this guard could
|
|
890
882
|
// do with it. Pass it through as the old text-only measure did, rather than
|
|
891
883
|
// turning a call that used to succeed into result_processing_failed.
|
|
892
|
-
return { content };
|
|
884
|
+
return { result: { content }, truncated: false };
|
|
893
885
|
}
|
|
894
886
|
const bytes = enc.encode(text);
|
|
895
887
|
// Under the cap the downstream blocks pass through untouched, non-text ones
|
|
896
888
|
// included, in their original order.
|
|
897
|
-
if (bytes.length <= cap)
|
|
889
|
+
if (bytes.length <= cap) {
|
|
890
|
+
return { result: { content }, truncated: false };
|
|
891
|
+
}
|
|
898
892
|
if (content.every((b) => b.type === "text")) {
|
|
899
893
|
return guardEncoded(text, bytes, results, cap);
|
|
900
894
|
}
|
|
901
895
|
const notice = await stashResult(text, results, bytes.length);
|
|
902
|
-
return {
|
|
896
|
+
return {
|
|
897
|
+
result: { content: [{ type: "text", text: JSON.stringify(notice) }] },
|
|
898
|
+
truncated: true,
|
|
899
|
+
};
|
|
903
900
|
}
|
|
904
901
|
|
|
905
902
|
// --- compact schema rendering (feature 3a) --------------------------------
|
|
@@ -915,17 +912,6 @@ export interface SearchArgs {
|
|
|
915
912
|
fullDescriptions?: boolean;
|
|
916
913
|
includeSchemas?: "compact" | "json";
|
|
917
914
|
}
|
|
918
|
-
export type DescribeArgs = (
|
|
919
|
-
| { address: string; addresses?: never }
|
|
920
|
-
| { address?: never; addresses: string[] }
|
|
921
|
-
) & {
|
|
922
|
-
format?: "compact" | "json";
|
|
923
|
-
fullDescriptions?: boolean;
|
|
924
|
-
};
|
|
925
|
-
export interface ListArgs {
|
|
926
|
-
/** When false, return cached/observed health without downstream I/O. */
|
|
927
|
-
probe?: boolean;
|
|
928
|
-
}
|
|
929
915
|
type ResultMode = "mcp" | "value";
|
|
930
916
|
export interface CallArgs {
|
|
931
917
|
address: string;
|
|
@@ -938,6 +924,10 @@ export interface CallArgs {
|
|
|
938
924
|
/** Include connector/catalog/result-processing timing segments. */
|
|
939
925
|
diagnostics?: boolean;
|
|
940
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
|
+
}
|
|
941
931
|
export interface GetResultArgs {
|
|
942
932
|
id: string;
|
|
943
933
|
/**
|
|
@@ -948,14 +938,6 @@ export interface GetResultArgs {
|
|
|
948
938
|
/** Page size in bytes; a whole number >= 1. Defaults to the deployment cap. */
|
|
949
939
|
maxBytes?: number;
|
|
950
940
|
}
|
|
951
|
-
export type BatchCall = CallArgs;
|
|
952
|
-
export interface BatchArgs {
|
|
953
|
-
calls: BatchCall[];
|
|
954
|
-
resultMode?: ResultMode;
|
|
955
|
-
timeoutMs?: number;
|
|
956
|
-
maxRetries?: number;
|
|
957
|
-
diagnostics?: boolean;
|
|
958
|
-
}
|
|
959
941
|
export interface AuthorizeArgs {
|
|
960
942
|
connector: string;
|
|
961
943
|
force?: boolean;
|
|
@@ -965,17 +947,27 @@ export interface SkillArgs {
|
|
|
965
947
|
}
|
|
966
948
|
|
|
967
949
|
/**
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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.
|
|
974
967
|
*
|
|
975
968
|
* Deployment-wide result-size caps are read off the registry view rather than
|
|
976
|
-
* passed in: `ConnectaConfig.calls.maxResultBytes
|
|
977
|
-
*
|
|
978
|
-
* have one runtime source of truth.
|
|
969
|
+
* passed in: `ConnectaConfig.calls.maxResultBytes` and its per-connector
|
|
970
|
+
* override each have one runtime source of truth.
|
|
979
971
|
*/
|
|
980
972
|
export function createMetaTools(
|
|
981
973
|
registry: RegistryView,
|
|
@@ -983,27 +975,17 @@ export function createMetaTools(
|
|
|
983
975
|
opts: {
|
|
984
976
|
/** Deadline applied when a call passes no `timeoutMs`. Off when unset. */
|
|
985
977
|
defaultToolTimeoutMs?: number;
|
|
986
|
-
/** Per-connector deadline for the
|
|
978
|
+
/** Per-connector deadline for the search/describe probe fan-out. Default 30_000. */
|
|
987
979
|
probeTimeoutMs?: number;
|
|
988
980
|
/** Maximum simultaneous connector discovery operations. Default 4. */
|
|
989
981
|
discoveryConcurrency?: number;
|
|
990
982
|
activity?: ActivityRequestContext;
|
|
991
|
-
/** Inbound request cancellation shared by
|
|
983
|
+
/** Inbound request cancellation shared by every call this request makes. */
|
|
992
984
|
requestSignal?: AbortSignal;
|
|
993
|
-
/** Runtime continuation for the bounded tail of probe-owned teardown. */
|
|
994
|
-
defer?: DeferredWork;
|
|
995
|
-
/**
|
|
996
|
-
* The advertised surface, which the `skills` guidance must match: a
|
|
997
|
-
* code-first deployment never gets guidance naming a tool it does not
|
|
998
|
-
* advertise. Default `classic`.
|
|
999
|
-
*/
|
|
1000
|
-
surface?: ConnectaSurface;
|
|
1001
985
|
} = {},
|
|
1002
986
|
) {
|
|
1003
|
-
const surface: ConnectaSurface = opts.surface ?? "classic";
|
|
1004
987
|
// Already normalized and warned about at registry construction.
|
|
1005
988
|
const globalCap = registry.maxResultBytes;
|
|
1006
|
-
const batchCap = registry.maxBatchResultBytes;
|
|
1007
989
|
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
|
|
1008
990
|
const probeTimeoutMs =
|
|
1009
991
|
normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
@@ -1018,20 +1000,10 @@ export function createMetaTools(
|
|
|
1018
1000
|
requestScope,
|
|
1019
1001
|
probeTimeoutMs,
|
|
1020
1002
|
concurrency: discoveryConcurrency,
|
|
1003
|
+
// searchRoute keeps its top-level default. In-program callers use a
|
|
1004
|
+
// separate CatalogService configured for connecta.search.
|
|
1021
1005
|
});
|
|
1022
1006
|
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
1023
|
-
const withProbeDeadline = <T>(
|
|
1024
|
-
label: string,
|
|
1025
|
-
operation: (options: {
|
|
1026
|
-
signal: AbortSignal;
|
|
1027
|
-
timeoutMs: number;
|
|
1028
|
-
}) => Promise<T>,
|
|
1029
|
-
) =>
|
|
1030
|
-
withAbortableTimeout(
|
|
1031
|
-
(signal) => operation({ signal, timeoutMs: probeTimeoutMs }),
|
|
1032
|
-
probeTimeoutMs,
|
|
1033
|
-
label,
|
|
1034
|
-
);
|
|
1035
1007
|
|
|
1036
1008
|
interface RunCallOutcome {
|
|
1037
1009
|
toolResult: ToolResult;
|
|
@@ -1045,11 +1017,17 @@ export function createMetaTools(
|
|
|
1045
1017
|
interface ProcessedCallResult {
|
|
1046
1018
|
toolResult: ToolResult;
|
|
1047
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";
|
|
1048
1026
|
}
|
|
1049
1027
|
|
|
1050
1028
|
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
1051
1029
|
async function runCall(
|
|
1052
|
-
call:
|
|
1030
|
+
call: CallArgs,
|
|
1053
1031
|
source: ActivityCallSource,
|
|
1054
1032
|
options: { allowDestructive?: boolean } = {},
|
|
1055
1033
|
): Promise<RunCallOutcome> {
|
|
@@ -1075,7 +1053,7 @@ export function createMetaTools(
|
|
|
1075
1053
|
processResult: async (result, resolved) => {
|
|
1076
1054
|
// Result-size cap for THIS call: the connector's own override wins,
|
|
1077
1055
|
// then the deployment-wide value, then the built-in default (already
|
|
1078
|
-
// folded into `globalCap`). Resolved per call so one
|
|
1056
|
+
// folded into `globalCap`). Resolved per call so one request can
|
|
1079
1057
|
// mix a tight-capped connector with siblings on the global cap. An
|
|
1080
1058
|
// override the registry already warned about at startup is dropped
|
|
1081
1059
|
// here, so the connector simply inherits `globalCap`.
|
|
@@ -1091,10 +1069,14 @@ export function createMetaTools(
|
|
|
1091
1069
|
resolved.definition.outputSchema,
|
|
1092
1070
|
)
|
|
1093
1071
|
: result;
|
|
1094
|
-
|
|
1072
|
+
const guarded = await guardValue(value, results, cap);
|
|
1073
|
+
value = guarded.result;
|
|
1095
1074
|
return {
|
|
1096
1075
|
toolResult: jsonResult({ ok: true, data: value }),
|
|
1097
1076
|
value,
|
|
1077
|
+
...(guarded.truncated
|
|
1078
|
+
? { friction: "result_too_large" as const }
|
|
1079
|
+
: {}),
|
|
1098
1080
|
};
|
|
1099
1081
|
}
|
|
1100
1082
|
if (resolved.connector.kind === "mcp") {
|
|
@@ -1107,7 +1089,13 @@ export function createMetaTools(
|
|
|
1107
1089
|
resolved.definition.outputSchema,
|
|
1108
1090
|
);
|
|
1109
1091
|
}
|
|
1110
|
-
|
|
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
|
+
};
|
|
1111
1099
|
}
|
|
1112
1100
|
const value = fields
|
|
1113
1101
|
? projectionValue(
|
|
@@ -1116,19 +1104,26 @@ export function createMetaTools(
|
|
|
1116
1104
|
resolved.definition.outputSchema,
|
|
1117
1105
|
)
|
|
1118
1106
|
: result;
|
|
1107
|
+
const guarded = await guardText(
|
|
1108
|
+
serializeResultText(value),
|
|
1109
|
+
results,
|
|
1110
|
+
cap,
|
|
1111
|
+
);
|
|
1119
1112
|
return {
|
|
1120
|
-
toolResult:
|
|
1121
|
-
serializeResultText(value),
|
|
1122
|
-
results,
|
|
1123
|
-
cap,
|
|
1124
|
-
),
|
|
1113
|
+
toolResult: guarded.result,
|
|
1125
1114
|
value,
|
|
1115
|
+
...(guarded.truncated
|
|
1116
|
+
? { friction: "result_too_large" as const }
|
|
1117
|
+
: {}),
|
|
1126
1118
|
};
|
|
1127
1119
|
},
|
|
1120
|
+
activityFriction: (processed) => processed.friction,
|
|
1128
1121
|
},
|
|
1129
1122
|
);
|
|
1130
1123
|
if (!outcome.ok) {
|
|
1124
|
+
const structuredRecovery = outcome.error.nextAction !== undefined;
|
|
1131
1125
|
const failedResult =
|
|
1126
|
+
structuredRecovery ||
|
|
1132
1127
|
outcome.error.code === "auth_required" ||
|
|
1133
1128
|
outcome.error.code === "invalid_args" ||
|
|
1134
1129
|
outcome.error.code === "input_required_unsupported" ||
|
|
@@ -1142,6 +1137,7 @@ export function createMetaTools(
|
|
|
1142
1137
|
})
|
|
1143
1138
|
: errorResult(outcome.error.message);
|
|
1144
1139
|
if (
|
|
1140
|
+
structuredRecovery ||
|
|
1145
1141
|
outcome.error.code === "auth_required" ||
|
|
1146
1142
|
outcome.error.code === "invalid_args" ||
|
|
1147
1143
|
outcome.error.code === "input_required_unsupported"
|
|
@@ -1187,143 +1183,18 @@ export function createMetaTools(
|
|
|
1187
1183
|
type: "text",
|
|
1188
1184
|
text:
|
|
1189
1185
|
'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
|
|
1190
|
-
listSkills(connectors
|
|
1186
|
+
listSkills(connectors)
|
|
1191
1187
|
.map((skill) => `- \`${skill.name}\` — ${skill.description}`)
|
|
1192
1188
|
.join("\n"),
|
|
1193
1189
|
},
|
|
1194
1190
|
],
|
|
1195
1191
|
};
|
|
1196
1192
|
}
|
|
1197
|
-
const skill = resolveSkill(args.name, connectors
|
|
1193
|
+
const skill = resolveSkill(args.name, connectors);
|
|
1198
1194
|
if (!skill.found) return errorResult(skill.message);
|
|
1199
1195
|
return { content: [{ type: "text", text: skill.content }] };
|
|
1200
1196
|
},
|
|
1201
1197
|
|
|
1202
|
-
async listConnectors(args: ListArgs = {}): Promise<ToolResult> {
|
|
1203
|
-
const probe = args.probe ?? true;
|
|
1204
|
-
// Live inventory owns a short-lived scope separate from the request's
|
|
1205
|
-
// call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
|
|
1206
|
-
const connectors = registry.listConnectors();
|
|
1207
|
-
const scope = probe ? {} : requestScope;
|
|
1208
|
-
const inspect = async (c: (typeof connectors)[number]) => {
|
|
1209
|
-
const statusStarted = Date.now();
|
|
1210
|
-
const observed = registry.healthFor(c.id);
|
|
1211
|
-
const drift = await registry.credentialDriftFor(c.id);
|
|
1212
|
-
let status:
|
|
1213
|
-
| ConnectorStatus
|
|
1214
|
-
| { state: "ok" | "error" | "unknown"; message?: string };
|
|
1215
|
-
if (drift) {
|
|
1216
|
-
status = { state: "auth_required", message: drift };
|
|
1217
|
-
} else if (probe) {
|
|
1218
|
-
try {
|
|
1219
|
-
status = await withProbeDeadline(
|
|
1220
|
-
`list_connectors probe of "${c.id}"`,
|
|
1221
|
-
(options) =>
|
|
1222
|
-
registry.statusFor(c.id, baseUrl, scope, options),
|
|
1223
|
-
);
|
|
1224
|
-
} catch (err) {
|
|
1225
|
-
// A probe that outran probeTimeoutMs (or otherwise threw)
|
|
1226
|
-
// degrades this connector to an error status rather than
|
|
1227
|
-
// hanging the whole list_connectors call.
|
|
1228
|
-
status = { state: "error", message: msg(err) };
|
|
1229
|
-
}
|
|
1230
|
-
} else {
|
|
1231
|
-
const derived =
|
|
1232
|
-
observed?.consecutiveFailures && observed.consecutiveFailures > 0
|
|
1233
|
-
? ("error" as const)
|
|
1234
|
-
: registry.hasObservedSuccess(c.id) || c.kind === "api"
|
|
1235
|
-
? ("ok" as const)
|
|
1236
|
-
: ("unknown" as const);
|
|
1237
|
-
status = {
|
|
1238
|
-
state: derived,
|
|
1239
|
-
...(observed?.lastError ? { message: observed.lastError } : {}),
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
// Stamped after any live probe so the response reports when its
|
|
1243
|
-
// observation completed, not when a potentially slow request began.
|
|
1244
|
-
const checkedAt = new Date().toISOString();
|
|
1245
|
-
let tools = registry.peekTools(c.id);
|
|
1246
|
-
// An auth_required status may have just started OAuth. A second
|
|
1247
|
-
// listTools probe would overwrite its state/verifier while returning
|
|
1248
|
-
// the first (now stale) authorization URL.
|
|
1249
|
-
if (probe && status.state === "ok") {
|
|
1250
|
-
try {
|
|
1251
|
-
tools = await withProbeDeadline(
|
|
1252
|
-
`list_connectors catalog refresh of "${c.id}"`,
|
|
1253
|
-
(options) =>
|
|
1254
|
-
registry.refreshTools(c.id, baseUrl, scope, options),
|
|
1255
|
-
);
|
|
1256
|
-
registry.recordSuccess(c.id, Date.now() - statusStarted);
|
|
1257
|
-
} catch (err) {
|
|
1258
|
-
const details = classifyCallError(err);
|
|
1259
|
-
if (details.code === "auth_required") {
|
|
1260
|
-
let authStatus: ConnectorStatus | undefined;
|
|
1261
|
-
try {
|
|
1262
|
-
authStatus = await withProbeDeadline(
|
|
1263
|
-
`list_connectors authorization status of "${c.id}"`,
|
|
1264
|
-
(options) =>
|
|
1265
|
-
registry.statusFor(c.id, baseUrl, scope, options),
|
|
1266
|
-
);
|
|
1267
|
-
} catch {
|
|
1268
|
-
// The typed auth verdict is still authoritative; this second
|
|
1269
|
-
// read exists only to recover the connector's pending URL.
|
|
1270
|
-
}
|
|
1271
|
-
status =
|
|
1272
|
-
authStatus?.state === "auth_required"
|
|
1273
|
-
? authStatus
|
|
1274
|
-
: {
|
|
1275
|
-
state: "auth_required" as const,
|
|
1276
|
-
message: details.message,
|
|
1277
|
-
};
|
|
1278
|
-
} else {
|
|
1279
|
-
status = { state: "error" as const, message: msg(err) };
|
|
1280
|
-
}
|
|
1281
|
-
registry.recordFailure(c.id, Date.now() - statusStarted, err);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
const latencyMs = Date.now() - statusStarted;
|
|
1285
|
-
const latestObserved = registry.healthFor(c.id);
|
|
1286
|
-
return {
|
|
1287
|
-
id: c.id,
|
|
1288
|
-
...(c.title ? { title: c.title } : {}),
|
|
1289
|
-
description: c.description,
|
|
1290
|
-
toolCount: tools?.length ?? 0,
|
|
1291
|
-
status: status.state,
|
|
1292
|
-
checkedAt,
|
|
1293
|
-
latencyMs,
|
|
1294
|
-
probe,
|
|
1295
|
-
...(latestObserved ?? observed),
|
|
1296
|
-
...("authorizationUrl" in status &&
|
|
1297
|
-
status.authorizationUrl && {
|
|
1298
|
-
authorizationUrl: status.authorizationUrl,
|
|
1299
|
-
}),
|
|
1300
|
-
...(status.message && { message: status.message }),
|
|
1301
|
-
};
|
|
1302
|
-
};
|
|
1303
|
-
const settled = await mapSettledWithConcurrency(
|
|
1304
|
-
connectors,
|
|
1305
|
-
discoveryConcurrency,
|
|
1306
|
-
inspect,
|
|
1307
|
-
);
|
|
1308
|
-
if (probe) {
|
|
1309
|
-
await mapSettledWithConcurrency(
|
|
1310
|
-
connectors,
|
|
1311
|
-
discoveryConcurrency,
|
|
1312
|
-
(connector) =>
|
|
1313
|
-
closeConnectorScope(
|
|
1314
|
-
connector,
|
|
1315
|
-
registry.contextFor(connector.id, baseUrl, scope),
|
|
1316
|
-
opts.defer,
|
|
1317
|
-
),
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1320
|
-
const out = settled.map((result) => {
|
|
1321
|
-
if (result.status === "rejected") throw result.reason;
|
|
1322
|
-
return result.value;
|
|
1323
|
-
});
|
|
1324
|
-
return jsonResult({ connectors: out });
|
|
1325
|
-
},
|
|
1326
|
-
|
|
1327
1198
|
async searchTools(args: SearchArgs): Promise<ToolResult> {
|
|
1328
1199
|
return discoveryResult(
|
|
1329
1200
|
async () =>
|
|
@@ -1337,18 +1208,13 @@ export function createMetaTools(
|
|
|
1337
1208
|
);
|
|
1338
1209
|
},
|
|
1339
1210
|
|
|
1340
|
-
async describeTools(args: DescribeArgs): Promise<ToolResult> {
|
|
1341
|
-
return discoveryResult(
|
|
1342
|
-
async () => ({ tools: await catalog.describe(args) }),
|
|
1343
|
-
'Split the address list or use format: "compact".',
|
|
1344
|
-
);
|
|
1345
|
-
},
|
|
1346
|
-
|
|
1347
1211
|
async callTool(args: CallArgs): Promise<ToolResult> {
|
|
1348
1212
|
return (await runCall(args, "call_tool")).toolResult;
|
|
1349
1213
|
},
|
|
1350
1214
|
|
|
1351
|
-
async callDestructiveTool(args:
|
|
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.
|
|
1352
1218
|
return (
|
|
1353
1219
|
await runCall(args, "call_destructive_tool", { allowDestructive: true })
|
|
1354
1220
|
).toolResult;
|
|
@@ -1413,143 +1279,6 @@ export function createMetaTools(
|
|
|
1413
1279
|
});
|
|
1414
1280
|
},
|
|
1415
1281
|
|
|
1416
|
-
async batchCall(args: BatchArgs): Promise<ToolResult> {
|
|
1417
|
-
const batchStarted = Date.now();
|
|
1418
|
-
const settled = await Promise.allSettled(
|
|
1419
|
-
args.calls.map((c) =>
|
|
1420
|
-
runCall(
|
|
1421
|
-
{
|
|
1422
|
-
...c,
|
|
1423
|
-
...((c.resultMode ?? args.resultMode) !== undefined
|
|
1424
|
-
? { resultMode: c.resultMode ?? args.resultMode }
|
|
1425
|
-
: {}),
|
|
1426
|
-
...((c.timeoutMs ?? args.timeoutMs) !== undefined
|
|
1427
|
-
? { timeoutMs: c.timeoutMs ?? args.timeoutMs }
|
|
1428
|
-
: {}),
|
|
1429
|
-
...((c.maxRetries ?? args.maxRetries) !== undefined
|
|
1430
|
-
? { maxRetries: c.maxRetries ?? args.maxRetries }
|
|
1431
|
-
: {}),
|
|
1432
|
-
...((c.diagnostics ?? args.diagnostics) !== undefined
|
|
1433
|
-
? { diagnostics: c.diagnostics ?? args.diagnostics }
|
|
1434
|
-
: {}),
|
|
1435
|
-
},
|
|
1436
|
-
"batch_call",
|
|
1437
|
-
),
|
|
1438
|
-
),
|
|
1439
|
-
);
|
|
1440
|
-
const results = settled.map((s, i) => {
|
|
1441
|
-
const call = args.calls[i];
|
|
1442
|
-
if (!call) {
|
|
1443
|
-
throw new Error("Batch result has no corresponding call");
|
|
1444
|
-
}
|
|
1445
|
-
const { address } = call;
|
|
1446
|
-
if (s.status === "rejected") {
|
|
1447
|
-
return {
|
|
1448
|
-
address,
|
|
1449
|
-
ok: false,
|
|
1450
|
-
error: msg(s.reason),
|
|
1451
|
-
errorDetails: classifyCallError(s.reason, "batch_call_failed"),
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
|
-
const r = s.value;
|
|
1455
|
-
if (r.error) {
|
|
1456
|
-
return {
|
|
1457
|
-
address,
|
|
1458
|
-
ok: false,
|
|
1459
|
-
error: r.error.message,
|
|
1460
|
-
errorDetails: r.error,
|
|
1461
|
-
durationMs: r.durationMs,
|
|
1462
|
-
attempts: r.attempts,
|
|
1463
|
-
...((call.diagnostics ?? args.diagnostics)
|
|
1464
|
-
? { timing: r.timing }
|
|
1465
|
-
: {}),
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
if ((call.resultMode ?? args.resultMode) === "value") {
|
|
1469
|
-
return {
|
|
1470
|
-
address,
|
|
1471
|
-
ok: true,
|
|
1472
|
-
data: r.value,
|
|
1473
|
-
durationMs: r.durationMs,
|
|
1474
|
-
attempts: r.attempts,
|
|
1475
|
-
...((call.diagnostics ?? args.diagnostics)
|
|
1476
|
-
? { timing: r.timing }
|
|
1477
|
-
: {}),
|
|
1478
|
-
};
|
|
1479
|
-
}
|
|
1480
|
-
return {
|
|
1481
|
-
address,
|
|
1482
|
-
ok: true,
|
|
1483
|
-
result: r.toolResult.content,
|
|
1484
|
-
durationMs: r.durationMs,
|
|
1485
|
-
attempts: r.attempts,
|
|
1486
|
-
...((call.diagnostics ?? args.diagnostics)
|
|
1487
|
-
? { timing: r.timing }
|
|
1488
|
-
: {}),
|
|
1489
|
-
};
|
|
1490
|
-
});
|
|
1491
|
-
const envelope = {
|
|
1492
|
-
results,
|
|
1493
|
-
durationMs: Date.now() - batchStarted,
|
|
1494
|
-
};
|
|
1495
|
-
const text = serializeResultText(envelope);
|
|
1496
|
-
const bytes = enc.encode(text);
|
|
1497
|
-
if (bytes.length <= batchCap) return jsonResult(envelope);
|
|
1498
|
-
|
|
1499
|
-
const notice = await stashResult(
|
|
1500
|
-
text,
|
|
1501
|
-
registry.resultsStorage(),
|
|
1502
|
-
bytes.length,
|
|
1503
|
-
);
|
|
1504
|
-
return jsonResult({
|
|
1505
|
-
results: results.map((result) => {
|
|
1506
|
-
const common = {
|
|
1507
|
-
address: batchSummaryString(result.address),
|
|
1508
|
-
ok: !("error" in result),
|
|
1509
|
-
...("durationMs" in result
|
|
1510
|
-
? { durationMs: result.durationMs }
|
|
1511
|
-
: {}),
|
|
1512
|
-
...("attempts" in result ? { attempts: result.attempts } : {}),
|
|
1513
|
-
...("timing" in result ? { timing: result.timing } : {}),
|
|
1514
|
-
};
|
|
1515
|
-
if (!("error" in result)) return common;
|
|
1516
|
-
const error = result.error ?? "Batch call failed";
|
|
1517
|
-
const details =
|
|
1518
|
-
result.errorDetails ??
|
|
1519
|
-
errorDetails("batch_call_failed", error);
|
|
1520
|
-
return {
|
|
1521
|
-
...common,
|
|
1522
|
-
error: batchSummaryString(error),
|
|
1523
|
-
errorDetails: {
|
|
1524
|
-
code: batchSummaryString(details.code),
|
|
1525
|
-
message: batchSummaryString(details.message),
|
|
1526
|
-
retryable: details.retryable,
|
|
1527
|
-
...(details.retryAfterMs !== undefined
|
|
1528
|
-
? { retryAfterMs: details.retryAfterMs }
|
|
1529
|
-
: {}),
|
|
1530
|
-
...(details.connector !== undefined
|
|
1531
|
-
? { connector: batchSummaryString(details.connector) }
|
|
1532
|
-
: {}),
|
|
1533
|
-
...(details.operation !== undefined
|
|
1534
|
-
? { operation: batchSummaryString(details.operation) }
|
|
1535
|
-
: {}),
|
|
1536
|
-
...(details.recovery !== undefined
|
|
1537
|
-
? { recovery: details.recovery }
|
|
1538
|
-
: {}),
|
|
1539
|
-
...(details.nextAction !== undefined
|
|
1540
|
-
? { nextAction: details.nextAction }
|
|
1541
|
-
: {}),
|
|
1542
|
-
...(details.retry !== undefined
|
|
1543
|
-
? { retry: batchSummaryString(details.retry) }
|
|
1544
|
-
: {}),
|
|
1545
|
-
},
|
|
1546
|
-
};
|
|
1547
|
-
}),
|
|
1548
|
-
durationMs: envelope.durationMs,
|
|
1549
|
-
...notice,
|
|
1550
|
-
});
|
|
1551
|
-
},
|
|
1552
|
-
|
|
1553
1282
|
async authorizeConnector(args: AuthorizeArgs): Promise<ToolResult> {
|
|
1554
1283
|
const connector = registry.getConnector(args.connector);
|
|
1555
1284
|
if (!connector) {
|
|
@@ -1626,7 +1355,8 @@ export function createMetaTools(
|
|
|
1626
1355
|
? {
|
|
1627
1356
|
authorizationUrl: status.authorizationUrl,
|
|
1628
1357
|
instructions:
|
|
1629
|
-
"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.
|
|
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),
|
|
1630
1360
|
}
|
|
1631
1361
|
: {}),
|
|
1632
1362
|
...(status.message ? { message: status.message } : {}),
|
|
@@ -1642,39 +1372,19 @@ export function createMetaTools(
|
|
|
1642
1372
|
};
|
|
1643
1373
|
}
|
|
1644
1374
|
|
|
1645
|
-
const LIST_DESC =
|
|
1646
|
-
"List connectors with status, cached tool count, and recent real-call health. Use probe=false for a fast inventory; use probe=true (default) only to diagnose live health or authorization.";
|
|
1647
1375
|
const SEARCH_DESC = `Unknown address: use 2–4 distinctive action/object terms, not the full request; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}) and page only if needed, up to ${MAX_SEARCH_LIMIT}. Partial and no-match searches report term coverage and next-step guidance. safety="readOnly" returns only calls available to call_tool and generated code; "approvalRequired" returns everything else; omitted or "all" preserves the complete catalog. This filters results, not authority. includeSchemas="compact" adds the input and any declared output shape, each bounded; plain-object schemas also expose inputKeys, requiredInputKeys, and outputKeys, while inputSchemaTruncated/outputSchemaTruncated mark shapes that need exact retrieval; matches also carry declared annotations. Call directly when sufficient. Empty query browses all.`;
|
|
1648
|
-
const DESCRIBE_DESC = `Only when search_tools omitted schemas, a compact shape is ambiguous, or exact JSON constraints are needed. Inspects up to ${MAX_DESCRIBE_ADDRESSES} addresses with schemas and annotations; "compact" is default, while "json" preserves exact constraints.`;
|
|
1649
1376
|
const CALL_DESC =
|
|
1650
|
-
'Use for
|
|
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.';
|
|
1651
1378
|
const CALL_DESTRUCTIVE_DESC =
|
|
1652
|
-
"Invoke any tool that is not explicitly annotated readOnlyHint: true, including unannotated, write-capable, or destructive tools. 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.";
|
|
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.";
|
|
1653
1380
|
const GET_RESULT_DESC =
|
|
1654
|
-
"Page a truncated result stashed by call_tool
|
|
1655
|
-
const BATCH_DESC =
|
|
1656
|
-
"Use for 2–10 independent tools explicitly annotated readOnlyHint: true. Calls run in parallel with shared request-scoped clients; use execute_code when available instead for dependencies or in-sandbox reduction. Unannotated, write-capable, and destructive tools are refused. Batch timeout, safe retry, result mode, and diagnostics defaults may be overridden per call. An oversized final envelope returns ordered outcome summaries plus a get_result page handle.";
|
|
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.";
|
|
1657
1382
|
const AUTHORIZE_DESC =
|
|
1658
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.";
|
|
1659
1384
|
const SKILLS_DESC =
|
|
1660
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.';
|
|
1661
1386
|
|
|
1662
|
-
|
|
1663
|
-
* Code-first replacements for the descriptions that route work between tools.
|
|
1664
|
-
* Every one of these mentions a tool the consolidated surface removed, so on a
|
|
1665
|
-
* code-first deployment the routing sentence has to point at the in-program
|
|
1666
|
-
* function that took the work over — a description naming `batch_call` on a
|
|
1667
|
-
* surface without one teaches a call that cannot succeed.
|
|
1668
|
-
*
|
|
1669
|
-
* The classic strings above are left byte-for-byte alone: classic is the
|
|
1670
|
-
* compatibility surface and the eval's control arm, and rewording it would
|
|
1671
|
-
* change what that control measures.
|
|
1672
|
-
*/
|
|
1673
|
-
const CODE_FIRST_SEARCH_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
|
|
1674
|
-
const CODE_FIRST_CALL_DESC =
|
|
1675
|
-
'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.';
|
|
1676
|
-
const CODE_FIRST_GET_RESULT_DESC =
|
|
1677
|
-
"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.";
|
|
1387
|
+
const SEARCH_WITH_DESCRIBE_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
|
|
1678
1388
|
|
|
1679
1389
|
/**
|
|
1680
1390
|
* Sentences appended to a meta-tool description only when this connection
|
|
@@ -1689,8 +1399,6 @@ const GUIDE_NOTES = {
|
|
|
1689
1399
|
' skills({}) also lists this deployment\'s per-connector usage guides as "connector:<connectorId>"; fetch the guide for a connector before working with it for the first time.',
|
|
1690
1400
|
search:
|
|
1691
1401
|
" A connector group carrying `guide` has a usage guide; fetch it with skills({ name: <guide> }).",
|
|
1692
|
-
describe:
|
|
1693
|
-
" An entry carrying `guide` belongs to a connector with a usage guide; fetch it with skills({ name: <guide> }).",
|
|
1694
1402
|
} as const;
|
|
1695
1403
|
|
|
1696
1404
|
/** `base`, plus its guide note when any VISIBLE connector carries a guide. */
|
|
@@ -1735,15 +1443,12 @@ const CALL_INPUT_SCHEMA = {
|
|
|
1735
1443
|
};
|
|
1736
1444
|
|
|
1737
1445
|
/**
|
|
1738
|
-
* Register the
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1742
|
-
*
|
|
1743
|
-
*
|
|
1744
|
-
* `createMetaTools` returns, and a folded tool's behavior is reached through
|
|
1745
|
-
* `connecta.search` / `connecta.describe` / `connecta.batch` inside a program —
|
|
1746
|
-
* the same code paths, one layer down.
|
|
1446
|
+
* Register the six explicit meta-tools onto an McpServer instance.
|
|
1447
|
+
* `registerExecuteTool` adds the seventh, `execute_code`. Broad discovery and
|
|
1448
|
+
* multi-call work is reached through `connecta.search` / `connecta.describe` /
|
|
1449
|
+
* `connecta.batch` inside a program, which `execute_code` builds over the same
|
|
1450
|
+
* `CatalogService` and `InvocationService` these handlers use — one shared
|
|
1451
|
+
* services layer, two adapters above it.
|
|
1747
1452
|
*/
|
|
1748
1453
|
export function registerMetaTools(
|
|
1749
1454
|
server: McpServer,
|
|
@@ -1755,15 +1460,9 @@ export function registerMetaTools(
|
|
|
1755
1460
|
discoveryConcurrency?: number;
|
|
1756
1461
|
activity?: ActivityRequestContext;
|
|
1757
1462
|
requestSignal?: AbortSignal;
|
|
1758
|
-
defer?: DeferredWork;
|
|
1759
|
-
/** The advertised surface. Default `classic`. */
|
|
1760
|
-
surface?: ConnectaSurface;
|
|
1761
1463
|
},
|
|
1762
1464
|
): void {
|
|
1763
|
-
const surface: ConnectaSurface = ctx.surface ?? "classic";
|
|
1764
|
-
const codeFirst = surface === "code-first";
|
|
1765
1465
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1766
|
-
surface,
|
|
1767
1466
|
...(ctx.defaultToolTimeoutMs !== undefined
|
|
1768
1467
|
? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
|
|
1769
1468
|
: {}),
|
|
@@ -1777,7 +1476,6 @@ export function registerMetaTools(
|
|
|
1777
1476
|
...(ctx.requestSignal !== undefined
|
|
1778
1477
|
? { requestSignal: ctx.requestSignal }
|
|
1779
1478
|
: {}),
|
|
1780
|
-
...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
|
|
1781
1479
|
});
|
|
1782
1480
|
|
|
1783
1481
|
server.registerTool(
|
|
@@ -1790,28 +1488,12 @@ export function registerMetaTools(
|
|
|
1790
1488
|
async (args) => mt.skills(args as SkillArgs),
|
|
1791
1489
|
);
|
|
1792
1490
|
|
|
1793
|
-
// Folded on the code-first surface: a program browses the same inventory with
|
|
1794
|
-
// connecta.search({}) (every catalog) or connecta.search({ connector }) (one).
|
|
1795
|
-
// Live connector probing is an operator concern, not a model one — it stays on
|
|
1796
|
-
// the operator pages and /health, which is where the ethos puts observability.
|
|
1797
|
-
if (!codeFirst) {
|
|
1798
|
-
server.registerTool(
|
|
1799
|
-
"list_connectors",
|
|
1800
|
-
{
|
|
1801
|
-
description: LIST_DESC,
|
|
1802
|
-
inputSchema: z.object({ probe: z.boolean().optional() }),
|
|
1803
|
-
annotations: READ_ONLY_REMOTE,
|
|
1804
|
-
},
|
|
1805
|
-
async (args) => mt.listConnectors(args as ListArgs),
|
|
1806
|
-
);
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
1491
|
server.registerTool(
|
|
1810
1492
|
"search_tools",
|
|
1811
1493
|
{
|
|
1812
1494
|
description: describedFor(
|
|
1813
1495
|
registry,
|
|
1814
|
-
|
|
1496
|
+
SEARCH_WITH_DESCRIBE_DESC,
|
|
1815
1497
|
"search",
|
|
1816
1498
|
),
|
|
1817
1499
|
inputSchema: z.object({
|
|
@@ -1830,28 +1512,10 @@ export function registerMetaTools(
|
|
|
1830
1512
|
async (args) => mt.searchTools(args as SearchArgs),
|
|
1831
1513
|
);
|
|
1832
1514
|
|
|
1833
|
-
// Folded on the code-first surface: connecta.describe takes the same
|
|
1834
|
-
// addresses, format, and per-address error reporting inside a program.
|
|
1835
|
-
if (!codeFirst) {
|
|
1836
|
-
server.registerTool(
|
|
1837
|
-
"describe_tools",
|
|
1838
|
-
{
|
|
1839
|
-
description: describedFor(registry, DESCRIBE_DESC, "describe"),
|
|
1840
|
-
inputSchema: z.object({
|
|
1841
|
-
addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
|
|
1842
|
-
format: z.enum(["compact", "json"]).optional(),
|
|
1843
|
-
fullDescriptions: z.boolean().optional(),
|
|
1844
|
-
}),
|
|
1845
|
-
annotations: READ_ONLY_REMOTE,
|
|
1846
|
-
},
|
|
1847
|
-
async (args) => mt.describeTools(args as DescribeArgs),
|
|
1848
|
-
);
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
1515
|
server.registerTool(
|
|
1852
1516
|
"call_tool",
|
|
1853
1517
|
{
|
|
1854
|
-
description:
|
|
1518
|
+
description: CALL_DESC,
|
|
1855
1519
|
inputSchema: z.object(CALL_INPUT_SCHEMA),
|
|
1856
1520
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
1857
1521
|
// anything else is refused and routed to call_destructive_tool.
|
|
@@ -1864,14 +1528,29 @@ export function registerMetaTools(
|
|
|
1864
1528
|
"call_destructive_tool",
|
|
1865
1529
|
{
|
|
1866
1530
|
description: CALL_DESTRUCTIVE_DESC,
|
|
1867
|
-
inputSchema: z.object(
|
|
1531
|
+
inputSchema: z.object({
|
|
1532
|
+
...CALL_INPUT_SCHEMA,
|
|
1533
|
+
// Bounded above, but with no lower bound: a model that sends `""` or
|
|
1534
|
+
// whitespace has written no reason, and failing an entire consequential
|
|
1535
|
+
// call over a cosmetic field the host merely displays is the wrong
|
|
1536
|
+
// trade. It is normalized to absent below instead.
|
|
1537
|
+
reason: z.string().max(500).optional(),
|
|
1538
|
+
}),
|
|
1868
1539
|
annotations: {
|
|
1869
1540
|
destructiveHint: true,
|
|
1870
1541
|
readOnlyHint: false,
|
|
1871
1542
|
openWorldHint: true,
|
|
1872
1543
|
},
|
|
1873
1544
|
},
|
|
1874
|
-
async (args) =>
|
|
1545
|
+
async (args) => {
|
|
1546
|
+
// `reason` is the host's to display and connecta's to keep out of the
|
|
1547
|
+
// downstream call, so this destructuring is the whole of its handling:
|
|
1548
|
+
// nothing below reads it. Dropping it is also what makes an empty or
|
|
1549
|
+
// whitespace-only one "absent" rather than a validation failure — there
|
|
1550
|
+
// is no field left for it to be absent from.
|
|
1551
|
+
const { reason: _hostContext, ...call } = args as DestructiveCallArgs;
|
|
1552
|
+
return mt.callDestructiveTool(call);
|
|
1553
|
+
},
|
|
1875
1554
|
);
|
|
1876
1555
|
|
|
1877
1556
|
server.registerTool(
|
|
@@ -1896,7 +1575,7 @@ export function registerMetaTools(
|
|
|
1896
1575
|
server.registerTool(
|
|
1897
1576
|
"get_result",
|
|
1898
1577
|
{
|
|
1899
|
-
description:
|
|
1578
|
+
description: GET_RESULT_DESC,
|
|
1900
1579
|
inputSchema: z.object({
|
|
1901
1580
|
id: z.string(),
|
|
1902
1581
|
// Both bounds are the shared rules (isValidResultOffset,
|
|
@@ -1910,29 +1589,4 @@ export function registerMetaTools(
|
|
|
1910
1589
|
},
|
|
1911
1590
|
async (args) => mt.getResult(args as GetResultArgs),
|
|
1912
1591
|
);
|
|
1913
|
-
|
|
1914
|
-
// Folded on the code-first surface: connecta.batch runs the same 1–10
|
|
1915
|
-
// parallel read-only calls and returns the same typed per-call outcomes.
|
|
1916
|
-
if (!codeFirst) {
|
|
1917
|
-
server.registerTool(
|
|
1918
|
-
"batch_call",
|
|
1919
|
-
{
|
|
1920
|
-
description: BATCH_DESC,
|
|
1921
|
-
inputSchema: z.object({
|
|
1922
|
-
calls: z
|
|
1923
|
-
.array(z.object(CALL_INPUT_SCHEMA))
|
|
1924
|
-
.min(1)
|
|
1925
|
-
.max(10),
|
|
1926
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1927
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1928
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1929
|
-
diagnostics: z.boolean().optional(),
|
|
1930
|
-
}),
|
|
1931
|
-
// Same gate as call_tool: every call in the batch must be explicitly
|
|
1932
|
-
// read-only or the batch is refused.
|
|
1933
|
-
annotations: READ_ONLY_REMOTE,
|
|
1934
|
-
},
|
|
1935
|
-
async (args) => mt.batchCall(args as BatchArgs),
|
|
1936
|
-
);
|
|
1937
|
-
}
|
|
1938
1592
|
}
|