@zackbart/connecta 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +169 -0
- package/README.md +40 -5
- package/SECURITY.md +10 -6
- package/dist/activity.d.ts +8 -0
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +1 -0
- package/dist/activity.js.map +1 -1
- package/dist/connectors/api.d.ts +13 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +2 -0
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +13 -0
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/execute.d.ts +4 -4
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +22 -4
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +91 -18
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +183 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +293 -27
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +7 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +67 -7
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +52 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +161 -1
- package/dist/skills.js.map +1 -1
- package/dist/toolkits.d.ts +44 -0
- package/dist/toolkits.d.ts.map +1 -0
- package/dist/toolkits.js +134 -0
- package/dist/toolkits.js.map +1 -0
- package/dist/types.d.ts +20 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +28 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +89 -8
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/src/activity.ts +9 -0
- package/src/connectors/api.ts +15 -0
- package/src/connectors/remote-mcp.ts +15 -0
- package/src/execute.ts +4 -4
- package/src/index.ts +69 -1
- package/src/meta-tools.ts +126 -25
- package/src/registry.ts +416 -29
- package/src/server.ts +98 -7
- package/src/skills.ts +184 -1
- package/src/toolkits.ts +215 -0
- package/src/types.ts +20 -1
- package/src/ui.ts +92 -8
- package/src/version.ts +1 -1
package/src/meta-tools.ts
CHANGED
|
@@ -13,8 +13,19 @@ import {
|
|
|
13
13
|
messageLooksRetryable,
|
|
14
14
|
type CallErrorDetails,
|
|
15
15
|
} from "./errors.js";
|
|
16
|
-
import
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
isValidMaxResultBytes,
|
|
18
|
+
MIN_MAX_RESULT_BYTES,
|
|
19
|
+
resolveMaxResultBytes,
|
|
20
|
+
type RegistryView,
|
|
21
|
+
} from "./registry.js";
|
|
22
|
+
import {
|
|
23
|
+
connectorGuide,
|
|
24
|
+
connectorSkillName,
|
|
25
|
+
hasConnectorGuides,
|
|
26
|
+
listSkills,
|
|
27
|
+
resolveSkill,
|
|
28
|
+
} from "./skills.js";
|
|
18
29
|
import type { ConnectorStatus, KVStorage, ToolDef } from "./types.js";
|
|
19
30
|
|
|
20
31
|
interface TextContent {
|
|
@@ -150,19 +161,28 @@ function isContinuationByte(b: number): boolean {
|
|
|
150
161
|
* forward to the end of that codepoint instead so paging always advances.
|
|
151
162
|
* Assumes `offset` is itself a codepoint boundary (offsets are the prior
|
|
152
163
|
* `nextOffset`, which this function guarantees, and 0 is always a boundary).
|
|
164
|
+
*
|
|
165
|
+
* The return is always `> offset` while `offset < total`, whatever `end` is
|
|
166
|
+
* asked for. That is the belt-and-braces half of issue #32: cap validation
|
|
167
|
+
* keeps an empty window from arising in the first place, and this keeps an
|
|
168
|
+
* empty window from turning into a `nextOffset === offset` paging loop if one
|
|
169
|
+
* ever does. Exported for direct testing of that invariant.
|
|
153
170
|
*/
|
|
154
|
-
function alignEndToCharBoundary(
|
|
171
|
+
export function alignEndToCharBoundary(
|
|
155
172
|
bytes: Uint8Array,
|
|
156
173
|
offset: number,
|
|
157
174
|
end: number,
|
|
158
175
|
total: number,
|
|
159
176
|
): number {
|
|
160
177
|
if (end >= total) return total;
|
|
161
|
-
|
|
178
|
+
// A window that reaches no further than `offset` yields no bytes and no
|
|
179
|
+
// progress; widen it to one byte and let the codepoint walk below finish it.
|
|
180
|
+
const wanted = Math.max(end, offset + 1);
|
|
181
|
+
let e = wanted;
|
|
162
182
|
while (e > offset && isContinuationByte(bytes[e])) e--;
|
|
163
183
|
if (e === offset) {
|
|
164
184
|
// Window is narrower than the codepoint at `offset`; take the whole thing.
|
|
165
|
-
e =
|
|
185
|
+
e = wanted;
|
|
166
186
|
while (e < total && isContinuationByte(bytes[e])) e++;
|
|
167
187
|
}
|
|
168
188
|
return e;
|
|
@@ -310,6 +330,7 @@ export interface CallArgs {
|
|
|
310
330
|
export interface GetResultArgs {
|
|
311
331
|
id: string;
|
|
312
332
|
offset?: number;
|
|
333
|
+
/** Page size in bytes; a whole number >= 1. Defaults to the deployment cap. */
|
|
313
334
|
maxBytes?: number;
|
|
314
335
|
}
|
|
315
336
|
export interface BatchCall {
|
|
@@ -339,12 +360,13 @@ export interface SkillArgs {
|
|
|
339
360
|
/**
|
|
340
361
|
* The nine meta-tool handlers over a registry. Exported for direct testing;
|
|
341
362
|
* registerMetaTools() wires them onto an McpServer. `opts.maxResultBytes`
|
|
342
|
-
* overrides the registry's default result-size cap
|
|
363
|
+
* overrides the registry's default result-size cap (a connector's own
|
|
364
|
+
* `maxResultBytes` overrides it in turn); `opts.defaultToolTimeoutMs`
|
|
343
365
|
* supplies a deadline for calls that don't carry one. (execute_code, the
|
|
344
366
|
* optional tenth tool, is registered separately by registerExecuteTool.)
|
|
345
367
|
*/
|
|
346
368
|
export function createMetaTools(
|
|
347
|
-
registry:
|
|
369
|
+
registry: RegistryView,
|
|
348
370
|
baseUrl: string,
|
|
349
371
|
opts: {
|
|
350
372
|
maxResultBytes?: number;
|
|
@@ -355,7 +377,10 @@ export function createMetaTools(
|
|
|
355
377
|
activity?: ActivityRequestContext;
|
|
356
378
|
} = {},
|
|
357
379
|
) {
|
|
358
|
-
const
|
|
380
|
+
const globalCap = resolveMaxResultBytes(
|
|
381
|
+
opts.maxResultBytes,
|
|
382
|
+
registry.maxResultBytes,
|
|
383
|
+
);
|
|
359
384
|
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
|
|
360
385
|
const probeTimeoutMs =
|
|
361
386
|
normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
@@ -442,6 +467,16 @@ export function createMetaTools(
|
|
|
442
467
|
);
|
|
443
468
|
}
|
|
444
469
|
const results = registry.resultsStorage();
|
|
470
|
+
// Result-size cap for THIS call: the connector's own override wins, then
|
|
471
|
+
// the deployment-wide value, then the built-in default (already folded
|
|
472
|
+
// into `globalCap`). Resolved per call so one batch_call can mix a
|
|
473
|
+
// tight-capped connector with siblings on the global cap. An override the
|
|
474
|
+
// registry already warned about at startup is dropped here, so the
|
|
475
|
+
// connector simply inherits `globalCap`.
|
|
476
|
+
const cap = resolveMaxResultBytes(
|
|
477
|
+
resolved.connector.maxResultBytes,
|
|
478
|
+
globalCap,
|
|
479
|
+
);
|
|
445
480
|
const fields = call.fields && call.fields.length > 0 ? call.fields : null;
|
|
446
481
|
// An explicit per-call deadline always wins; the config default only fills
|
|
447
482
|
// the gap, and stays off entirely when the deployment sets none.
|
|
@@ -632,6 +667,7 @@ export function createMetaTools(
|
|
|
632
667
|
|
|
633
668
|
return {
|
|
634
669
|
async skills(args: SkillArgs = {}): Promise<ToolResult> {
|
|
670
|
+
const connectors = registry.listConnectors();
|
|
635
671
|
if (!args.name) {
|
|
636
672
|
return {
|
|
637
673
|
content: [
|
|
@@ -639,19 +675,15 @@ export function createMetaTools(
|
|
|
639
675
|
type: "text",
|
|
640
676
|
text:
|
|
641
677
|
'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
|
|
642
|
-
|
|
643
|
-
(skill) => `- \`${skill.name}\` — ${skill.description}
|
|
644
|
-
|
|
678
|
+
listSkills(connectors)
|
|
679
|
+
.map((skill) => `- \`${skill.name}\` — ${skill.description}`)
|
|
680
|
+
.join("\n"),
|
|
645
681
|
},
|
|
646
682
|
],
|
|
647
683
|
};
|
|
648
684
|
}
|
|
649
|
-
const skill =
|
|
650
|
-
if (!skill)
|
|
651
|
-
return errorResult(
|
|
652
|
-
`Unknown skill "${args.name}". Available: ${AVAILABLE_SKILLS.map((item) => item.name).join(", ")}.`,
|
|
653
|
-
);
|
|
654
|
-
}
|
|
685
|
+
const skill = resolveSkill(args.name, connectors);
|
|
686
|
+
if (!skill.found) return errorResult(skill.message);
|
|
655
687
|
return { content: [{ type: "text", text: skill.content }] };
|
|
656
688
|
},
|
|
657
689
|
|
|
@@ -679,12 +711,17 @@ export function createMetaTools(
|
|
|
679
711
|
status = { state: "error", message: msg(err) };
|
|
680
712
|
}
|
|
681
713
|
} else {
|
|
714
|
+
// "error" comes from THIS view's own observations — a sibling
|
|
715
|
+
// toolkit's failure is not this session's experience — while
|
|
716
|
+
// ok/unknown may lean on the deployment-wide success signal, since
|
|
717
|
+
// "the connector answers at all" is a fact about the connector.
|
|
718
|
+
// Unscoped, the two are the same log, so this is unchanged there.
|
|
682
719
|
status = {
|
|
683
720
|
state:
|
|
684
721
|
observed?.consecutiveFailures &&
|
|
685
722
|
observed.consecutiveFailures > 0
|
|
686
723
|
? ("error" as const)
|
|
687
|
-
:
|
|
724
|
+
: registry.hasObservedSuccess(c.id) || c.kind === "api"
|
|
688
725
|
? ("ok" as const)
|
|
689
726
|
: ("unknown" as const),
|
|
690
727
|
...(observed?.lastError ? { message: observed.lastError } : {}),
|
|
@@ -742,6 +779,7 @@ export function createMetaTools(
|
|
|
742
779
|
connectorId: string;
|
|
743
780
|
connectorTitle?: string;
|
|
744
781
|
connectorDescription?: string;
|
|
782
|
+
connectorGuideSkill?: string;
|
|
745
783
|
tool: ToolDef;
|
|
746
784
|
score: number;
|
|
747
785
|
order: number;
|
|
@@ -764,6 +802,9 @@ export function createMetaTools(
|
|
|
764
802
|
connectorId: c.id,
|
|
765
803
|
connectorTitle: c.title,
|
|
766
804
|
connectorDescription: c.description,
|
|
805
|
+
...(connectorGuide(c)
|
|
806
|
+
? { connectorGuideSkill: connectorSkillName(c.id) }
|
|
807
|
+
: {}),
|
|
767
808
|
tool: ranked.tool,
|
|
768
809
|
score: ranked.score,
|
|
769
810
|
order: orderBase + ranked.order,
|
|
@@ -778,6 +819,8 @@ export function createMetaTools(
|
|
|
778
819
|
id: string;
|
|
779
820
|
title?: string;
|
|
780
821
|
description?: string;
|
|
822
|
+
/** Skill name of this connector's usage guide, when it has one. */
|
|
823
|
+
guide?: string;
|
|
781
824
|
tools: Array<{
|
|
782
825
|
name: string;
|
|
783
826
|
address: string;
|
|
@@ -795,6 +838,9 @@ export function createMetaTools(
|
|
|
795
838
|
id: match.connectorId,
|
|
796
839
|
...(match.connectorTitle ? { title: match.connectorTitle } : {}),
|
|
797
840
|
description: match.connectorDescription,
|
|
841
|
+
...(match.connectorGuideSkill
|
|
842
|
+
? { guide: match.connectorGuideSkill }
|
|
843
|
+
: {}),
|
|
798
844
|
tools: [],
|
|
799
845
|
};
|
|
800
846
|
byConnector.set(match.connectorId, group);
|
|
@@ -899,6 +945,9 @@ export function createMetaTools(
|
|
|
899
945
|
tool.description,
|
|
900
946
|
args.fullDescriptions === true,
|
|
901
947
|
),
|
|
948
|
+
...(connectorGuide(resolved.connector)
|
|
949
|
+
? { guide: connectorSkillName(resolved.connector.id) }
|
|
950
|
+
: {}),
|
|
902
951
|
inputSchema: format === "json" ? schema : compactSchema(schema),
|
|
903
952
|
...(tool.outputSchema
|
|
904
953
|
? {
|
|
@@ -925,6 +974,20 @@ export function createMetaTools(
|
|
|
925
974
|
},
|
|
926
975
|
|
|
927
976
|
async getResult(args: GetResultArgs): Promise<ToolResult> {
|
|
977
|
+
// Client-supplied page size: a normal input-validation error, not a
|
|
978
|
+
// clamp. Callers arriving over MCP are rejected earlier by the
|
|
979
|
+
// registered zod schema and never reach this branch, so it exists for
|
|
980
|
+
// in-process callers of createMetaTools — which have no schema in front
|
|
981
|
+
// of them — and to keep the rule true of the handler on its own terms.
|
|
982
|
+
if (
|
|
983
|
+
args.maxBytes !== undefined &&
|
|
984
|
+
!isValidMaxResultBytes(args.maxBytes)
|
|
985
|
+
) {
|
|
986
|
+
return errorResult(
|
|
987
|
+
`Invalid maxBytes ${args.maxBytes}: must be a whole number of bytes ` +
|
|
988
|
+
`>= ${MIN_MAX_RESULT_BYTES}. Omit it to use the deployment default.`,
|
|
989
|
+
);
|
|
990
|
+
}
|
|
928
991
|
const results = registry.resultsStorage();
|
|
929
992
|
const stored = await results.get(`result:${args.id}`);
|
|
930
993
|
if (stored === null || stored === undefined) {
|
|
@@ -933,7 +996,11 @@ export function createMetaTools(
|
|
|
933
996
|
const bytes = enc.encode(stored);
|
|
934
997
|
const total = bytes.length;
|
|
935
998
|
const offset = Math.max(0, Math.trunc(args.offset ?? 0));
|
|
936
|
-
|
|
999
|
+
// Page size only: a stashed result carries no connector identity, so
|
|
1000
|
+
// get_result keeps the deployment-wide default when none is requested.
|
|
1001
|
+
// Both sides are validated by now — the argument above, `globalCap` at
|
|
1002
|
+
// intake — so `offset + maxBytes` always reaches past `offset`.
|
|
1003
|
+
const maxBytes = args.maxBytes ?? globalCap;
|
|
937
1004
|
// Align the slice end to a codepoint boundary so a multi-byte char is
|
|
938
1005
|
// never split across pages (which would emit U+FFFD on both sides).
|
|
939
1006
|
// `nextOffset` is this aligned end, so it is a valid boundary for the
|
|
@@ -1076,7 +1143,7 @@ const CALL_DESC =
|
|
|
1076
1143
|
const CALL_DESTRUCTIVE_DESC =
|
|
1077
1144
|
"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.";
|
|
1078
1145
|
const GET_RESULT_DESC =
|
|
1079
|
-
"Page a truncated result stashed by call_tool/batch_call. Input { id, offset?, maxBytes? } → { text, offset, nextOffset?, totalBytes } sliced by byte offset. Unknown/expired id is an error.";
|
|
1146
|
+
"Page a truncated result stashed by call_tool/batch_call. 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). Unknown/expired id is an error.";
|
|
1080
1147
|
const BATCH_DESC =
|
|
1081
1148
|
"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.";
|
|
1082
1149
|
const AUTHORIZE_DESC =
|
|
@@ -1084,6 +1151,37 @@ const AUTHORIZE_DESC =
|
|
|
1084
1151
|
const SKILLS_DESC =
|
|
1085
1152
|
'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.';
|
|
1086
1153
|
|
|
1154
|
+
/**
|
|
1155
|
+
* Sentences appended to a meta-tool description only when this connection
|
|
1156
|
+
* actually has connector guides. Tool descriptions are always-loaded context,
|
|
1157
|
+
* so a deployment with no guides gets every base description unchanged rather
|
|
1158
|
+
* than paying for text about a feature it does not use.
|
|
1159
|
+
*
|
|
1160
|
+
* Registration is per connection and reads the connection's own registry view,
|
|
1161
|
+
* so under a toolkit these sentences reflect the SCOPED connector set: a scoped
|
|
1162
|
+
* session whose connectors carry no guides sees the base descriptions, and
|
|
1163
|
+
* never learns from a tool description that guides exist out of scope.
|
|
1164
|
+
*/
|
|
1165
|
+
const GUIDE_NOTES = {
|
|
1166
|
+
skills:
|
|
1167
|
+
' 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.',
|
|
1168
|
+
search:
|
|
1169
|
+
" A connector group carrying `guide` has a usage guide; fetch it with skills({ name: <guide> }).",
|
|
1170
|
+
describe:
|
|
1171
|
+
" An entry carrying `guide` belongs to a connector with a usage guide; fetch it with skills({ name: <guide> }).",
|
|
1172
|
+
} as const;
|
|
1173
|
+
|
|
1174
|
+
/** `base`, plus its guide note when any VISIBLE connector carries a guide. */
|
|
1175
|
+
function describedFor(
|
|
1176
|
+
registry: RegistryView,
|
|
1177
|
+
base: string,
|
|
1178
|
+
note: keyof typeof GUIDE_NOTES,
|
|
1179
|
+
): string {
|
|
1180
|
+
return hasConnectorGuides(registry.listConnectors())
|
|
1181
|
+
? base + GUIDE_NOTES[note]
|
|
1182
|
+
: base;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1087
1185
|
/**
|
|
1088
1186
|
* Connecta refuses downstream tools that are not explicitly annotated
|
|
1089
1187
|
* read-only, so its own meta-tools must carry the same hints — otherwise a
|
|
@@ -1107,7 +1205,7 @@ const READ_ONLY_LOCAL = {
|
|
|
1107
1205
|
/** Register the nine meta-tools onto an McpServer instance. */
|
|
1108
1206
|
export function registerMetaTools(
|
|
1109
1207
|
server: McpServer,
|
|
1110
|
-
registry:
|
|
1208
|
+
registry: RegistryView,
|
|
1111
1209
|
ctx: {
|
|
1112
1210
|
baseUrl: string;
|
|
1113
1211
|
maxResultBytes?: number;
|
|
@@ -1126,7 +1224,7 @@ export function registerMetaTools(
|
|
|
1126
1224
|
server.registerTool(
|
|
1127
1225
|
"skills",
|
|
1128
1226
|
{
|
|
1129
|
-
description: SKILLS_DESC,
|
|
1227
|
+
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
1130
1228
|
inputSchema: { name: z.string().optional() },
|
|
1131
1229
|
annotations: {
|
|
1132
1230
|
readOnlyHint: true,
|
|
@@ -1151,7 +1249,7 @@ export function registerMetaTools(
|
|
|
1151
1249
|
server.registerTool(
|
|
1152
1250
|
"search_tools",
|
|
1153
1251
|
{
|
|
1154
|
-
description: SEARCH_DESC,
|
|
1252
|
+
description: describedFor(registry, SEARCH_DESC, "search"),
|
|
1155
1253
|
inputSchema: {
|
|
1156
1254
|
query: z.string().optional(),
|
|
1157
1255
|
connector: z.string().optional(),
|
|
@@ -1168,7 +1266,7 @@ export function registerMetaTools(
|
|
|
1168
1266
|
server.registerTool(
|
|
1169
1267
|
"describe_tools",
|
|
1170
1268
|
{
|
|
1171
|
-
description: DESCRIBE_DESC,
|
|
1269
|
+
description: describedFor(registry, DESCRIBE_DESC, "describe"),
|
|
1172
1270
|
inputSchema: {
|
|
1173
1271
|
addresses: z.array(z.string()),
|
|
1174
1272
|
format: z.enum(["compact", "json"]).optional(),
|
|
@@ -1247,7 +1345,10 @@ export function registerMetaTools(
|
|
|
1247
1345
|
inputSchema: {
|
|
1248
1346
|
id: z.string(),
|
|
1249
1347
|
offset: z.number().int().nonnegative().optional(),
|
|
1250
|
-
|
|
1348
|
+
// Same rule as isValidMaxResultBytes, expressed for the wire: sharing
|
|
1349
|
+
// the floor constant keeps the schema from drifting away from the
|
|
1350
|
+
// in-handler check if MIN_MAX_RESULT_BYTES ever moves.
|
|
1351
|
+
maxBytes: z.number().int().min(MIN_MAX_RESULT_BYTES).optional(),
|
|
1251
1352
|
},
|
|
1252
1353
|
annotations: READ_ONLY_LOCAL,
|
|
1253
1354
|
},
|