@zackbart/connecta 0.40.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 (195) hide show
  1. package/CHANGELOG.md +148 -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 +32 -5
  7. package/dist/activity.js +32 -6
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-observation.js +24 -2
  10. package/dist/agent-routes.js +33 -44
  11. package/dist/auth/clerk.d.ts +6 -1
  12. package/dist/auth/clerk.js +16 -0
  13. package/dist/auth/cloudflare-access.js +49 -7
  14. package/dist/auth/downstream-oauth/consent.js +79 -29
  15. package/dist/auth/downstream-oauth/grant-store.js +58 -7
  16. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  17. package/dist/auth/downstream-oauth/refresh-records.js +6 -5
  18. package/dist/auth/downstream-oauth.js +15 -4
  19. package/dist/auth/oauth-request-not-sent.js +4 -0
  20. package/dist/auth/static-oauth.js +77 -39
  21. package/dist/auth-elicitation.js +118 -61
  22. package/dist/authorization-handoff.js +36 -3
  23. package/dist/catalog-cache.js +352 -43
  24. package/dist/catalog-freshness.js +25 -0
  25. package/dist/catalog-service.js +408 -78
  26. package/dist/catalog.js +216 -62
  27. package/dist/config-defaults.d.ts +2 -1
  28. package/dist/config-defaults.js +4 -1
  29. package/dist/config.d.ts +53 -21
  30. package/dist/config.js +56 -17
  31. package/dist/connectors/remote-mcp/call.js +1 -1
  32. package/dist/connectors/remote-mcp/catalog.js +52 -28
  33. package/dist/connectors/remote-mcp/connection.js +113 -22
  34. package/dist/connectors/remote-mcp/oauth.js +2 -2
  35. package/dist/connectors/remote-mcp/resources.js +5 -2
  36. package/dist/connectors/remote-mcp/transport.js +24 -6
  37. package/dist/connectors/remote-mcp.js +14 -5
  38. package/dist/credential-contract.d.ts +3 -1
  39. package/dist/describe-config.d.ts +4 -2
  40. package/dist/describe-config.js +1 -0
  41. package/dist/discovery-health.js +26 -11
  42. package/dist/downstream-input.js +11 -2
  43. package/dist/errors.d.ts +60 -4
  44. package/dist/errors.js +88 -3
  45. package/dist/execute.js +491 -202
  46. package/dist/executor-admission.js +2 -0
  47. package/dist/executor-contract.js +6 -0
  48. package/dist/executor-result.js +3 -3
  49. package/dist/executors/quickjs-child.js +14 -5
  50. package/dist/executors/quickjs-runtime.d.ts +2 -0
  51. package/dist/executors/quickjs-runtime.js +37 -20
  52. package/dist/executors/quickjs.js +24 -7
  53. package/dist/guarded-transport.js +2 -0
  54. package/dist/guest-functions.js +5 -4
  55. package/dist/guest-runtime.js +37 -1
  56. package/dist/guest-types.d.ts +18 -0
  57. package/dist/http/client.js +14 -2
  58. package/dist/http/errors.js +9 -4
  59. package/dist/http/fields.js +17 -5
  60. package/dist/http/hatch.js +11 -3
  61. package/dist/http/node-transport.js +110 -4
  62. package/dist/http/oauth.js +17 -6
  63. package/dist/http/paging.js +1 -1
  64. package/dist/http/provider.js +2 -6
  65. package/dist/http/vendor.js +6 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.js +15 -21
  68. package/dist/invocation-failure.js +28 -0
  69. package/dist/invocation.js +167 -64
  70. package/dist/membership.d.ts +1 -1
  71. package/dist/meta-output.js +24 -18
  72. package/dist/meta-tool-names.d.ts +1 -1
  73. package/dist/meta-tool-names.js +1 -1
  74. package/dist/meta-tools.js +286 -73
  75. package/dist/module-contracts.d.ts +3 -1
  76. package/dist/native-recovery.js +4 -1
  77. package/dist/native-tools.js +177 -55
  78. package/dist/node.d.ts +7 -0
  79. package/dist/node.js +85 -4
  80. package/dist/oauth-handoff.js +53 -43
  81. package/dist/operation-activity.js +104 -0
  82. package/dist/operation-vocabulary.js +6 -0
  83. package/dist/operator-record.d.ts +9 -1
  84. package/dist/operator-record.js +16 -0
  85. package/dist/operator-ui/generated.js +2 -2
  86. package/dist/program-result.js +2 -0
  87. package/dist/program-source.js +1 -1
  88. package/dist/program-writes.js +112 -19
  89. package/dist/provider.d.ts +1 -14
  90. package/dist/provider.js +0 -3
  91. package/dist/providers/_shared/google/kit.js +2 -2
  92. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  93. package/dist/providers/breeze/api.js +2 -2
  94. package/dist/providers/breeze/index.js +1 -1
  95. package/dist/providers/breeze/skill.generated.js +1 -1
  96. package/dist/providers/ccb/api.js +2 -2
  97. package/dist/providers/ccb/index.js +1 -1
  98. package/dist/providers/ccb/skill.generated.js +1 -1
  99. package/dist/providers/cloudflare/api.js +2 -2
  100. package/dist/providers/cloudflare/index.js +3 -1
  101. package/dist/providers/cloudflare/skill.generated.js +1 -1
  102. package/dist/providers/docs/index.js +1 -1
  103. package/dist/providers/docs/skill.generated.js +1 -1
  104. package/dist/providers/drive/index.js +1 -1
  105. package/dist/providers/drive/skill.generated.js +1 -1
  106. package/dist/providers/forms/index.js +2 -1
  107. package/dist/providers/forms/skill.generated.js +1 -1
  108. package/dist/providers/github/api.js +98 -6
  109. package/dist/providers/github/index.js +8 -3
  110. package/dist/providers/github/lib/auth.d.ts +3 -1
  111. package/dist/providers/github/lib/auth.js +43 -12
  112. package/dist/providers/github/lib/discovery.js +11 -1
  113. package/dist/providers/github/lib/operations.js +8 -0
  114. package/dist/providers/github/safety.js +39 -0
  115. package/dist/providers/github/skill.generated.js +1 -1
  116. package/dist/providers/github/tools.js +12 -5
  117. package/dist/providers/gmail/index.js +2 -1
  118. package/dist/providers/gmail/skill.generated.js +1 -1
  119. package/dist/providers/granola/index.js +1 -1
  120. package/dist/providers/granola/skill.generated.js +1 -1
  121. package/dist/providers/overflow/index.js +3 -1
  122. package/dist/providers/overflow/skill.generated.js +1 -1
  123. package/dist/providers/planning-center/index.js +12 -6
  124. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  125. package/dist/providers/planning-center/skill.generated.js +1 -1
  126. package/dist/providers/quickbooks/api.js +28 -25
  127. package/dist/providers/quickbooks/index.js +3 -6
  128. package/dist/providers/quickbooks/lib/writes.js +55 -0
  129. package/dist/providers/quickbooks/safety.js +17 -5
  130. package/dist/providers/quickbooks/skill.generated.js +1 -1
  131. package/dist/providers/sheets/index.js +2 -1
  132. package/dist/providers/sheets/skill.generated.js +1 -1
  133. package/dist/providers/slides/index.js +3 -3
  134. package/dist/providers/slides/skill.generated.js +1 -1
  135. package/dist/providers/stripe/index.js +2 -2
  136. package/dist/providers/stripe/skill.generated.js +1 -1
  137. package/dist/providers/tithely/index.js +2 -1
  138. package/dist/providers/tithely/skill.generated.js +1 -1
  139. package/dist/providers/vercel/api.js +3 -2
  140. package/dist/providers/vercel/skill.generated.js +1 -1
  141. package/dist/registry.d.ts +36 -4
  142. package/dist/registry.js +142 -30
  143. package/dist/resource-read.js +21 -2
  144. package/dist/result-preview.js +210 -0
  145. package/dist/result-seal.js +168 -0
  146. package/dist/route-names.js +53 -0
  147. package/dist/routes/activity-context.js +1 -0
  148. package/dist/routes/activity-disclosure.js +26 -4
  149. package/dist/routes/activity.js +2 -1
  150. package/dist/routes/connect.js +21 -13
  151. package/dist/routes/mcp.js +83 -35
  152. package/dist/routes/shared.d.ts +6 -0
  153. package/dist/routes/ui-config.js +1 -1
  154. package/dist/routes/ui-data.js +3 -2
  155. package/dist/runtime/call.js +15 -10
  156. package/dist/runtime/native-storage.js +12 -0
  157. package/dist/runtime/request-reads.js +66 -14
  158. package/dist/runtime/request-scope.js +38 -2
  159. package/dist/runtime/run.js +4 -1
  160. package/dist/sent-secrets.d.ts +20 -0
  161. package/dist/sent-secrets.js +49 -1
  162. package/dist/skill-origin.js +16 -0
  163. package/dist/skills.js +213 -57
  164. package/dist/storage/d1-driver.js +33 -5
  165. package/dist/storage/keys.js +16 -7
  166. package/dist/storage/sql-native.js +60 -3
  167. package/dist/storage/sql.js +6 -0
  168. package/dist/timing-trace.js +84 -0
  169. package/dist/tool-safety.d.ts +33 -0
  170. package/dist/tool-safety.js +51 -15
  171. package/dist/types.d.ts +36 -0
  172. package/dist/url-safety.js +38 -20
  173. package/dist/usage-guide.js +14 -13
  174. package/dist/utility-recovery.js +33 -0
  175. package/dist/validate.js +29 -10
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/dist/worker.js +60 -15
  179. package/documentation/architecture.md +152 -43
  180. package/documentation/auth.md +160 -53
  181. package/documentation/code-mode.md +396 -119
  182. package/documentation/deploying.md +35 -10
  183. package/documentation/hosted-mcp-migration.md +9 -3
  184. package/documentation/integrating.md +27 -5
  185. package/documentation/meta-tools.md +761 -157
  186. package/documentation/operating.md +91 -4
  187. package/documentation/operator-ui.md +51 -2
  188. package/documentation/providers.md +34 -20
  189. package/examples/worker/README.md +3 -3
  190. package/guides/how-it-works.md +2 -1
  191. package/guides/safety-and-trust.md +1 -3
  192. package/package.json +10 -8
  193. package/templates/node/AGENTS.md +3 -3
  194. package/templates/node/README.md +1 -1
  195. 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,39 +188,98 @@ 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
- function indexFor(tools) {
168
- let index = searchIndexes.get(tools);
169
- if (!index) {
170
- const nameTokenDocuments = new Map();
171
- const descriptionTokenDocuments = new Map();
172
- const addTokens = (target, tokens, tool) => {
173
- for (const token of new Set(tokens)) {
174
- const documents = target.get(token) ?? [];
175
- documents.push(tool);
176
- target.set(token, documents);
177
- }
193
+ // Only immutable token strings and numeric positions cross requests. The key
194
+ // holds exactly what buildIndex reads, in order, so schemas cost nothing here;
195
+ // it avoids hash collisions and charges its UTF-16 storage as well.
196
+ const retainedIndexes = new Map();
197
+ const INDEX_BYTES = 8 * 1024 * 1024;
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
+ }
204
+ function buildIndex(tools) {
205
+ const postings = ZONES.map(() => new Map());
206
+ const documents = tools.map((tool, position) => {
207
+ const description = tool.description ?? "";
208
+ const nameTokens = lexicalTokens(tool.name);
209
+ const summary = new Set(lexicalTokens(discoverySummary(description).text));
210
+ const doc = {
211
+ name: nameTokens.join(" "),
212
+ nameTokens,
213
+ titleTokens: [...new Set(lexicalTokens(toolTitle(tool)))],
214
+ summaryTokens: [...summary],
215
+ tailTokens: [...new Set(lexicalTokens(description))].filter((token) => !summary.has(token)),
178
216
  };
179
- const documents = tools.map((tool) => {
180
- const nameTokens = lexicalTokens(tool.name);
181
- const descriptionTokens = lexicalTokens(tool.description ?? "");
182
- addTokens(nameTokenDocuments, nameTokens, tool);
183
- addTokens(descriptionTokenDocuments, descriptionTokens, tool);
184
- return {
185
- tool,
186
- name: nameTokens.join(" "),
187
- nameTokens,
188
- descriptionTokens,
189
- };
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
+ }
190
224
  });
191
- index = {
192
- documents,
193
- nameTokenDocuments,
194
- descriptionTokenDocuments,
195
- };
196
- searchIndexes.set(tools, index);
225
+ return Object.freeze(doc);
226
+ });
227
+ for (const map of postings)
228
+ for (const positions of map.values())
229
+ Object.freeze(positions);
230
+ // Maps remain private to this module and are never mutated after construction.
231
+ return Object.freeze({ documents: Object.freeze(documents), postings: Object.freeze(postings) });
232
+ }
233
+ function bindIndex(tools, index) {
234
+ const view = { index, tools: new Map(tools.map((tool, position) => [position, tool])) };
235
+ searchIndexes.set(tools, view);
236
+ return view;
237
+ }
238
+ /** Only call after complete catalog acceptance, redaction and classification. */
239
+ export function prepareLexicalCatalog(tools, partition) {
240
+ if (searchIndexes.has(tools))
241
+ return;
242
+ const key = JSON.stringify([partition, tools.map((tool) => [tool.name, tool.description ?? "", toolTitle(tool)])]);
243
+ const retained = retainedIndexes.get(key);
244
+ if (retained) {
245
+ bindIndex(tools, retained.index);
246
+ return;
247
+ }
248
+ const index = buildIndex(tools);
249
+ bindIndex(tools, index);
250
+ let bytes = key.length * 2 + 128;
251
+ for (const doc of index.documents) {
252
+ bytes += 128 + doc.name.length * 2;
253
+ for (const zone of ZONES)
254
+ for (const token of doc[zone])
255
+ bytes += 32 + token.length * 2;
256
+ }
257
+ for (const map of index.postings)
258
+ for (const [token, positions] of map)
259
+ bytes += 96 + token.length * 2 + positions.length * 8;
260
+ if (bytes > INDEX_BYTES)
261
+ return;
262
+ while (retainedIndexes.size >= 4 || retainedBytes + bytes > INDEX_BYTES) {
263
+ const oldest = retainedIndexes.keys().next().value;
264
+ retainedBytes -= retainedIndexes.get(oldest).bytes;
265
+ retainedIndexes.delete(oldest);
197
266
  }
198
- return index;
267
+ retainedIndexes.set(key, { index, bytes });
268
+ retainedBytes += bytes;
269
+ }
270
+ /** Project immutable positions onto this request's currently visible objects. */
271
+ export function carryLexicalCatalog(source, visible) {
272
+ const view = searchIndexes.get(source);
273
+ if (!view || source === visible)
274
+ return;
275
+ const allowed = new Set(visible);
276
+ searchIndexes.set(visible, {
277
+ index: view.index,
278
+ tools: new Map([...view.tools].filter(([, tool]) => allowed.has(tool))),
279
+ });
280
+ }
281
+ function indexFor(tools) {
282
+ return searchIndexes.get(tools) ?? bindIndex(tools, buildIndex(tools));
199
283
  }
200
284
  /**
201
285
  * Whole-token equality is the ordinary lexical match. A deliberately narrow
@@ -212,6 +296,24 @@ function inflectionVariants(base) {
212
296
  ...(base.endsWith("y") ? [`${base.slice(0, -1)}ies`, `${base.slice(0, -1)}ied`] : []),
213
297
  ];
214
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
+ ]));
215
317
  function matchingTokenCandidates(term) {
216
318
  const candidates = new Set([term, ...inflectionVariants(term)]);
217
319
  const possibleBases = [
@@ -227,6 +329,12 @@ function matchingTokenCandidates(term) {
227
329
  candidates.add(base);
228
330
  }
229
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
+ }
230
338
  return candidates;
231
339
  }
232
340
  /**
@@ -279,26 +387,38 @@ export function lexicalCorpusStatistics(toolSets, query, synonymsFor) {
279
387
  documentFrequency: new Map(),
280
388
  nameMatches: new Map(),
281
389
  descriptionMatches: new Map(),
390
+ headMatches: new Map(),
282
391
  };
283
392
  }
284
393
  const nameMatches = new Map();
285
394
  const descriptionMatches = new Map();
395
+ const headMatches = new Map();
286
396
  for (const term of terms) {
287
397
  const termNameMatches = new Set();
288
398
  const termDescriptionMatches = new Set();
399
+ const termHeadMatches = new Set();
289
400
  for (const tools of toolSets) {
290
401
  const index = indexFor(tools);
291
402
  for (const candidate of synonymCandidates(term, synonymsFor?.(tools))) {
292
- for (const tool of index.nameTokenDocuments.get(candidate) ?? []) {
293
- termNameMatches.add(tool);
294
- }
295
- for (const tool of index.descriptionTokenDocuments.get(candidate) ?? []) {
296
- termDescriptionMatches.add(tool);
297
- }
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
+ });
298
417
  }
299
418
  }
300
419
  nameMatches.set(term, termNameMatches);
301
420
  descriptionMatches.set(term, termDescriptionMatches);
421
+ headMatches.set(term, termHeadMatches);
302
422
  }
303
423
  const documentFrequency = new Map(terms.map((term) => [
304
424
  term,
@@ -309,15 +429,20 @@ export function lexicalCorpusStatistics(toolSets, query, synonymsFor) {
309
429
  documentFrequency,
310
430
  nameMatches,
311
431
  descriptionMatches,
432
+ headMatches,
312
433
  };
313
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
+ }
314
439
  function inverseDocumentFrequency(term, statistics) {
315
440
  const frequency = statistics.documentFrequency.get(term) ?? 0;
316
441
  return Math.log(1 + (statistics.documentCount - frequency + 0.5) / (frequency + 0.5));
317
442
  }
318
443
  function scoreDocument(doc, phrase, terms, mode, statistics) {
319
444
  if (!phrase)
320
- return { score: 0, matchedTermCount: 0 };
445
+ return { score: 0, matchedTermCount: 0, headTerms: [], nameTerms: [], namesDistinctiveTerm: false };
321
446
  const matchedTerms = terms.filter((term) => statistics.nameMatches.get(term)?.has(doc.tool) || statistics.descriptionMatches.get(term)?.has(doc.tool));
322
447
  if (mode === "all" && matchedTerms.length !== terms.length)
323
448
  return null;
@@ -325,8 +450,10 @@ function scoreDocument(doc, phrase, terms, mode, statistics) {
325
450
  return null;
326
451
  // Coverage remains meaningful in partial mode, but is IDF-weighted rather
327
452
  // than a raw term count: one rare domain term can beat several ubiquitous
328
- // catalog verbs.
329
- 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);
330
457
  const phraseWeight = terms.reduce((total, term) => total + inverseDocumentFrequency(term, statistics), 0);
331
458
  if (doc.name === phrase)
332
459
  score += 40 * phraseWeight;
@@ -342,13 +469,31 @@ function scoreDocument(doc, phrase, terms, mode, statistics) {
342
469
  else if (statistics.nameMatches.get(term)?.has(doc.tool)) {
343
470
  score += 8 * weight;
344
471
  }
345
- 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))
346
474
  score += 3 * weight;
347
- 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))
348
480
  score += 1.5 * weight;
349
- }
481
+ else if (statistics.descriptionMatches.get(term)?.has(doc.tool))
482
+ score += 0.25 * weight;
350
483
  }
351
- 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
+ };
352
497
  }
353
498
  function queryContainsExactName(doc, phrase) {
354
499
  if (!doc.name || !phrase)
@@ -365,15 +510,24 @@ export function rankTools(tools, query, mode = "all", statistics = lexicalCorpus
365
510
  const exactNamePhrase = normalized(exactNameQuery);
366
511
  const terms = [...new Set(phrase.split(/\s+/).filter(Boolean))];
367
512
  const ranked = [];
368
- indexFor(tools).documents.forEach((doc, order) => {
369
- const scored = scoreDocument(doc, phrase, terms, mode, statistics);
513
+ const view = indexFor(tools);
514
+ let order = 0;
515
+ view.index.documents.forEach((doc, position) => {
516
+ const tool = view.tools.get(position);
517
+ if (!tool)
518
+ return;
519
+ const visibleOrder = order++;
520
+ const scored = scoreDocument({ ...doc, tool }, phrase, terms, mode, statistics);
370
521
  if (scored !== null) {
371
522
  ranked.push({
372
- tool: doc.tool,
523
+ tool,
373
524
  score: scored.score,
374
- order,
525
+ order: visibleOrder,
375
526
  exactName: queryContainsExactName(doc, exactNamePhrase),
376
527
  matchedTermCount: scored.matchedTermCount,
528
+ headTerms: scored.headTerms,
529
+ nameTerms: scored.nameTerms,
530
+ namesDistinctiveTerm: scored.namesDistinctiveTerm,
377
531
  });
378
532
  }
379
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. */
@@ -211,7 +222,7 @@ declare const pool: {
211
222
  * What this pool is for, in plain words for the model: appended to the MCP
212
223
  * `initialize` instructions at `/mcp/<name>` and `/agent/<name>`, so a
213
224
  * caller learns its scope ("Team tier: shared tools only."). Trimmed, 1 to
214
- * 500 characters, no line breaks or control characters. It grants and hides nothing; plain
225
+ * 500 UTF-16 code units, no line breaks or control characters. It grants and hides nothing; plain
215
226
  * `/mcp` never shows it.
216
227
  */
217
228
  description: import("./config-schema.js").Field<string, string | undefined, true>;
@@ -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
@@ -283,8 +297,9 @@ declare const connectaConfig: {
283
297
  */
284
298
  storage: import("./config-schema.js").Field<KVStorage, KVStorage, true>;
285
299
  /**
286
- * Public base URL. Defaults to the request origin per-request. Configuring an
287
- * HTTPS URL also redirects matching inbound HTTP requests to HTTPS.
300
+ * Public base URL. Defaults to the request origin per-request, except that
301
+ * clerkAuth requires it here or as its own `publicUrl`. Configuring an HTTPS
302
+ * URL also redirects matching inbound HTTP requests to HTTPS.
288
303
  */
289
304
  publicUrl: import("./config-schema.js").Field<string, string | undefined, true>;
290
305
  /**
@@ -294,7 +309,10 @@ declare const connectaConfig: {
294
309
  allowedOrigins: import("./config-schema.js").Field<readonly string[] | "*", readonly string[] | "*" | undefined, true>;
295
310
  /** Optional recorder and reader, created by activityHistory() from /activity. */
296
311
  activity: import("./config-schema.js").Field<ActivityModule, ActivityModule | undefined, true>;
297
- /** Replaceable owner-partitioned credential storage. Omit for config-owned secrets. */
312
+ /**
313
+ * Replaceable owner-partitioned credential storage. Omit for config-owned
314
+ * secrets. With any OAuth connector it must implement `seal` and `open`.
315
+ */
298
316
  vault: import("./config-schema.js").Field<CredentialVault, CredentialVault | undefined, true>;
299
317
  /** Optional connection UI, created by operatorUi() from /ui. */
300
318
  ui: import("./config-schema.js").Field<OperatorSurface, OperatorSurface | undefined, true>;
@@ -351,7 +369,8 @@ declare const connectaConfig: {
351
369
  /**
352
370
  * Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
353
371
  * `timeoutMs`. An explicit per-call value wins. Unset by default, when those
354
- * calls fall back to `execute.hostCallTimeoutMs`.
372
+ * calls fall back to `execute.hostCallTimeoutMs`. `execute.watchdogMs` caps
373
+ * every one of them.
355
374
  *
356
375
  * Each call makes one attempt. `connecta.call` inside `execute_code` uses
357
376
  * it too, ahead of `execute.hostCallTimeoutMs`; `connecta.result`,
@@ -390,6 +409,7 @@ declare const connectaConfig: {
390
409
  watchdogMs?: number | undefined;
391
410
  maxWrites?: number | undefined;
392
411
  maxResultChars?: number | undefined;
412
+ readRoute?: boolean | undefined;
393
413
  }, {
394
414
  readonly maxEmittedBytes: number;
395
415
  readonly maxEmittedBlocks: number;
@@ -398,6 +418,7 @@ declare const connectaConfig: {
398
418
  readonly watchdogMs: number;
399
419
  readonly maxWrites: number;
400
420
  readonly maxResultChars: number;
421
+ readonly readRoute: boolean;
401
422
  }, true> & {
402
423
  readonly shape: {
403
424
  /**
@@ -415,16 +436,19 @@ declare const connectaConfig: {
415
436
  * `connecta.read`, in milliseconds. Default 15_000. Discovery uses
416
437
  * `discovery.probeTimeoutMs`. `connecta.call` and the direct call tools use it only when
417
438
  * `calls.defaultTimeoutMs` is unset, and an explicit per-call `timeoutMs`
418
- * always wins. Raise it for providers whose legitimate calls run longer,
419
- * such as analytics queries.
439
+ * wins, up to `execute.watchdogMs`. Raise it for providers whose legitimate
440
+ * calls run longer, such as analytics queries.
420
441
  */
421
442
  hostCallTimeoutMs: import("./config-schema.js").Field<number, number, true>;
422
443
  /**
423
- * Hard ceiling on one execution, in milliseconds, enforced outside the
424
- * sandbox. Default 120_000, above both executors' own deadlines (QuickJS
425
- * 30_000, the Dynamic Worker 60_000), so it only ends a run whose executor
426
- * never settled; that run fails as unresponsive and its admission slot is
427
- * 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.
428
452
  */
429
453
  watchdogMs: import("./config-schema.js").Field<number, number, true>;
430
454
  /**
@@ -440,6 +464,12 @@ declare const connectaConfig: {
440
464
  * model step. From 4_000 to 1_000_000.
441
465
  */
442
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>;
443
473
  };
444
474
  };
445
475
  /**
@@ -542,6 +572,8 @@ declare const connectaConfig: {
542
572
  };
543
573
  };
544
574
  };
575
+ /** Payload-free request timing logs; 0 disables tracing, 1 samples every MCP request. */
576
+ timingTraceSampleRate: import("./config-schema.js").Field<number, number, true>;
545
577
  /** Diagnostic output. Use "silent" to disable all diagnostic logging. */
546
578
  logger: import("./config-schema.js").Field<Logger | "silent", Logger, true>;
547
579
  serverInfo: import("./config-schema.js").Field<{