@zackbart/connecta 0.41.0 → 0.42.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 (148) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/connecta.mjs +26 -17
  3. package/bin/meta-tool-names.d.mts +3 -1
  4. package/bin/meta-tool-names.mjs +4 -1
  5. package/bin/version.mjs +1 -1
  6. package/dist/activity.d.ts +2 -1
  7. package/dist/activity.js +5 -5
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-routes.js +31 -5
  10. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  11. package/dist/auth/downstream-oauth.js +3 -0
  12. package/dist/auth/static-oauth.js +26 -18
  13. package/dist/auth-elicitation.js +118 -61
  14. package/dist/authorization-handoff.js +36 -3
  15. package/dist/catalog-cache.js +292 -24
  16. package/dist/catalog-freshness.js +12 -0
  17. package/dist/catalog-service.js +373 -70
  18. package/dist/catalog.js +147 -58
  19. package/dist/config-defaults.d.ts +2 -1
  20. package/dist/config-defaults.js +4 -1
  21. package/dist/config.d.ts +43 -17
  22. package/dist/config.js +26 -10
  23. package/dist/connectors/remote-mcp/catalog.js +52 -28
  24. package/dist/connectors/remote-mcp/connection.js +62 -5
  25. package/dist/connectors/remote-mcp.js +10 -1
  26. package/dist/describe-config.d.ts +3 -2
  27. package/dist/discovery-health.js +26 -11
  28. package/dist/errors.d.ts +52 -3
  29. package/dist/errors.js +86 -3
  30. package/dist/execute.js +476 -200
  31. package/dist/executor-admission.js +2 -0
  32. package/dist/executor-contract.js +6 -0
  33. package/dist/executor-result.js +2 -2
  34. package/dist/executors/quickjs-child.js +13 -4
  35. package/dist/executors/quickjs-runtime.d.ts +2 -0
  36. package/dist/executors/quickjs-runtime.js +36 -19
  37. package/dist/executors/quickjs.js +23 -6
  38. package/dist/guest-functions.js +5 -4
  39. package/dist/guest-runtime.js +37 -1
  40. package/dist/guest-types.d.ts +18 -0
  41. package/dist/http/client.js +7 -2
  42. package/dist/http/errors.js +9 -4
  43. package/dist/http/fields.js +17 -5
  44. package/dist/http/hatch.js +9 -3
  45. package/dist/http/paging.js +1 -1
  46. package/dist/http/provider.js +2 -6
  47. package/dist/http/vendor.js +6 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +5 -0
  50. package/dist/invocation.js +98 -34
  51. package/dist/meta-output.js +24 -18
  52. package/dist/meta-tool-names.d.ts +1 -1
  53. package/dist/meta-tool-names.js +1 -1
  54. package/dist/meta-tools.js +259 -60
  55. package/dist/native-recovery.js +4 -1
  56. package/dist/native-tools.js +166 -51
  57. package/dist/node.js +3 -1
  58. package/dist/oauth-handoff.js +53 -43
  59. package/dist/operation-activity.js +2 -1
  60. package/dist/operation-vocabulary.js +6 -0
  61. package/dist/operator-record.d.ts +1 -1
  62. package/dist/operator-record.js +1 -0
  63. package/dist/operator-ui/generated.js +2 -2
  64. package/dist/program-result.js +2 -0
  65. package/dist/program-source.js +1 -1
  66. package/dist/program-writes.js +112 -19
  67. package/dist/provider.d.ts +0 -13
  68. package/dist/provider.js +0 -3
  69. package/dist/providers/_shared/google/kit.js +2 -2
  70. package/dist/providers/breeze/api.js +2 -2
  71. package/dist/providers/breeze/skill.generated.js +1 -1
  72. package/dist/providers/ccb/api.js +2 -2
  73. package/dist/providers/ccb/skill.generated.js +1 -1
  74. package/dist/providers/cloudflare/api.js +2 -2
  75. package/dist/providers/cloudflare/index.js +2 -0
  76. package/dist/providers/cloudflare/skill.generated.js +1 -1
  77. package/dist/providers/docs/skill.generated.js +1 -1
  78. package/dist/providers/drive/skill.generated.js +1 -1
  79. package/dist/providers/forms/index.js +1 -0
  80. package/dist/providers/forms/skill.generated.js +1 -1
  81. package/dist/providers/github/api.js +98 -6
  82. package/dist/providers/github/index.js +7 -2
  83. package/dist/providers/github/lib/auth.d.ts +3 -1
  84. package/dist/providers/github/lib/auth.js +43 -12
  85. package/dist/providers/github/lib/discovery.js +11 -1
  86. package/dist/providers/github/lib/operations.js +8 -0
  87. package/dist/providers/github/safety.js +39 -0
  88. package/dist/providers/github/skill.generated.js +1 -1
  89. package/dist/providers/github/tools.js +12 -5
  90. package/dist/providers/gmail/index.js +1 -0
  91. package/dist/providers/gmail/skill.generated.js +1 -1
  92. package/dist/providers/granola/skill.generated.js +1 -1
  93. package/dist/providers/overflow/index.js +2 -0
  94. package/dist/providers/overflow/skill.generated.js +1 -1
  95. package/dist/providers/planning-center/index.js +11 -5
  96. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  97. package/dist/providers/planning-center/skill.generated.js +1 -1
  98. package/dist/providers/quickbooks/index.js +2 -5
  99. package/dist/providers/quickbooks/skill.generated.js +1 -1
  100. package/dist/providers/sheets/index.js +1 -0
  101. package/dist/providers/sheets/skill.generated.js +1 -1
  102. package/dist/providers/slides/index.js +2 -2
  103. package/dist/providers/slides/skill.generated.js +1 -1
  104. package/dist/providers/stripe/skill.generated.js +1 -1
  105. package/dist/providers/tithely/index.js +1 -0
  106. package/dist/providers/tithely/skill.generated.js +1 -1
  107. package/dist/providers/vercel/api.js +2 -2
  108. package/dist/providers/vercel/skill.generated.js +1 -1
  109. package/dist/registry.d.ts +36 -4
  110. package/dist/registry.js +127 -27
  111. package/dist/result-preview.js +210 -0
  112. package/dist/result-seal.js +168 -0
  113. package/dist/route-names.js +13 -4
  114. package/dist/routes/connect.js +21 -13
  115. package/dist/routes/mcp.js +55 -26
  116. package/dist/routes/shared.d.ts +6 -0
  117. package/dist/runtime/call.js +15 -10
  118. package/dist/runtime/request-scope.js +36 -1
  119. package/dist/runtime/run.js +4 -1
  120. package/dist/sent-secrets.d.ts +20 -0
  121. package/dist/sent-secrets.js +49 -1
  122. package/dist/skills.js +100 -33
  123. package/dist/storage/keys.js +9 -6
  124. package/dist/storage/sql-native.js +1 -1
  125. package/dist/tool-safety.d.ts +33 -0
  126. package/dist/tool-safety.js +51 -15
  127. package/dist/types.d.ts +31 -0
  128. package/dist/usage-guide.js +14 -13
  129. package/dist/validate.js +29 -10
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/dist/worker.js +28 -8
  133. package/documentation/architecture.md +104 -42
  134. package/documentation/auth.md +46 -19
  135. package/documentation/code-mode.md +378 -116
  136. package/documentation/deploying.md +10 -9
  137. package/documentation/hosted-mcp-migration.md +9 -3
  138. package/documentation/integrating.md +27 -5
  139. package/documentation/meta-tools.md +655 -153
  140. package/documentation/operating.md +5 -3
  141. package/documentation/providers.md +18 -6
  142. package/examples/worker/README.md +3 -3
  143. package/guides/how-it-works.md +2 -1
  144. package/guides/safety-and-trust.md +1 -3
  145. package/package.json +1 -1
  146. package/templates/node/AGENTS.md +3 -3
  147. package/templates/node/README.md +1 -1
  148. package/templates/node/package.json +1 -1
package/dist/catalog.js CHANGED
@@ -1,5 +1,5 @@
1
- const DEFAULT_DESCRIPTION_LENGTH = 240;
2
1
  const DISCOVERY_DESCRIPTION_LENGTH = 160;
2
+ const DISCOVERY_DESCRIPTION_MINIMUM = 40;
3
3
  export const MAX_COMPACT_DISCOVERY_SCHEMA_BYTES = 1_024;
4
4
  const MAX_COMPACT_DISCOVERY_ENUM_BYTES = MAX_COMPACT_DISCOVERY_SCHEMA_BYTES / 4;
5
5
  const MAX_COMPACT_DISCOVERY_CONSTRAINT_BYTES = MAX_COMPACT_DISCOVERY_SCHEMA_BYTES / 4;
@@ -75,21 +75,46 @@ class SchemaWork {
75
75
  }));
76
76
  }
77
77
  }
78
- export function summarizeDescription(text, full) {
79
- return summarizeToLength(text, full, DEFAULT_DESCRIPTION_LENGTH);
80
- }
81
- export function summarizeDiscoveryDescription(text, full) {
82
- return summarizeToLength(text, full, DISCOVERY_DESCRIPTION_LENGTH);
83
- }
84
- function summarizeToLength(text, full, maxLength) {
85
- if (!text)
86
- return undefined;
87
- if (full)
88
- return text;
89
- const compact = text.replace(/\s+/g, " ").trim();
90
- if (compact.length <= maxLength)
91
- return compact;
92
- return `${compact.slice(0, maxLength - 1).trimEnd()}…`;
78
+ /** A sentence's end, or the paragraph break `discoverySummary` marks with a newline. */
79
+ const SENTENCE_END = /[.!?]["'`)\]]*(?= |\n|$)|\n/g;
80
+ /** A period that follows an initial or a common abbreviation ends no sentence. */
81
+ const ABBREVIATED = /(?:^|[\s(.])(?:e\.g|i\.e|etc|vs|cf|approx|[a-z])\.$/i;
82
+ /**
83
+ * What search shows of a description: the first complete sentence, joined to
84
+ * the next ones until it reaches 40 characters, within 160. Text with no such
85
+ * boundary is cut at a word. `truncated` says text was left out.
86
+ */
87
+ export function discoverySummary(text) {
88
+ const marked = text
89
+ .trim()
90
+ .split(/\n\s*\n/)
91
+ .map((paragraph) => paragraph.replace(/\s+/g, " ").trim())
92
+ .filter(Boolean)
93
+ .join("\n");
94
+ const whole = marked.replaceAll("\n", " ");
95
+ let end;
96
+ for (const match of marked.matchAll(SENTENCE_END)) {
97
+ const candidate = match[0] === "\n" ? match.index : match.index + match[0].length;
98
+ if (candidate < DISCOVERY_DESCRIPTION_MINIMUM)
99
+ continue;
100
+ if (candidate > DISCOVERY_DESCRIPTION_LENGTH)
101
+ break;
102
+ if (ABBREVIATED.test(marked.slice(Math.max(0, candidate - 8), candidate)))
103
+ continue;
104
+ end = candidate;
105
+ break;
106
+ }
107
+ if (end !== undefined)
108
+ return { text: whole.slice(0, end), truncated: end < whole.length };
109
+ if (whole.length <= DISCOVERY_DESCRIPTION_LENGTH)
110
+ return { text: whole, truncated: false };
111
+ let cut = DISCOVERY_DESCRIPTION_LENGTH - 1;
112
+ const space = whole.lastIndexOf(" ", cut);
113
+ if (space >= DISCOVERY_DESCRIPTION_MINIMUM)
114
+ cut = space;
115
+ else if (/[\uD800-\uDBFF]/.test(whole.charAt(cut - 1)))
116
+ cut -= 1;
117
+ return { text: `${whole.slice(0, cut).trimEnd()}…`, truncated: true };
93
118
  }
94
119
  function lexicalTokens(text) {
95
120
  return text
@@ -163,6 +188,7 @@ export function lexicalSearchContent(query) {
163
188
  const contentTerms = terms.filter((term) => !CONVERSATIONAL_QUERY_WORDS.has(term));
164
189
  return contentTerms.join(" ");
165
190
  }
191
+ const ZONES = ["nameTokens", "titleTokens", "summaryTokens", "tailTokens"];
166
192
  const searchIndexes = new WeakMap();
167
193
  // Only immutable token strings and numeric positions cross requests. The key
168
194
  // holds exactly what buildIndex reads, in order, so schemas cost nothing here;
@@ -170,36 +196,39 @@ const searchIndexes = new WeakMap();
170
196
  const retainedIndexes = new Map();
171
197
  const INDEX_BYTES = 8 * 1024 * 1024;
172
198
  let retainedBytes = 0;
199
+ /** A tool's display title; MCP carries it on the tool or on its annotations. */
200
+ function toolTitle(tool) {
201
+ const title = tool.title ?? tool.annotations?.title;
202
+ return typeof title === "string" ? title : "";
203
+ }
173
204
  function buildIndex(tools) {
174
- const nameTokenDocuments = new Map();
175
- const descriptionTokenDocuments = new Map();
176
- const addTokens = (target, tokens, position) => {
177
- for (const token of new Set(tokens)) {
178
- const documents = target.get(token) ?? [];
179
- documents.push(position);
180
- target.set(token, documents);
181
- }
182
- };
205
+ const postings = ZONES.map(() => new Map());
183
206
  const documents = tools.map((tool, position) => {
207
+ const description = tool.description ?? "";
184
208
  const nameTokens = lexicalTokens(tool.name);
185
- const descriptionTokens = [...new Set(lexicalTokens(tool.description ?? ""))];
186
- addTokens(nameTokenDocuments, nameTokens, position);
187
- addTokens(descriptionTokenDocuments, descriptionTokens, position);
188
- return Object.freeze({
209
+ const summary = new Set(lexicalTokens(discoverySummary(description).text));
210
+ const doc = {
189
211
  name: nameTokens.join(" "),
190
- nameTokens: Object.freeze(nameTokens),
191
- descriptionTokens: Object.freeze(descriptionTokens),
212
+ nameTokens,
213
+ titleTokens: [...new Set(lexicalTokens(toolTitle(tool)))],
214
+ summaryTokens: [...summary],
215
+ tailTokens: [...new Set(lexicalTokens(description))].filter((token) => !summary.has(token)),
216
+ };
217
+ ZONES.forEach((zone, index) => {
218
+ Object.freeze(doc[zone]);
219
+ for (const token of new Set(doc[zone])) {
220
+ const positions = postings[index].get(token) ?? [];
221
+ positions.push(position);
222
+ postings[index].set(token, positions);
223
+ }
192
224
  });
225
+ return Object.freeze(doc);
193
226
  });
194
- for (const map of [nameTokenDocuments, descriptionTokenDocuments])
227
+ for (const map of postings)
195
228
  for (const positions of map.values())
196
229
  Object.freeze(positions);
197
230
  // Maps remain private to this module and are never mutated after construction.
198
- return Object.freeze({
199
- documents: Object.freeze(documents),
200
- nameTokenDocuments,
201
- descriptionTokenDocuments,
202
- });
231
+ return Object.freeze({ documents: Object.freeze(documents), postings: Object.freeze(postings) });
203
232
  }
204
233
  function bindIndex(tools, index) {
205
234
  const view = { index, tools: new Map(tools.map((tool, position) => [position, tool])) };
@@ -210,7 +239,7 @@ function bindIndex(tools, index) {
210
239
  export function prepareLexicalCatalog(tools, partition) {
211
240
  if (searchIndexes.has(tools))
212
241
  return;
213
- const key = JSON.stringify([partition, tools.map((tool) => [tool.name, tool.description ?? ""])]);
242
+ const key = JSON.stringify([partition, tools.map((tool) => [tool.name, tool.description ?? "", toolTitle(tool)])]);
214
243
  const retained = retainedIndexes.get(key);
215
244
  if (retained) {
216
245
  bindIndex(tools, retained.index);
@@ -221,11 +250,11 @@ export function prepareLexicalCatalog(tools, partition) {
221
250
  let bytes = key.length * 2 + 128;
222
251
  for (const doc of index.documents) {
223
252
  bytes += 128 + doc.name.length * 2;
224
- for (const tokens of [doc.nameTokens, doc.descriptionTokens])
225
- for (const token of tokens)
253
+ for (const zone of ZONES)
254
+ for (const token of doc[zone])
226
255
  bytes += 32 + token.length * 2;
227
256
  }
228
- for (const map of [index.nameTokenDocuments, index.descriptionTokenDocuments])
257
+ for (const map of index.postings)
229
258
  for (const [token, positions] of map)
230
259
  bytes += 96 + token.length * 2 + positions.length * 8;
231
260
  if (bytes > INDEX_BYTES)
@@ -267,6 +296,24 @@ function inflectionVariants(base) {
267
296
  ...(base.endsWith("y") ? [`${base.slice(0, -1)}ies`, `${base.slice(0, -1)}ied`] : []),
268
297
  ];
269
298
  }
299
+ /**
300
+ * Shortenings API names use for words people write out. Only unambiguous
301
+ * pairs: `auth` could mean either authentication or authorization.
302
+ */
303
+ const ABBREVIATIONS = new Map([
304
+ "config configuration",
305
+ "db database",
306
+ "doc document",
307
+ "env environment",
308
+ "org organization",
309
+ "repo repository",
310
+ "var variable",
311
+ ]
312
+ .map((pair) => pair.split(" "))
313
+ .flatMap(([short, long]) => [
314
+ [short, long],
315
+ [long, short],
316
+ ]));
270
317
  function matchingTokenCandidates(term) {
271
318
  const candidates = new Set([term, ...inflectionVariants(term)]);
272
319
  const possibleBases = [
@@ -282,6 +329,12 @@ function matchingTokenCandidates(term) {
282
329
  candidates.add(base);
283
330
  }
284
331
  }
332
+ for (const candidate of candidates) {
333
+ const other = ABBREVIATIONS.get(candidate);
334
+ if (other)
335
+ for (const variant of [other, ...inflectionVariants(other)])
336
+ candidates.add(variant);
337
+ }
285
338
  return candidates;
286
339
  }
287
340
  /**
@@ -334,30 +387,38 @@ export function lexicalCorpusStatistics(toolSets, query, synonymsFor) {
334
387
  documentFrequency: new Map(),
335
388
  nameMatches: new Map(),
336
389
  descriptionMatches: new Map(),
390
+ headMatches: new Map(),
337
391
  };
338
392
  }
339
393
  const nameMatches = new Map();
340
394
  const descriptionMatches = new Map();
395
+ const headMatches = new Map();
341
396
  for (const term of terms) {
342
397
  const termNameMatches = new Set();
343
398
  const termDescriptionMatches = new Set();
399
+ const termHeadMatches = new Set();
344
400
  for (const tools of toolSets) {
345
401
  const index = indexFor(tools);
346
402
  for (const candidate of synonymCandidates(term, synonymsFor?.(tools))) {
347
- for (const position of index.index.nameTokenDocuments.get(candidate) ?? []) {
348
- const tool = index.tools.get(position);
349
- if (tool)
350
- termNameMatches.add(tool);
351
- }
352
- for (const position of index.index.descriptionTokenDocuments.get(candidate) ?? []) {
353
- const tool = index.tools.get(position);
354
- if (tool)
355
- termDescriptionMatches.add(tool);
356
- }
403
+ // Name and title, summary and tail: only the tail is outside the head.
404
+ [
405
+ [termNameMatches, termHeadMatches],
406
+ [termDescriptionMatches, termHeadMatches],
407
+ [termDescriptionMatches, termHeadMatches],
408
+ [termDescriptionMatches],
409
+ ].forEach((targets, zone) => {
410
+ for (const position of index.index.postings[zone].get(candidate) ?? []) {
411
+ const tool = index.tools.get(position);
412
+ if (tool)
413
+ for (const target of targets)
414
+ target.add(tool);
415
+ }
416
+ });
357
417
  }
358
418
  }
359
419
  nameMatches.set(term, termNameMatches);
360
420
  descriptionMatches.set(term, termDescriptionMatches);
421
+ headMatches.set(term, termHeadMatches);
361
422
  }
362
423
  const documentFrequency = new Map(terms.map((term) => [
363
424
  term,
@@ -368,15 +429,20 @@ export function lexicalCorpusStatistics(toolSets, query, synonymsFor) {
368
429
  documentFrequency,
369
430
  nameMatches,
370
431
  descriptionMatches,
432
+ headMatches,
371
433
  };
372
434
  }
435
+ /** A hatch's standing when one operation states every term: coverage, without a named tool's name evidence. */
436
+ export function vocabularyScore(terms, statistics) {
437
+ return terms.reduce((total, term) => total + 5 * inverseDocumentFrequency(term, statistics), 0);
438
+ }
373
439
  function inverseDocumentFrequency(term, statistics) {
374
440
  const frequency = statistics.documentFrequency.get(term) ?? 0;
375
441
  return Math.log(1 + (statistics.documentCount - frequency + 0.5) / (frequency + 0.5));
376
442
  }
377
443
  function scoreDocument(doc, phrase, terms, mode, statistics) {
378
444
  if (!phrase)
379
- return { score: 0, matchedTermCount: 0 };
445
+ return { score: 0, matchedTermCount: 0, headTerms: [], nameTerms: [], namesDistinctiveTerm: false };
380
446
  const matchedTerms = terms.filter((term) => statistics.nameMatches.get(term)?.has(doc.tool) || statistics.descriptionMatches.get(term)?.has(doc.tool));
381
447
  if (mode === "all" && matchedTerms.length !== terms.length)
382
448
  return null;
@@ -384,8 +450,10 @@ function scoreDocument(doc, phrase, terms, mode, statistics) {
384
450
  return null;
385
451
  // Coverage remains meaningful in partial mode, but is IDF-weighted rather
386
452
  // than a raw term count: one rare domain term can beat several ubiquitous
387
- // catalog verbs.
388
- let score = matchedTerms.reduce((total, term) => total + 4 * inverseDocumentFrequency(term, statistics), 0);
453
+ // catalog verbs. A term met only in the description's tail is weak coverage,
454
+ // so a long description cannot outrank a tool that names what was asked.
455
+ const inHead = (term) => statistics.headMatches.get(term)?.has(doc.tool) === true;
456
+ let score = matchedTerms.reduce((total, term) => total + (inHead(term) ? 4 : 1) * inverseDocumentFrequency(term, statistics), 0);
389
457
  const phraseWeight = terms.reduce((total, term) => total + inverseDocumentFrequency(term, statistics), 0);
390
458
  if (doc.name === phrase)
391
459
  score += 40 * phraseWeight;
@@ -401,13 +469,31 @@ function scoreDocument(doc, phrase, terms, mode, statistics) {
401
469
  else if (statistics.nameMatches.get(term)?.has(doc.tool)) {
402
470
  score += 8 * weight;
403
471
  }
404
- if (doc.descriptionTokens.includes(term))
472
+ // A term reaching a zone only through an inflection or synonym earns half.
473
+ if (doc.summaryTokens.includes(term))
405
474
  score += 3 * weight;
406
- else if (statistics.descriptionMatches.get(term)?.has(doc.tool)) {
475
+ else if (doc.titleTokens.includes(term))
476
+ score += 2 * weight;
477
+ else if (doc.tailTokens.includes(term))
478
+ score += 0.5 * weight;
479
+ else if (inHead(term))
407
480
  score += 1.5 * weight;
408
- }
481
+ else if (statistics.descriptionMatches.get(term)?.has(doc.tool))
482
+ score += 0.25 * weight;
409
483
  }
410
- return { score, matchedTermCount: matchedTerms.length };
484
+ // Partial evidence earns only its squared share of the query's rarity, wherever
485
+ // it appears, so a common verb in a name cannot outweigh rare words in a tail.
486
+ const covered = matchedTerms.reduce((total, term) => total + inverseDocumentFrequency(term, statistics), 0);
487
+ score *= (covered / phraseWeight) ** 2;
488
+ const averageWeight = phraseWeight / terms.length;
489
+ const nameTerms = matchedTerms.filter((term) => statistics.nameMatches.get(term)?.has(doc.tool));
490
+ return {
491
+ score,
492
+ matchedTermCount: matchedTerms.length,
493
+ headTerms: matchedTerms.filter(inHead),
494
+ nameTerms,
495
+ namesDistinctiveTerm: nameTerms.some((term) => inverseDocumentFrequency(term, statistics) >= averageWeight),
496
+ };
411
497
  }
412
498
  function queryContainsExactName(doc, phrase) {
413
499
  if (!doc.name || !phrase)
@@ -439,6 +525,9 @@ export function rankTools(tools, query, mode = "all", statistics = lexicalCorpus
439
525
  order: visibleOrder,
440
526
  exactName: queryContainsExactName(doc, exactNamePhrase),
441
527
  matchedTermCount: scored.matchedTermCount,
528
+ headTerms: scored.headTerms,
529
+ nameTerms: scored.nameTerms,
530
+ namesDistinctiveTerm: scored.namesDistinctiveTerm,
442
531
  });
443
532
  }
444
533
  });
@@ -18,9 +18,10 @@ export declare const CONFIG_DEFAULTS: {
18
18
  readonly maxEmittedBlocks: 32;
19
19
  readonly maxHostCalls: 20;
20
20
  readonly hostCallTimeoutMs: 15000;
21
- readonly watchdogMs: 120000;
21
+ readonly watchdogMs: 55000;
22
22
  readonly maxWrites: 10;
23
23
  readonly maxResultChars: 24000;
24
+ readonly readRoute: true;
24
25
  };
25
26
  readonly admission: {
26
27
  readonly requests: {
@@ -25,9 +25,12 @@ export const CONFIG_DEFAULTS = {
25
25
  maxEmittedBlocks: 32,
26
26
  maxHostCalls: 20,
27
27
  hostCallTimeoutMs: 15_000,
28
- watchdogMs: 120_000,
28
+ // Below the 60 s MCP clients give one request, leaving time to send a typed
29
+ // result. See documentation/code-mode.md#cancellation-and-limits.
30
+ watchdogMs: 55_000,
29
31
  maxWrites: 10,
30
32
  maxResultChars: 24_000,
33
+ readRoute: true,
31
34
  },
32
35
  admission: {
33
36
  requests: {
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ClassificationOverride } from "./tool-safety.js";
1
2
  import type { CredentialVault } from "./credential-contract.js";
2
3
  import { type ConfigInput, type ConfigOutput } from "./config-schema.js";
3
4
  import type { MemberRef } from "./membership.js";
@@ -31,7 +32,8 @@ declare const calls: {
31
32
  /**
32
33
  * Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
33
34
  * `timeoutMs`. An explicit per-call value wins. Unset by default, when those
34
- * calls fall back to `execute.hostCallTimeoutMs`.
35
+ * calls fall back to `execute.hostCallTimeoutMs`. `execute.watchdogMs` caps
36
+ * every one of them.
35
37
  *
36
38
  * Each call makes one attempt. `connecta.call` inside `execute_code` uses
37
39
  * it too, ahead of `execute.hostCallTimeoutMs`; `connecta.result`,
@@ -67,16 +69,19 @@ declare const execute: {
67
69
  * `connecta.read`, in milliseconds. Default 15_000. Discovery uses
68
70
  * `discovery.probeTimeoutMs`. `connecta.call` and the direct call tools use it only when
69
71
  * `calls.defaultTimeoutMs` is unset, and an explicit per-call `timeoutMs`
70
- * always wins. Raise it for providers whose legitimate calls run longer,
71
- * such as analytics queries.
72
+ * wins, up to `execute.watchdogMs`. Raise it for providers whose legitimate
73
+ * calls run longer, such as analytics queries.
72
74
  */
73
75
  hostCallTimeoutMs: import("./config-schema.js").Field<number, number, true>;
74
76
  /**
75
- * Hard ceiling on one execution, in milliseconds, enforced outside the
76
- * sandbox. Default 120_000, above both executors' own deadlines (QuickJS
77
- * 30_000, the Dynamic Worker 60_000), so it only ends a run whose executor
78
- * never settled; that run fails as unresponsive and its admission slot is
79
- * released. Keep it above any raised executor deadline.
77
+ * Ceiling on one `execute_code` or direct call response, in milliseconds,
78
+ * counted from the request's arrival and enforced outside the sandbox.
79
+ * Default 55_000: MCP clients commonly abandon a request at 60_000, and only
80
+ * a response sent before that reaches the agent as a typed result. It sits
81
+ * above both executors' own deadlines (QuickJS 30_000, the Dynamic Worker
82
+ * 50_000), so a run ends here only if its executor never settled, or if
83
+ * queueing spent its time. An explicit `timeoutMs` above it is cut to it. Raise it,
84
+ * and any executor deadline with it, only for clients whose own limit is raised.
80
85
  */
81
86
  watchdogMs: import("./config-schema.js").Field<number, number, true>;
82
87
  /**
@@ -92,6 +97,12 @@ declare const execute: {
92
97
  * model step. From 4_000 to 1_000_000.
93
98
  */
94
99
  maxResultChars: import("./config-schema.js").Field<number, number, true>;
100
+ /**
101
+ * `false` stops listing `execute_read_code` on writable human endpoints. A
102
+ * read program runs unprompted on hosts that auto-approve reads, so a deployment
103
+ * that wants a person to see every program can turn it off. Default true.
104
+ */
105
+ readRoute: import("./config-schema.js").Field<boolean, boolean, true>;
95
106
  };
96
107
  declare const codeAdmission: {
97
108
  /** Simultaneous work admitted to this pool. */
@@ -271,8 +282,11 @@ declare const connectaConfig: {
271
282
  readonly executeCode: boolean | undefined;
272
283
  }>> | undefined, true>;
273
284
  /** Exact connector id -> tool name -> verdict. Hidden tools cannot be discovered or called.
285
+ * `"read"` applies only where the source is silent, and asserts the call is safe to repeat; a
286
+ * source's declared write stays a write and is reported. `"read-repeatable"` also serves a
287
+ * declared write as a read: the deployment has checked it and accepts that hosts may repeat it.
274
288
  * Unknown static names fail construction; absent runtime names stay dormant and are reported. */
275
- classification: import("./config-schema.js").Field<Record<string, Record<string, "read" | "write" | "hidden">>, Readonly<Record<string, Readonly<Record<string, "read" | "write" | "hidden">>>> | undefined, true>;
289
+ classification: import("./config-schema.js").Field<Record<string, Record<string, ClassificationOverride>>, Readonly<Record<string, Readonly<Record<string, ClassificationOverride>>>> | undefined, true>;
276
290
  /**
277
291
  * The deployment's one store: `d1Storage(env.CONNECTA_DB)` from
278
292
  * `@zackbart/connecta/d1` on Workers, `sqliteStorage(path)` from
@@ -355,7 +369,8 @@ declare const connectaConfig: {
355
369
  /**
356
370
  * Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
357
371
  * `timeoutMs`. An explicit per-call value wins. Unset by default, when those
358
- * calls fall back to `execute.hostCallTimeoutMs`.
372
+ * calls fall back to `execute.hostCallTimeoutMs`. `execute.watchdogMs` caps
373
+ * every one of them.
359
374
  *
360
375
  * Each call makes one attempt. `connecta.call` inside `execute_code` uses
361
376
  * it too, ahead of `execute.hostCallTimeoutMs`; `connecta.result`,
@@ -394,6 +409,7 @@ declare const connectaConfig: {
394
409
  watchdogMs?: number | undefined;
395
410
  maxWrites?: number | undefined;
396
411
  maxResultChars?: number | undefined;
412
+ readRoute?: boolean | undefined;
397
413
  }, {
398
414
  readonly maxEmittedBytes: number;
399
415
  readonly maxEmittedBlocks: number;
@@ -402,6 +418,7 @@ declare const connectaConfig: {
402
418
  readonly watchdogMs: number;
403
419
  readonly maxWrites: number;
404
420
  readonly maxResultChars: number;
421
+ readonly readRoute: boolean;
405
422
  }, true> & {
406
423
  readonly shape: {
407
424
  /**
@@ -419,16 +436,19 @@ declare const connectaConfig: {
419
436
  * `connecta.read`, in milliseconds. Default 15_000. Discovery uses
420
437
  * `discovery.probeTimeoutMs`. `connecta.call` and the direct call tools use it only when
421
438
  * `calls.defaultTimeoutMs` is unset, and an explicit per-call `timeoutMs`
422
- * always wins. Raise it for providers whose legitimate calls run longer,
423
- * such as analytics queries.
439
+ * wins, up to `execute.watchdogMs`. Raise it for providers whose legitimate
440
+ * calls run longer, such as analytics queries.
424
441
  */
425
442
  hostCallTimeoutMs: import("./config-schema.js").Field<number, number, true>;
426
443
  /**
427
- * Hard ceiling on one execution, in milliseconds, enforced outside the
428
- * sandbox. Default 120_000, above both executors' own deadlines (QuickJS
429
- * 30_000, the Dynamic Worker 60_000), so it only ends a run whose executor
430
- * never settled; that run fails as unresponsive and its admission slot is
431
- * released. Keep it above any raised executor deadline.
444
+ * Ceiling on one `execute_code` or direct call response, in milliseconds,
445
+ * counted from the request's arrival and enforced outside the sandbox.
446
+ * Default 55_000: MCP clients commonly abandon a request at 60_000, and only
447
+ * a response sent before that reaches the agent as a typed result. It sits
448
+ * above both executors' own deadlines (QuickJS 30_000, the Dynamic Worker
449
+ * 50_000), so a run ends here only if its executor never settled, or if
450
+ * queueing spent its time. An explicit `timeoutMs` above it is cut to it. Raise it,
451
+ * and any executor deadline with it, only for clients whose own limit is raised.
432
452
  */
433
453
  watchdogMs: import("./config-schema.js").Field<number, number, true>;
434
454
  /**
@@ -444,6 +464,12 @@ declare const connectaConfig: {
444
464
  * model step. From 4_000 to 1_000_000.
445
465
  */
446
466
  maxResultChars: import("./config-schema.js").Field<number, number, true>;
467
+ /**
468
+ * `false` stops listing `execute_read_code` on writable human endpoints. A
469
+ * read program runs unprompted on hosts that auto-approve reads, so a deployment
470
+ * that wants a person to see every program can turn it off. Default true.
471
+ */
472
+ readRoute: import("./config-schema.js").Field<boolean, boolean, true>;
447
473
  };
448
474
  };
449
475
  /**
package/dist/config.js CHANGED
@@ -82,7 +82,8 @@ const calls = {
82
82
  /**
83
83
  * Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
84
84
  * `timeoutMs`. An explicit per-call value wins. Unset by default, when those
85
- * calls fall back to `execute.hostCallTimeoutMs`.
85
+ * calls fall back to `execute.hostCallTimeoutMs`. `execute.watchdogMs` caps
86
+ * every one of them.
86
87
  *
87
88
  * Each call makes one attempt. `connecta.call` inside `execute_code` uses
88
89
  * it too, ahead of `execute.hostCallTimeoutMs`; `connecta.result`,
@@ -118,16 +119,19 @@ const execute = {
118
119
  * `connecta.read`, in milliseconds. Default 15_000. Discovery uses
119
120
  * `discovery.probeTimeoutMs`. `connecta.call` and the direct call tools use it only when
120
121
  * `calls.defaultTimeoutMs` is unset, and an explicit per-call `timeoutMs`
121
- * always wins. Raise it for providers whose legitimate calls run longer,
122
- * such as analytics queries.
122
+ * wins, up to `execute.watchdogMs`. Raise it for providers whose legitimate
123
+ * calls run longer, such as analytics queries.
123
124
  */
124
125
  hostCallTimeoutMs: whole({ min: 1, default: D.execute.hostCallTimeoutMs }),
125
126
  /**
126
- * Hard ceiling on one execution, in milliseconds, enforced outside the
127
- * sandbox. Default 120_000, above both executors' own deadlines (QuickJS
128
- * 30_000, the Dynamic Worker 60_000), so it only ends a run whose executor
129
- * never settled; that run fails as unresponsive and its admission slot is
130
- * released. Keep it above any raised executor deadline.
127
+ * Ceiling on one `execute_code` or direct call response, in milliseconds,
128
+ * counted from the request's arrival and enforced outside the sandbox.
129
+ * Default 55_000: MCP clients commonly abandon a request at 60_000, and only
130
+ * a response sent before that reaches the agent as a typed result. It sits
131
+ * above both executors' own deadlines (QuickJS 30_000, the Dynamic Worker
132
+ * 50_000), so a run ends here only if its executor never settled, or if
133
+ * queueing spent its time. An explicit `timeoutMs` above it is cut to it. Raise it,
134
+ * and any executor deadline with it, only for clients whose own limit is raised.
131
135
  */
132
136
  watchdogMs: whole({ min: 1, default: D.execute.watchdogMs }),
133
137
  /**
@@ -143,6 +147,15 @@ const execute = {
143
147
  * model step. From 4_000 to 1_000_000.
144
148
  */
145
149
  maxResultChars: whole({ min: 4_000, max: 1_000_000, default: D.execute.maxResultChars }),
150
+ /**
151
+ * `false` stops listing `execute_read_code` on writable human endpoints. A
152
+ * read program runs unprompted on hosts that auto-approve reads, so a deployment
153
+ * that wants a person to see every program can turn it off. Default true.
154
+ */
155
+ readRoute: opaque({
156
+ check: assertFlag,
157
+ resolve: (value) => value ?? D.execute.readRoute,
158
+ }),
146
159
  };
147
160
  function admissionPool(defaults) {
148
161
  return {
@@ -359,11 +372,14 @@ const connectaConfig = {
359
372
  /** Named tool pools, each served at `/mcp/<name>` to its members only. */
360
373
  pools: record(object(pool)),
361
374
  /** Exact connector id -> tool name -> verdict. Hidden tools cannot be discovered or called.
375
+ * `"read"` applies only where the source is silent, and asserts the call is safe to repeat; a
376
+ * source's declared write stays a write and is reported. `"read-repeatable"` also serves a
377
+ * declared write as a read: the deployment has checked it and accepts that hosts may repeat it.
362
378
  * Unknown static names fail construction; absent runtime names stay dormant and are reported. */
363
379
  classification: record(record(opaque({
364
380
  check: (value, path) => {
365
- if (value !== "read" && value !== "write" && value !== "hidden") {
366
- throw new ConfigError(`${path} must be "read", "write", or "hidden".`);
381
+ if (value !== "read" && value !== "read-repeatable" && value !== "write" && value !== "hidden") {
382
+ throw new ConfigError(`${path} must be "read", "read-repeatable", "write", or "hidden".`);
367
383
  }
368
384
  },
369
385
  }))),