@zackbart/connecta 0.1.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +249 -0
  4. package/SECURITY.md +25 -0
  5. package/assets/connecta-clay-hero.png +0 -0
  6. package/dist/activity.d.ts +82 -0
  7. package/dist/activity.d.ts.map +1 -0
  8. package/dist/activity.js +51 -0
  9. package/dist/activity.js.map +1 -0
  10. package/dist/auth/bearer.d.ts +10 -0
  11. package/dist/auth/bearer.d.ts.map +1 -0
  12. package/dist/auth/bearer.js +47 -0
  13. package/dist/auth/bearer.js.map +1 -0
  14. package/dist/auth/clerk.d.ts +20 -0
  15. package/dist/auth/clerk.d.ts.map +1 -0
  16. package/dist/auth/clerk.js +167 -0
  17. package/dist/auth/clerk.js.map +1 -0
  18. package/dist/auth/downstream-oauth.d.ts +78 -0
  19. package/dist/auth/downstream-oauth.d.ts.map +1 -0
  20. package/dist/auth/downstream-oauth.js +180 -0
  21. package/dist/auth/downstream-oauth.js.map +1 -0
  22. package/dist/catalog.d.ts +11 -0
  23. package/dist/catalog.d.ts.map +1 -0
  24. package/dist/catalog.js +144 -0
  25. package/dist/catalog.js.map +1 -0
  26. package/dist/connectors/api.d.ts +34 -0
  27. package/dist/connectors/api.d.ts.map +1 -0
  28. package/dist/connectors/api.js +36 -0
  29. package/dist/connectors/api.js.map +1 -0
  30. package/dist/connectors/remote-mcp.d.ts +33 -0
  31. package/dist/connectors/remote-mcp.d.ts.map +1 -0
  32. package/dist/connectors/remote-mcp.js +255 -0
  33. package/dist/connectors/remote-mcp.js.map +1 -0
  34. package/dist/credentials.d.ts +33 -0
  35. package/dist/credentials.d.ts.map +1 -0
  36. package/dist/credentials.js +177 -0
  37. package/dist/credentials.js.map +1 -0
  38. package/dist/execute.d.ts +40 -0
  39. package/dist/execute.d.ts.map +1 -0
  40. package/dist/execute.js +424 -0
  41. package/dist/execute.js.map +1 -0
  42. package/dist/executors/quickjs.d.ts +26 -0
  43. package/dist/executors/quickjs.d.ts.map +1 -0
  44. package/dist/executors/quickjs.js +267 -0
  45. package/dist/executors/quickjs.js.map +1 -0
  46. package/dist/favicon.d.ts +2 -0
  47. package/dist/favicon.d.ts.map +1 -0
  48. package/dist/favicon.js +49 -0
  49. package/dist/favicon.js.map +1 -0
  50. package/dist/index.d.ts +85 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +73 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/mcp-result.d.ts +9 -0
  55. package/dist/mcp-result.d.ts.map +1 -0
  56. package/dist/mcp-result.js +35 -0
  57. package/dist/mcp-result.js.map +1 -0
  58. package/dist/meta-tools.d.ts +100 -0
  59. package/dist/meta-tools.d.ts.map +1 -0
  60. package/dist/meta-tools.js +797 -0
  61. package/dist/meta-tools.js.map +1 -0
  62. package/dist/node.d.ts +9 -0
  63. package/dist/node.d.ts.map +1 -0
  64. package/dist/node.js +57 -0
  65. package/dist/node.js.map +1 -0
  66. package/dist/registry.d.ts +78 -0
  67. package/dist/registry.d.ts.map +1 -0
  68. package/dist/registry.js +299 -0
  69. package/dist/registry.js.map +1 -0
  70. package/dist/server.d.ts +31 -0
  71. package/dist/server.d.ts.map +1 -0
  72. package/dist/server.js +543 -0
  73. package/dist/server.js.map +1 -0
  74. package/dist/skills.d.ts +8 -0
  75. package/dist/skills.d.ts.map +1 -0
  76. package/dist/skills.js +60 -0
  77. package/dist/skills.js.map +1 -0
  78. package/dist/storage/file.d.ts +8 -0
  79. package/dist/storage/file.d.ts.map +1 -0
  80. package/dist/storage/file.js +53 -0
  81. package/dist/storage/file.js.map +1 -0
  82. package/dist/storage/memory.d.ts +4 -0
  83. package/dist/storage/memory.d.ts.map +1 -0
  84. package/dist/storage/memory.js +29 -0
  85. package/dist/storage/memory.js.map +1 -0
  86. package/dist/types.d.ts +219 -0
  87. package/dist/types.d.ts.map +1 -0
  88. package/dist/types.js +3 -0
  89. package/dist/types.js.map +1 -0
  90. package/dist/ui.d.ts +91 -0
  91. package/dist/ui.d.ts.map +1 -0
  92. package/dist/ui.js +1031 -0
  93. package/dist/ui.js.map +1 -0
  94. package/package.json +96 -0
  95. package/src/activity.ts +144 -0
  96. package/src/auth/bearer.ts +54 -0
  97. package/src/auth/clerk.ts +226 -0
  98. package/src/auth/downstream-oauth.ts +202 -0
  99. package/src/catalog.ts +166 -0
  100. package/src/connectors/api.ts +80 -0
  101. package/src/connectors/remote-mcp.ts +324 -0
  102. package/src/credentials.ts +259 -0
  103. package/src/execute.ts +550 -0
  104. package/src/executors/quickjs.ts +321 -0
  105. package/src/favicon.ts +53 -0
  106. package/src/index.ts +197 -0
  107. package/src/mcp-result.ts +43 -0
  108. package/src/meta-tools.ts +1120 -0
  109. package/src/node.ts +62 -0
  110. package/src/registry.ts +400 -0
  111. package/src/server.ts +758 -0
  112. package/src/skills.ts +63 -0
  113. package/src/storage/file.ts +62 -0
  114. package/src/storage/memory.ts +34 -0
  115. package/src/types.ts +254 -0
  116. package/src/ui.ts +1121 -0
@@ -0,0 +1,1120 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
4
+ import {
5
+ recordToolActivity,
6
+ type ActivityCallSource,
7
+ type ActivityRequestContext,
8
+ } from "./activity.js";
9
+ import { unwrapMcpResult } from "./mcp-result.js";
10
+ import type { Registry } from "./registry.js";
11
+ import { AVAILABLE_SKILLS } from "./skills.js";
12
+ import type { KVStorage, ToolDef } from "./types.js";
13
+
14
+ interface TextContent {
15
+ type: "text";
16
+ text: string;
17
+ }
18
+ export interface ToolResult {
19
+ content: TextContent[];
20
+ isError?: boolean;
21
+ structuredContent?: Record<string, unknown>;
22
+ [x: string]: unknown;
23
+ }
24
+
25
+ const RESULT_TTL_SECONDS = 900;
26
+
27
+ export function jsonResult(obj: unknown): ToolResult {
28
+ return {
29
+ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
30
+ ...(obj !== null && typeof obj === "object" && !Array.isArray(obj)
31
+ ? { structuredContent: obj as Record<string, unknown> }
32
+ : {}),
33
+ };
34
+ }
35
+
36
+ export function errorResult(message: string): ToolResult {
37
+ return { content: [{ type: "text", text: message }], isError: true };
38
+ }
39
+
40
+ function msg(err: unknown): string {
41
+ return err instanceof Error ? err.message : String(err);
42
+ }
43
+
44
+ const DEFAULT_SEARCH_LIMIT = 25;
45
+ const enc = new TextEncoder();
46
+ const dec = new TextDecoder();
47
+
48
+ interface ErrorDetails {
49
+ code: string;
50
+ message: string;
51
+ retryable: boolean;
52
+ }
53
+
54
+ function errorDetails(code: string, message: string): ErrorDetails {
55
+ return {
56
+ code,
57
+ message,
58
+ retryable:
59
+ /timeout|timed out|econnreset|econnrefused|temporar|rate.?limit|429|502|503|504|refcountedcanceler|different request/i.test(
60
+ message,
61
+ ),
62
+ };
63
+ }
64
+
65
+ /** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
66
+ function isContinuationByte(b: number): boolean {
67
+ return (b & 0xc0) === 0x80;
68
+ }
69
+
70
+ /**
71
+ * Move a byte `end` back to the nearest UTF-8 codepoint boundary in
72
+ * `(offset, total]`, so decoding `bytes[offset, end)` never splits a codepoint
73
+ * (which would emit U+FFFD and break byte-exact reassembly). If backing up
74
+ * would make no progress — a single codepoint wider than the window — extend
75
+ * forward to the end of that codepoint instead so paging always advances.
76
+ * Assumes `offset` is itself a codepoint boundary (offsets are the prior
77
+ * `nextOffset`, which this function guarantees, and 0 is always a boundary).
78
+ */
79
+ function alignEndToCharBoundary(
80
+ bytes: Uint8Array,
81
+ offset: number,
82
+ end: number,
83
+ total: number,
84
+ ): number {
85
+ if (end >= total) return total;
86
+ let e = end;
87
+ while (e > offset && isContinuationByte(bytes[e])) e--;
88
+ if (e === offset) {
89
+ // Window is narrower than the codepoint at `offset`; take the whole thing.
90
+ e = end;
91
+ while (e < total && isContinuationByte(bytes[e])) e++;
92
+ }
93
+ return e;
94
+ }
95
+
96
+ // --- fields selection (feature 2) -----------------------------------------
97
+
98
+ /** Resolve a dot-path (segments) against a value; `key[]` maps the tail over an array. */
99
+ function resolvePath(value: unknown, segments: string[]): unknown {
100
+ if (segments.length === 0) return value;
101
+ const [seg, ...rest] = segments;
102
+ const isArr = seg.endsWith("[]");
103
+ const key = isArr ? seg.slice(0, -2) : seg;
104
+ let next: unknown = value;
105
+ if (key !== "") {
106
+ if (value === null || typeof value !== "object") return undefined;
107
+ next = (value as Record<string, unknown>)[key];
108
+ }
109
+ if (isArr) {
110
+ if (!Array.isArray(next)) return undefined;
111
+ return next.map((el) => resolvePath(el, rest));
112
+ }
113
+ return resolvePath(next, rest);
114
+ }
115
+
116
+ /** Select the given dot-paths from a value → `{ "<path>": value }` (omitting misses). */
117
+ function applyFields(
118
+ value: unknown,
119
+ fields: string[],
120
+ ): Record<string, unknown> {
121
+ const out: Record<string, unknown> = {};
122
+ for (const path of fields) {
123
+ const resolved = resolvePath(value, path.split("."));
124
+ if (resolved !== undefined) out[path] = resolved;
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /** Apply fields to each JSON-parseable text block; non-JSON blocks pass through. */
130
+ function applyFieldsToContent(
131
+ content: TextContent[],
132
+ fields: string[],
133
+ ): TextContent[] {
134
+ return content.map((b) => {
135
+ if (b.type !== "text") return b;
136
+ let parsed: unknown;
137
+ try {
138
+ parsed = JSON.parse(b.text);
139
+ } catch {
140
+ return b;
141
+ }
142
+ return { ...b, text: JSON.stringify(applyFields(parsed, fields), null, 2) };
143
+ });
144
+ }
145
+
146
+ // --- result-size guard + get_result (feature 1) ---------------------------
147
+
148
+ function contentBytes(content: TextContent[]): number {
149
+ let n = 0;
150
+ for (const b of content)
151
+ if (b.type === "text") n += enc.encode(b.text).length;
152
+ return n;
153
+ }
154
+
155
+ /**
156
+ * Return `text` as a single content block; if it exceeds `cap` bytes, stash the
157
+ * full text under `result:<uuid>` (ttl 900s) and return the first `cap` bytes
158
+ * followed by a JSON truncation notice pointing at get_result.
159
+ */
160
+ async function guardText(
161
+ text: string,
162
+ results: KVStorage,
163
+ cap: number,
164
+ ): Promise<ToolResult> {
165
+ const bytes = enc.encode(text);
166
+ if (bytes.length <= cap) {
167
+ return { content: [{ type: "text", text }] };
168
+ }
169
+ const id = crypto.randomUUID();
170
+ await results.set(`result:${id}`, text, { ttlSeconds: RESULT_TTL_SECONDS });
171
+ const head = dec.decode(
172
+ bytes.slice(0, alignEndToCharBoundary(bytes, 0, cap, bytes.length)),
173
+ );
174
+ const notice = JSON.stringify({
175
+ truncated: true,
176
+ resultId: id,
177
+ totalBytes: bytes.length,
178
+ hint: "use get_result {id, offset} to page, or re-call with fields to select less",
179
+ });
180
+ return { content: [{ type: "text", text: `${head}\n${notice}` }] };
181
+ }
182
+
183
+ /** Store an oversized JSON value and replace it with a page handle. */
184
+ async function guardValue(
185
+ value: unknown,
186
+ results: KVStorage,
187
+ cap: number,
188
+ ): Promise<unknown> {
189
+ const text = JSON.stringify(value, null, 2) ?? String(value);
190
+ const bytes = enc.encode(text);
191
+ if (bytes.length <= cap) return value;
192
+ const id = crypto.randomUUID();
193
+ await results.set(`result:${id}`, text, { ttlSeconds: RESULT_TTL_SECONDS });
194
+ return {
195
+ truncated: true,
196
+ resultId: id,
197
+ totalBytes: bytes.length,
198
+ hint: "use get_result {id, offset} to page, or re-call with fields to select less",
199
+ };
200
+ }
201
+
202
+ // --- compact schema rendering (feature 3a) --------------------------------
203
+
204
+ // --- argument shapes -------------------------------------------------------
205
+
206
+ export interface SearchArgs {
207
+ query?: string;
208
+ connector?: string;
209
+ limit?: number;
210
+ offset?: number;
211
+ fullDescriptions?: boolean;
212
+ includeSchemas?: "compact" | "json";
213
+ }
214
+ export interface DescribeArgs {
215
+ addresses: string[];
216
+ format?: "compact" | "json";
217
+ fullDescriptions?: boolean;
218
+ }
219
+ export interface ListArgs {
220
+ /** When false, return cached/observed health without downstream I/O. */
221
+ probe?: boolean;
222
+ }
223
+ export type ResultMode = "mcp" | "value";
224
+ export interface CallArgs {
225
+ address: string;
226
+ args?: Record<string, unknown>;
227
+ fields?: string[];
228
+ resultMode?: ResultMode;
229
+ timeoutMs?: number;
230
+ /** Retries after the first attempt; honored only for safely annotated tools. */
231
+ maxRetries?: number;
232
+ /** Include connector/catalog/result-processing timing segments. */
233
+ diagnostics?: boolean;
234
+ }
235
+ export interface GetResultArgs {
236
+ id: string;
237
+ offset?: number;
238
+ maxBytes?: number;
239
+ }
240
+ export interface BatchCall {
241
+ address: string;
242
+ args?: Record<string, unknown>;
243
+ fields?: string[];
244
+ resultMode?: ResultMode;
245
+ timeoutMs?: number;
246
+ maxRetries?: number;
247
+ diagnostics?: boolean;
248
+ }
249
+ export interface BatchArgs {
250
+ calls: BatchCall[];
251
+ resultMode?: ResultMode;
252
+ timeoutMs?: number;
253
+ maxRetries?: number;
254
+ diagnostics?: boolean;
255
+ }
256
+ export interface AuthorizeArgs {
257
+ connector: string;
258
+ force?: boolean;
259
+ }
260
+ export interface SkillArgs {
261
+ name?: string;
262
+ }
263
+
264
+ /**
265
+ * The nine meta-tool handlers over a registry. Exported for direct testing;
266
+ * registerMetaTools() wires them onto an McpServer. `opts.maxResultBytes`
267
+ * overrides the registry's default result-size cap. (execute_code, the optional
268
+ * tenth tool, is registered separately by registerExecuteTool.)
269
+ */
270
+ export function createMetaTools(
271
+ registry: Registry,
272
+ baseUrl: string,
273
+ opts: {
274
+ maxResultBytes?: number;
275
+ activity?: ActivityRequestContext;
276
+ } = {},
277
+ ) {
278
+ const cap = opts.maxResultBytes ?? registry.maxResultBytes;
279
+ // createMetaTools() is called once per inbound MCP request. Sharing this
280
+ // identity lets remote connectors reuse one downstream client inside that
281
+ // request without leaking request-bound I/O into the next one.
282
+ const requestScope = {};
283
+
284
+ interface RunCallOutcome {
285
+ toolResult: ToolResult;
286
+ durationMs: number;
287
+ attempts: number;
288
+ timing: {
289
+ catalogMs: number;
290
+ connectorMs: number;
291
+ backoffMs: number;
292
+ resultProcessingMs: number;
293
+ totalMs: number;
294
+ };
295
+ value?: unknown;
296
+ error?: ErrorDetails;
297
+ }
298
+
299
+ /** Shared call path used by call tools and batch_call: safety → fields → size guard. */
300
+ async function runCall(
301
+ call: BatchCall,
302
+ source: ActivityCallSource,
303
+ options: { allowDestructive?: boolean } = {},
304
+ ): Promise<RunCallOutcome> {
305
+ const started = Date.now();
306
+ let catalogMs = 0;
307
+ let connectorMs = 0;
308
+ let backoffMs = 0;
309
+ let resultProcessingMs = 0;
310
+ let attempts = 0;
311
+ const timing = () => ({
312
+ catalogMs,
313
+ connectorMs,
314
+ backoffMs,
315
+ resultProcessingMs,
316
+ totalMs: Date.now() - started,
317
+ });
318
+ const resolved = registry.resolveAddress(call.address);
319
+ const record = (
320
+ outcome: "success" | "error" | "timeout",
321
+ errorCode?: string,
322
+ ) => {
323
+ if (!resolved) return;
324
+ recordToolActivity(opts.activity, {
325
+ connectorId: resolved.connector.id,
326
+ toolName: resolved.toolName,
327
+ address: `${resolved.connector.id}.${resolved.toolName}`,
328
+ source,
329
+ outcome,
330
+ durationMs: Date.now() - started,
331
+ attempts,
332
+ ...(errorCode ? { errorCode } : {}),
333
+ });
334
+ };
335
+ const failed = (code: string, message: string): RunCallOutcome => {
336
+ const durationMs = Date.now() - started;
337
+ const error = errorDetails(code, message);
338
+ const diagnostics = timing();
339
+ record(code === "timeout" ? "timeout" : "error", code);
340
+ return {
341
+ toolResult:
342
+ call.resultMode === "value"
343
+ ? jsonResult({
344
+ ok: false,
345
+ error,
346
+ durationMs,
347
+ attempts,
348
+ ...(call.diagnostics ? { timing: diagnostics } : {}),
349
+ })
350
+ : errorResult(message),
351
+ durationMs,
352
+ attempts,
353
+ timing: diagnostics,
354
+ error,
355
+ };
356
+ };
357
+ if (!resolved) {
358
+ return failed("unknown_address", `Unknown address "${call.address}"`);
359
+ }
360
+ const results = registry.resultsStorage();
361
+ const fields = call.fields && call.fields.length > 0 ? call.fields : null;
362
+ const timeoutMs =
363
+ call.timeoutMs && call.timeoutMs > 0
364
+ ? Math.max(1, Math.trunc(call.timeoutMs))
365
+ : undefined;
366
+ const maxRetries = Math.min(
367
+ 2,
368
+ Math.max(0, Math.trunc(call.maxRetries ?? 0)),
369
+ );
370
+ const catalogStarted = Date.now();
371
+ let definition: ToolDef | undefined;
372
+ try {
373
+ definition = (
374
+ await registry.getTools(resolved.connector.id, baseUrl, requestScope)
375
+ ).find((tool) => tool.name === resolved.toolName);
376
+ } catch (err) {
377
+ catalogMs += Date.now() - catalogStarted;
378
+ return failed("catalog_lookup_failed", msg(err));
379
+ }
380
+ catalogMs += Date.now() - catalogStarted;
381
+ if (!definition) {
382
+ return failed(
383
+ "unknown_tool",
384
+ `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
385
+ );
386
+ }
387
+ const explicitlyReadOnly =
388
+ definition.annotations?.readOnlyHint === true &&
389
+ definition.annotations?.destructiveHint !== true;
390
+ if (!explicitlyReadOnly && !options.allowDestructive) {
391
+ return failed(
392
+ "destructive_tool_requires_approval",
393
+ `Tool "${call.address}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`,
394
+ );
395
+ }
396
+ const retrySafe =
397
+ definition.annotations?.readOnlyHint === true ||
398
+ definition.annotations?.idempotentHint === true;
399
+
400
+ let result: unknown;
401
+ while (true) {
402
+ attempts++;
403
+ const controller = timeoutMs ? new AbortController() : undefined;
404
+ let timer: ReturnType<typeof setTimeout> | undefined;
405
+ const ctx = registry.contextFor(
406
+ resolved.connector.id,
407
+ baseUrl,
408
+ requestScope,
409
+ { signal: controller?.signal, timeoutMs },
410
+ );
411
+ const connectorStarted = Date.now();
412
+ try {
413
+ const pending = resolved.connector.callTool(
414
+ resolved.toolName,
415
+ call.args ?? {},
416
+ ctx,
417
+ );
418
+ result = timeoutMs
419
+ ? await Promise.race([
420
+ pending,
421
+ new Promise<never>((_, reject) => {
422
+ timer = setTimeout(() => {
423
+ reject(new Error(`Tool call timed out after ${timeoutMs}ms`));
424
+ controller?.abort();
425
+ }, timeoutMs);
426
+ }),
427
+ ])
428
+ : await pending;
429
+ connectorMs += Date.now() - connectorStarted;
430
+ const mcpResult = result as {
431
+ content?: TextContent[];
432
+ isError?: boolean;
433
+ };
434
+ if (resolved.connector.kind === "mcp" && mcpResult?.isError) {
435
+ throw new Error(
436
+ mcpResult.content?.map((block) => block.text).join("") ||
437
+ "Downstream tool call failed",
438
+ );
439
+ }
440
+ break;
441
+ } catch (err) {
442
+ // Includes connector setup, downstream execution, and timeout wait.
443
+ connectorMs += Date.now() - connectorStarted;
444
+ const details = errorDetails("connector_call_failed", msg(err));
445
+ if (attempts <= maxRetries && retrySafe && details.retryable) {
446
+ const backoffStarted = Date.now();
447
+ await new Promise((resolve) =>
448
+ setTimeout(resolve, Math.min(250 * 2 ** (attempts - 1), 1_000)),
449
+ );
450
+ backoffMs += Date.now() - backoffStarted;
451
+ continue;
452
+ }
453
+ registry.recordFailure(
454
+ resolved.connector.id,
455
+ Date.now() - started,
456
+ err,
457
+ );
458
+ return failed(
459
+ /timed out|timeout/i.test(msg(err))
460
+ ? "timeout"
461
+ : "connector_call_failed",
462
+ msg(err),
463
+ );
464
+ } finally {
465
+ if (timer) clearTimeout(timer);
466
+ }
467
+ }
468
+
469
+ registry.recordSuccess(resolved.connector.id, Date.now() - started);
470
+ const processingStarted = Date.now();
471
+ try {
472
+ const mr = result as { content?: TextContent[]; isError?: boolean };
473
+ if (call.resultMode === "value") {
474
+ let value = unwrapMcpResult(resolved.connector.kind, result);
475
+ if (fields) value = applyFields(value, fields);
476
+ value = await guardValue(value, results, cap);
477
+ resultProcessingMs += Date.now() - processingStarted;
478
+ const durationMs = Date.now() - started;
479
+ const diagnostics = timing();
480
+ record("success");
481
+ return {
482
+ toolResult: jsonResult({
483
+ ok: true,
484
+ data: value,
485
+ durationMs,
486
+ attempts,
487
+ ...(call.diagnostics ? { timing: diagnostics } : {}),
488
+ }),
489
+ durationMs,
490
+ attempts,
491
+ timing: diagnostics,
492
+ value,
493
+ };
494
+ }
495
+ if (resolved.connector.kind === "mcp") {
496
+ let content = mr?.content ?? [];
497
+ if (fields) content = applyFieldsToContent(content, fields);
498
+ let toolResult: ToolResult;
499
+ if (contentBytes(content) > cap) {
500
+ toolResult = await guardText(
501
+ JSON.stringify(content, null, 2),
502
+ results,
503
+ cap,
504
+ );
505
+ } else {
506
+ toolResult = { content };
507
+ }
508
+ resultProcessingMs += Date.now() - processingStarted;
509
+ record("success");
510
+ return {
511
+ toolResult,
512
+ durationMs: Date.now() - started,
513
+ attempts,
514
+ timing: timing(),
515
+ };
516
+ }
517
+ const value = fields ? applyFields(result, fields) : result;
518
+ const toolResult = await guardText(
519
+ JSON.stringify(value, null, 2),
520
+ results,
521
+ cap,
522
+ );
523
+ resultProcessingMs += Date.now() - processingStarted;
524
+ record("success");
525
+ return {
526
+ toolResult,
527
+ durationMs: Date.now() - started,
528
+ attempts,
529
+ timing: timing(),
530
+ value,
531
+ };
532
+ } catch (err) {
533
+ resultProcessingMs += Date.now() - processingStarted;
534
+ return failed("result_processing_failed", msg(err));
535
+ }
536
+ }
537
+
538
+ return {
539
+ async skills(args: SkillArgs = {}): Promise<ToolResult> {
540
+ if (!args.name) {
541
+ return {
542
+ content: [
543
+ {
544
+ type: "text",
545
+ text:
546
+ 'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
547
+ AVAILABLE_SKILLS.map(
548
+ (skill) => `- \`${skill.name}\` — ${skill.description}`,
549
+ ).join("\n"),
550
+ },
551
+ ],
552
+ };
553
+ }
554
+ const skill = AVAILABLE_SKILLS.find((item) => item.name === args.name);
555
+ if (!skill) {
556
+ return errorResult(
557
+ `Unknown skill "${args.name}". Available: ${AVAILABLE_SKILLS.map((item) => item.name).join(", ")}.`,
558
+ );
559
+ }
560
+ return { content: [{ type: "text", text: skill.content }] };
561
+ },
562
+
563
+ async listConnectors(args: ListArgs = {}): Promise<ToolResult> {
564
+ const probe = args.probe ?? true;
565
+ const out = await Promise.all(
566
+ registry.listConnectors().map(async (c) => {
567
+ const checkedAt = new Date().toISOString();
568
+ const statusStarted = Date.now();
569
+ const observed = registry.healthFor(c.id);
570
+ let status = probe
571
+ ? await registry.statusFor(c.id, baseUrl, requestScope)
572
+ : {
573
+ state:
574
+ observed?.consecutiveFailures &&
575
+ observed.consecutiveFailures > 0
576
+ ? ("error" as const)
577
+ : observed?.lastSuccessAt || c.kind === "api"
578
+ ? ("ok" as const)
579
+ : ("unknown" as const),
580
+ ...(observed?.lastError ? { message: observed.lastError } : {}),
581
+ };
582
+ let tools = registry.peekTools(c.id);
583
+ // An auth_required status may have just started OAuth. A second
584
+ // listTools probe would overwrite its state/verifier while returning
585
+ // the first (now stale) authorization URL.
586
+ if (probe && status.state === "ok") {
587
+ try {
588
+ tools = await registry.refreshTools(c.id, baseUrl, requestScope);
589
+ registry.recordSuccess(c.id, Date.now() - statusStarted);
590
+ } catch (err) {
591
+ status = { state: "error" as const, message: msg(err) };
592
+ registry.recordFailure(c.id, Date.now() - statusStarted, err);
593
+ }
594
+ }
595
+ const latencyMs = Date.now() - statusStarted;
596
+ const latestObserved = registry.healthFor(c.id);
597
+ return {
598
+ id: c.id,
599
+ ...(c.title ? { title: c.title } : {}),
600
+ description: c.description,
601
+ toolCount: tools?.length ?? 0,
602
+ status: status.state,
603
+ checkedAt,
604
+ latencyMs,
605
+ probe,
606
+ ...(latestObserved ?? observed ?? {}),
607
+ ...("authorizationUrl" in status && status.authorizationUrl
608
+ ? { authorizationUrl: status.authorizationUrl }
609
+ : {}),
610
+ ...(status.message ? { message: status.message } : {}),
611
+ };
612
+ }),
613
+ );
614
+ return jsonResult({ connectors: out });
615
+ },
616
+
617
+ async searchTools(args: SearchArgs): Promise<ToolResult> {
618
+ const q = args.query ?? "";
619
+ const limit = Math.max(1, Math.trunc(args.limit ?? DEFAULT_SEARCH_LIMIT));
620
+ const offset = Math.max(0, Math.trunc(args.offset ?? 0));
621
+ const conns = args.connector
622
+ ? [registry.getConnector(args.connector)].filter(
623
+ (c): c is NonNullable<typeof c> => Boolean(c),
624
+ )
625
+ : registry.listConnectors();
626
+ const matches: Array<{
627
+ connectorId: string;
628
+ connectorTitle?: string;
629
+ connectorDescription?: string;
630
+ tool: ToolDef;
631
+ score: number;
632
+ order: number;
633
+ }> = [];
634
+ const catalogs = await Promise.allSettled(
635
+ conns.map((c) => registry.getTools(c.id, baseUrl, requestScope)),
636
+ );
637
+ let orderBase = 0;
638
+ catalogs.forEach((catalog, connectorIndex) => {
639
+ const c = conns[connectorIndex];
640
+ if (catalog.status === "fulfilled") {
641
+ for (const ranked of rankTools(catalog.value, q)) {
642
+ matches.push({
643
+ connectorId: c.id,
644
+ connectorTitle: c.title,
645
+ connectorDescription: c.description,
646
+ tool: ranked.tool,
647
+ score: ranked.score,
648
+ order: orderBase + ranked.order,
649
+ });
650
+ }
651
+ }
652
+ orderBase += catalog.status === "fulfilled" ? catalog.value.length : 1;
653
+ });
654
+ matches.sort((a, b) => b.score - a.score || a.order - b.order);
655
+ const page = matches.slice(offset, offset + limit);
656
+ const groups: {
657
+ id: string;
658
+ title?: string;
659
+ description?: string;
660
+ tools: Array<{
661
+ name: string;
662
+ address: string;
663
+ description?: string;
664
+ inputSchema?: unknown;
665
+ outputSchema?: unknown;
666
+ annotations?: ToolDef["annotations"];
667
+ }>;
668
+ }[] = [];
669
+ const byConnector = new Map<string, (typeof groups)[number]>();
670
+ for (const match of page) {
671
+ let group = byConnector.get(match.connectorId);
672
+ if (!group) {
673
+ group = {
674
+ id: match.connectorId,
675
+ ...(match.connectorTitle ? { title: match.connectorTitle } : {}),
676
+ description: match.connectorDescription,
677
+ tools: [],
678
+ };
679
+ byConnector.set(match.connectorId, group);
680
+ groups.push(group);
681
+ }
682
+ const schema = match.tool.inputSchema ?? { type: "object" };
683
+ group.tools.push({
684
+ name: match.tool.name,
685
+ address: `${match.connectorId}.${match.tool.name}`,
686
+ description: summarizeDescription(
687
+ match.tool.description,
688
+ args.fullDescriptions === true,
689
+ ),
690
+ ...(args.includeSchemas
691
+ ? {
692
+ inputSchema:
693
+ args.includeSchemas === "json"
694
+ ? schema
695
+ : compactSchema(schema),
696
+ }
697
+ : {}),
698
+ ...(args.includeSchemas && match.tool.outputSchema
699
+ ? {
700
+ outputSchema:
701
+ args.includeSchemas === "json"
702
+ ? match.tool.outputSchema
703
+ : compactSchema(match.tool.outputSchema),
704
+ }
705
+ : {}),
706
+ ...(match.tool.annotations
707
+ ? { annotations: match.tool.annotations }
708
+ : {}),
709
+ });
710
+ }
711
+ const nextOffset =
712
+ offset + page.length < matches.length
713
+ ? offset + page.length
714
+ : undefined;
715
+ return jsonResult({
716
+ connectors: groups,
717
+ total: matches.length,
718
+ offset,
719
+ limit,
720
+ hasMore: nextOffset !== undefined,
721
+ ...(nextOffset !== undefined ? { nextOffset } : {}),
722
+ });
723
+ },
724
+
725
+ async describeTools(args: DescribeArgs): Promise<ToolResult> {
726
+ const format = args.format ?? "compact";
727
+ const resolved = args.addresses.map((address) => ({
728
+ address,
729
+ resolved: registry.resolveAddress(address),
730
+ }));
731
+ const connectorIds = [
732
+ ...new Set(
733
+ resolved
734
+ .map((entry) => entry.resolved?.connector.id)
735
+ .filter((id): id is string => Boolean(id)),
736
+ ),
737
+ ];
738
+ const loaded = await Promise.allSettled(
739
+ connectorIds.map((id) => registry.getTools(id, baseUrl, requestScope)),
740
+ );
741
+ const catalogs = new Map<string, ToolDef[] | Error>();
742
+ loaded.forEach((result, index) => {
743
+ catalogs.set(
744
+ connectorIds[index],
745
+ result.status === "fulfilled"
746
+ ? result.value
747
+ : result.reason instanceof Error
748
+ ? result.reason
749
+ : new Error(String(result.reason)),
750
+ );
751
+ });
752
+ const out = resolved.map(({ address, resolved }) => {
753
+ if (!resolved) {
754
+ return { address, error: `Unknown address "${address}"` };
755
+ }
756
+ const catalog = catalogs.get(resolved.connector.id);
757
+ if (catalog instanceof Error) {
758
+ return { address, error: catalog.message };
759
+ }
760
+ const tool = catalog?.find((t) => t.name === resolved.toolName);
761
+ if (!tool) {
762
+ return {
763
+ address,
764
+ error: `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
765
+ };
766
+ }
767
+ const schema = tool.inputSchema ?? { type: "object" };
768
+ return {
769
+ address,
770
+ name: tool.name,
771
+ description: summarizeDescription(
772
+ tool.description,
773
+ args.fullDescriptions === true,
774
+ ),
775
+ inputSchema: format === "json" ? schema : compactSchema(schema),
776
+ ...(tool.outputSchema
777
+ ? {
778
+ outputSchema:
779
+ format === "json"
780
+ ? tool.outputSchema
781
+ : compactSchema(tool.outputSchema),
782
+ }
783
+ : {}),
784
+ ...(tool.annotations ? { annotations: tool.annotations } : {}),
785
+ };
786
+ });
787
+ return jsonResult({ tools: out });
788
+ },
789
+
790
+ async callTool(args: CallArgs): Promise<ToolResult> {
791
+ return (await runCall(args, "call_tool")).toolResult;
792
+ },
793
+
794
+ async callDestructiveTool(args: CallArgs): Promise<ToolResult> {
795
+ return (
796
+ await runCall(args, "call_destructive_tool", { allowDestructive: true })
797
+ ).toolResult;
798
+ },
799
+
800
+ async getResult(args: GetResultArgs): Promise<ToolResult> {
801
+ const results = registry.resultsStorage();
802
+ const stored = await results.get(`result:${args.id}`);
803
+ if (stored === null || stored === undefined) {
804
+ return errorResult(`Unknown or expired result id "${args.id}"`);
805
+ }
806
+ const bytes = enc.encode(stored);
807
+ const total = bytes.length;
808
+ const offset = Math.max(0, Math.trunc(args.offset ?? 0));
809
+ const maxBytes = args.maxBytes ?? cap;
810
+ // Align the slice end to a codepoint boundary so a multi-byte char is
811
+ // never split across pages (which would emit U+FFFD on both sides).
812
+ // `nextOffset` is this aligned end, so it is a valid boundary for the
813
+ // next call and paging reassembles the original byte-for-byte.
814
+ const end = alignEndToCharBoundary(
815
+ bytes,
816
+ offset,
817
+ offset + maxBytes,
818
+ total,
819
+ );
820
+ const slice = dec.decode(bytes.slice(offset, end));
821
+ const nextOffset = end < total ? end : undefined;
822
+ return jsonResult({
823
+ offset,
824
+ ...(nextOffset !== undefined ? { nextOffset } : {}),
825
+ totalBytes: total,
826
+ text: slice,
827
+ });
828
+ },
829
+
830
+ async batchCall(args: BatchArgs): Promise<ToolResult> {
831
+ const batchStarted = Date.now();
832
+ const settled = await Promise.allSettled(
833
+ args.calls.map((c) =>
834
+ runCall(
835
+ {
836
+ ...c,
837
+ resultMode: c.resultMode ?? args.resultMode,
838
+ timeoutMs: c.timeoutMs ?? args.timeoutMs,
839
+ maxRetries: c.maxRetries ?? args.maxRetries,
840
+ diagnostics: c.diagnostics ?? args.diagnostics,
841
+ },
842
+ "batch_call",
843
+ ),
844
+ ),
845
+ );
846
+ const results = settled.map((s, i) => {
847
+ const address = args.calls[i].address;
848
+ if (s.status === "rejected") {
849
+ const message = msg(s.reason);
850
+ return {
851
+ address,
852
+ ok: false,
853
+ error: message,
854
+ errorDetails: errorDetails("batch_call_failed", message),
855
+ };
856
+ }
857
+ const r = s.value;
858
+ if (r.error) {
859
+ return {
860
+ address,
861
+ ok: false,
862
+ error: r.error.message,
863
+ errorDetails: r.error,
864
+ durationMs: r.durationMs,
865
+ attempts: r.attempts,
866
+ ...((args.calls[i].diagnostics ?? args.diagnostics)
867
+ ? { timing: r.timing }
868
+ : {}),
869
+ };
870
+ }
871
+ if ((args.calls[i].resultMode ?? args.resultMode) === "value") {
872
+ return {
873
+ address,
874
+ ok: true,
875
+ data: r.value,
876
+ durationMs: r.durationMs,
877
+ attempts: r.attempts,
878
+ ...((args.calls[i].diagnostics ?? args.diagnostics)
879
+ ? { timing: r.timing }
880
+ : {}),
881
+ };
882
+ }
883
+ return {
884
+ address,
885
+ ok: true,
886
+ result: r.toolResult.content,
887
+ durationMs: r.durationMs,
888
+ attempts: r.attempts,
889
+ ...((args.calls[i].diagnostics ?? args.diagnostics)
890
+ ? { timing: r.timing }
891
+ : {}),
892
+ };
893
+ });
894
+ return jsonResult({
895
+ results,
896
+ durationMs: Date.now() - batchStarted,
897
+ });
898
+ },
899
+
900
+ async authorizeConnector(args: AuthorizeArgs): Promise<ToolResult> {
901
+ const connector = registry.getConnector(args.connector);
902
+ if (!connector) {
903
+ return errorResult(`Unknown connector "${args.connector}"`);
904
+ }
905
+ if (!connector.startAuth) {
906
+ return errorResult(
907
+ `Connector "${args.connector}" does not use downstream OAuth — its auth is static (headers/none), so there is nothing to authorize.`,
908
+ );
909
+ }
910
+ const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
911
+ try {
912
+ const status = await connector.startAuth(ctx, { force: args.force });
913
+ if (status.state === "auth_required" && !status.authorizationUrl) {
914
+ // auth_required with nothing to open is a dead end for the operator.
915
+ return errorResult(
916
+ `Connector "${connector.id}": authorization required but no URL is available — retry authorize_connector.`,
917
+ );
918
+ }
919
+ return jsonResult({
920
+ connector: connector.id,
921
+ status: status.state,
922
+ ...(status.authorizationUrl
923
+ ? {
924
+ authorizationUrl: status.authorizationUrl,
925
+ instructions:
926
+ "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. Re-run list_connectors afterwards to confirm status is ok.",
927
+ }
928
+ : {}),
929
+ ...(status.message ? { message: status.message } : {}),
930
+ });
931
+ } catch (err) {
932
+ return errorResult(msg(err));
933
+ } finally {
934
+ // Auth state may have changed — even on a throw or a half-wiped force —
935
+ // so don't serve a stale tool list.
936
+ await registry.invalidateStored(connector.id);
937
+ }
938
+ },
939
+ };
940
+ }
941
+
942
+ const LIST_DESC =
943
+ "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.";
944
+ const SEARCH_DESC =
945
+ 'Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all. includeSchemas="compact" usually removes the describe_tools round trip.';
946
+ const DESCRIBE_DESC =
947
+ 'Inspect known tool addresses when search_tools did not include a sufficient schema. Returns descriptions, input/output schemas, and behavior annotations; format "compact" is the default.';
948
+ const CALL_DESC =
949
+ 'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths, resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
950
+ const CALL_DESTRUCTIVE_DESC =
951
+ "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.";
952
+ const GET_RESULT_DESC =
953
+ "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.";
954
+ const BATCH_DESC =
955
+ "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.";
956
+ const AUTHORIZE_DESC =
957
+ "Use after a connector reports auth_required. Starts downstream OAuth and returns an authorizationUrl for the operator to open. force=true wipes stored credentials first and restarts consent.";
958
+ const SKILLS_DESC =
959
+ '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.';
960
+
961
+ /** Register the nine meta-tools onto an McpServer instance. */
962
+ export function registerMetaTools(
963
+ server: McpServer,
964
+ registry: Registry,
965
+ ctx: {
966
+ baseUrl: string;
967
+ maxResultBytes?: number;
968
+ activity?: ActivityRequestContext;
969
+ },
970
+ ): void {
971
+ const mt = createMetaTools(registry, ctx.baseUrl, {
972
+ maxResultBytes: ctx.maxResultBytes,
973
+ activity: ctx.activity,
974
+ });
975
+
976
+ server.registerTool(
977
+ "skills",
978
+ {
979
+ description: SKILLS_DESC,
980
+ inputSchema: { name: z.string().optional() },
981
+ annotations: {
982
+ readOnlyHint: true,
983
+ destructiveHint: false,
984
+ idempotentHint: true,
985
+ openWorldHint: false,
986
+ },
987
+ },
988
+ async (args) => mt.skills(args as SkillArgs),
989
+ );
990
+
991
+ server.registerTool(
992
+ "list_connectors",
993
+ {
994
+ description: LIST_DESC,
995
+ inputSchema: { probe: z.boolean().optional() },
996
+ },
997
+ async (args) => mt.listConnectors(args as ListArgs),
998
+ );
999
+
1000
+ server.registerTool(
1001
+ "search_tools",
1002
+ {
1003
+ description: SEARCH_DESC,
1004
+ inputSchema: {
1005
+ query: z.string().optional(),
1006
+ connector: z.string().optional(),
1007
+ limit: z.number().int().positive().optional(),
1008
+ offset: z.number().int().nonnegative().optional(),
1009
+ fullDescriptions: z.boolean().optional(),
1010
+ includeSchemas: z.enum(["compact", "json"]).optional(),
1011
+ },
1012
+ },
1013
+ async (args) => mt.searchTools(args as SearchArgs),
1014
+ );
1015
+
1016
+ server.registerTool(
1017
+ "describe_tools",
1018
+ {
1019
+ description: DESCRIBE_DESC,
1020
+ inputSchema: {
1021
+ addresses: z.array(z.string()),
1022
+ format: z.enum(["compact", "json"]).optional(),
1023
+ fullDescriptions: z.boolean().optional(),
1024
+ },
1025
+ },
1026
+ async (args) => mt.describeTools(args as DescribeArgs),
1027
+ );
1028
+
1029
+ server.registerTool(
1030
+ "call_tool",
1031
+ {
1032
+ description: CALL_DESC,
1033
+ inputSchema: {
1034
+ address: z.string(),
1035
+ args: z.record(z.string(), z.unknown()).optional(),
1036
+ fields: z.array(z.string()).optional(),
1037
+ resultMode: z.enum(["mcp", "value"]).optional(),
1038
+ timeoutMs: z.number().int().positive().optional(),
1039
+ maxRetries: z.number().int().min(0).max(2).optional(),
1040
+ diagnostics: z.boolean().optional(),
1041
+ },
1042
+ },
1043
+ async (args) => mt.callTool(args as CallArgs),
1044
+ );
1045
+
1046
+ server.registerTool(
1047
+ "call_destructive_tool",
1048
+ {
1049
+ description: CALL_DESTRUCTIVE_DESC,
1050
+ inputSchema: {
1051
+ address: z.string(),
1052
+ args: z.record(z.string(), z.unknown()).optional(),
1053
+ fields: z.array(z.string()).optional(),
1054
+ resultMode: z.enum(["mcp", "value"]).optional(),
1055
+ timeoutMs: z.number().int().positive().optional(),
1056
+ maxRetries: z.number().int().min(0).max(2).optional(),
1057
+ diagnostics: z.boolean().optional(),
1058
+ },
1059
+ annotations: {
1060
+ destructiveHint: true,
1061
+ readOnlyHint: false,
1062
+ openWorldHint: true,
1063
+ },
1064
+ },
1065
+ async (args) => mt.callDestructiveTool(args as CallArgs),
1066
+ );
1067
+
1068
+ server.registerTool(
1069
+ "authorize_connector",
1070
+ {
1071
+ description: AUTHORIZE_DESC,
1072
+ inputSchema: {
1073
+ connector: z.string(),
1074
+ force: z.boolean().optional(),
1075
+ },
1076
+ },
1077
+ async (args) => mt.authorizeConnector(args as AuthorizeArgs),
1078
+ );
1079
+
1080
+ server.registerTool(
1081
+ "get_result",
1082
+ {
1083
+ description: GET_RESULT_DESC,
1084
+ inputSchema: {
1085
+ id: z.string(),
1086
+ offset: z.number().int().nonnegative().optional(),
1087
+ maxBytes: z.number().int().positive().optional(),
1088
+ },
1089
+ },
1090
+ async (args) => mt.getResult(args as GetResultArgs),
1091
+ );
1092
+
1093
+ server.registerTool(
1094
+ "batch_call",
1095
+ {
1096
+ description: BATCH_DESC,
1097
+ inputSchema: {
1098
+ calls: z
1099
+ .array(
1100
+ z.object({
1101
+ address: z.string(),
1102
+ args: z.record(z.string(), z.unknown()).optional(),
1103
+ fields: z.array(z.string()).optional(),
1104
+ resultMode: z.enum(["mcp", "value"]).optional(),
1105
+ timeoutMs: z.number().int().positive().optional(),
1106
+ maxRetries: z.number().int().min(0).max(2).optional(),
1107
+ diagnostics: z.boolean().optional(),
1108
+ }),
1109
+ )
1110
+ .min(1)
1111
+ .max(10),
1112
+ resultMode: z.enum(["mcp", "value"]).optional(),
1113
+ timeoutMs: z.number().int().positive().optional(),
1114
+ maxRetries: z.number().int().min(0).max(2).optional(),
1115
+ diagnostics: z.boolean().optional(),
1116
+ },
1117
+ },
1118
+ async (args) => mt.batchCall(args as BatchArgs),
1119
+ );
1120
+ }