@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
@@ -18,29 +18,32 @@ Start with [Operating an endpoint](./operating.md) for a workflow, or
18
18
  [the agent index](./README.md) for setup and repository tasks. This reference
19
19
  owns the top-level MCP contract; it does not require reading the core internals.
20
20
 
21
- | Task | Sections |
22
- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
23
- | Choose a tool or construct its arguments | [Six-tool table](#the-six-tools), [call routing](#routing-between-the-call-surfaces), [downstream input](#downstream-input-on-direct-calls) |
24
- | Find an operation and its schema | [Discovery context](#discovery-context), [TypeScript signatures](#typescript-signatures), [lexical discovery](#lexical-discovery) |
25
- | Load provider instructions or native skills | [Connector guides](#connector-guides-and-skills), [native extension](#native-skills-extension) |
26
- | Interpret a direct response or page a large result | [Representation](#result-representation), [truncation](#truncated-direct-call-results), [paging](#paging-with-connectaresult) |
27
- | Recover a refused or invalid call | [Authorization](#authorization-recovery), [routing](#routing-recovery), [arguments](#argument-recovery), [echo budgets](#echo-budgets) |
28
- | List downstream tools directly, or connect Eve | [Native tool surface](#native-tool-surface), [Eve agents](#eve-agents) |
29
- | Change the contract | [Source and tests](#source-and-tests), [guest API](./code-mode.md#verification) |
21
+ | Task | Sections |
22
+ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | Choose a tool or construct its arguments | [Meta-tool table](#the-meta-tools), [call routing](#routing-between-the-call-surfaces), [downstream input](#downstream-input-on-direct-calls) |
24
+ | Find an operation and its schema | [Discovery context](#discovery-context), [TypeScript signatures](#typescript-signatures), [lexical discovery](#lexical-discovery) |
25
+ | Load provider instructions or native skills | [Connector guides](#connector-guides-and-skills), [native extension](#native-skills-extension) |
26
+ | Interpret a direct response or page a large result | [Representation](#result-representation), [truncation](#truncated-direct-call-results), [paging](#paging-with-connectaresult) |
27
+ | Recover a refused or invalid call | [Authorization](#authorization-recovery), [routing](#routing-recovery), [arguments](#argument-recovery), [echo budgets](#echo-budgets) |
28
+ | List downstream tools directly, or connect Eve | [Native tool surface](#native-tool-surface), [Eve agents](#eve-agents) |
29
+ | Read an endpoint's surface and connector roster | [Endpoint facts](#endpoint-facts) |
30
+ | Change the contract | [Source and tests](#source-and-tests), [guest API](./code-mode.md#verification) |
30
31
 
31
32
  ## Tool presentation
32
33
 
33
- The default surface is the six meta-tools below. From 0.35.0, pools can use
34
+ The default surface is the meta-tools below. From 0.35.0, pools can use
34
35
  [native tool presentation](#native-tool-surface) for clients with their own
35
36
  tool search. Both presentations use the same invocation enforcement; native
36
37
  pools keep code available by default.
37
38
 
38
- ## The six tools
39
+ ## The meta-tools
39
40
 
40
- Every deployment requires an executor. A meta endpoint's `tools/list` is
41
- exactly six. This is the default; a pool may instead use the
42
- [native tool surface](#native-tool-surface). No other configuration adds a
43
- seventh or removes one, so a client's cached meta-tool list never depends on
41
+ Every deployment requires an executor. A meta endpoint's `tools/list` is exactly
42
+ these six, plus `execute_read_code` on a writable `/mcp` or `/mcp/<pool>` unless
43
+ `execute.readRoute` is `false` ([read programs](#read-programs)). A read-only
44
+ endpoint and `/agent/<pool>` list six. This is the default; a pool may instead use the
45
+ [native tool surface](#native-tool-surface). No other configuration changes the
46
+ list, so a client's cached meta-tool list never depends on
44
47
  the storage behind the deployment. `execute_code`
45
48
  is annotated `readOnlyHint: true, destructiveHint: false` on a read-only
46
49
  endpoint, and `readOnlyHint: false, destructiveHint: true` on a writable one.
@@ -48,13 +51,20 @@ Approval belongs to the client. A read-only endpoint keeps all six tools, omits
48
51
  write tools from `search_tools` results and describe, and refuses every write
49
52
  call. Connector `readOnly` applies the same restriction on every endpoint; effective read-only is endpoint OR connector. Neither setting changes connector visibility or read grants. Guides
50
53
  (`queryAnalysis.guideSummary`, `skills`, `connecta.skill`, skill
51
- `resources/read`) are documentation and may name tools the endpoint refuses. The four tools with structured-only success results
52
- advertise `outputSchema`. `search_tools`'s schema defines its typed failure,
53
- including every authorization-handoff field, once under `$defs.failure`;
54
- `catalogErrors[]` references it through `allOf` with `connector` required, and
55
- `queryAnalysis.catalogError` references it directly. The schema declares no
56
- `$schema`, and validators for drafts 4, 7, 2019-09 and 2020-12 accept the same
57
- pages. The two direct-call tools omit it because their default
54
+ `resources/read`) are documentation and may name tools the endpoint refuses. The three tools with structured-only success results
55
+ (`execute_code`, `search_tools`, `authorize_connector`) advertise an open
56
+ `outputSchema`: required keys and top-level types, with every other field open.
57
+ The schemas declare no `$schema`, and validators for drafts 4, 7, 2019-09 and
58
+ 2020-12 accept the same results. The server checks each successful result against
59
+ its full contract before sending it, so the shapes this guide states hold though
60
+ `tools/list` omits them. Both SDK edges exempt `isError: true` responses from
61
+ output-schema validation. Failures on a `search_tools` page are one such shape: each
62
+ `catalogErrors[]` entry and `queryAnalysis.catalogError` requires `code`, `message`
63
+ and `retryable`, may carry `retryAfterMs`, `retry`, `nextAction`, `stale`, `ageMs`
64
+ and the authorization-handoff fields (`recovery`, `status`, `authorizationUrl`,
65
+ `operatorUrl`, `instructions`, `credential`), and has no others; a `catalogErrors[]`
66
+ entry also requires `connector`, which `queryAnalysis.catalogError` may carry.
67
+ The two direct-call tools omit `outputSchema` because their default
58
68
  mode returns native `content`. Default-mode successes declare
59
69
  `_meta["dev.connecta/format"]: "json" | "text"`; value mode places the value in
60
70
  `data` and declares `format` in its complete result envelope.
@@ -62,6 +72,7 @@ mode returns native `content`. Default-mode successes declare
62
72
  | Tool | Arguments | Returns |
63
73
  | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
64
74
  | `execute_code` | `code`, `diagnostics?` | `{ result?, error?, hostCalls }`, plus `logs`, `emitted`, and `emittedDiscarded` when present and diagnostics when asked |
75
+ | `execute_read_code` | the same | the same; [read programs](#read-programs) lists where it appears and what it refuses |
65
76
  | `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json" \| "typescript"` | `{ catalogErrors, tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remain, `absence` when the query names a service with no configured connector, and `queryAnalysis` on a partial, failed, unsearchable, or absent-service search |
66
77
  | `call_tool` | `address`, `args?`, `resultMode?: "mcp" \| "value"`, `timeoutMs?`, `diagnostics?` | the downstream result, bounded as [result representation](#result-representation) describes |
67
78
  | `call_destructive_tool` | the same, plus `reason?` | the same |
@@ -96,10 +107,65 @@ endpoint, the `call_tool`, `call_destructive_tool`, and `search_tools`
96
107
  descriptions say so too; `call_destructive_tool` still runs reads there. `call_destructive_tool.reason` is dropped before
97
108
  anything runs.
98
109
 
110
+ ## Read programs
111
+
112
+ `execute_read_code` runs a program with the same guest API, sandbox and budgets as
113
+ `execute_code`, annotated `readOnlyHint: true, destructiveHint: false`, so a host
114
+ that gates on annotations does not prompt for a read-only program. On a writable
115
+ endpoint `execute_code` stays annotated as a write.
116
+
117
+ | Endpoint | Lists `execute_read_code` |
118
+ | --------------------------------------- | --------------------------------------------- |
119
+ | Writable `/mcp`, writable `/mcp/<pool>` | Yes, unless `execute.readRoute: false` |
120
+ | Read-only endpoint | No: `execute_code` is already read-only there |
121
+ | `/agent/<pool>` | No, for now |
122
+ | Native pool | No |
123
+
124
+ **Refusal.** Every call whose effective or source class is write is refused below
125
+ the sandbox, before validation and dispatch, with `retryable: false` and a message
126
+ naming `call_destructive_tool` and `execute_code`. Its code is
127
+ `destructive_tool_requires_approval`, or `read_only_endpoint` for a read-only
128
+ connector, which has no write route. A program can read and then attempt a write; it
129
+ gets that refusal with zero writes sent, and catching it or using
130
+ `Promise.allSettled` changes nothing. A plain `"read"` override does not bring a source
131
+ write here: a tool it serves as a read over a silent source is refused, though `call_tool`
132
+ runs it. `"read-repeatable"` is the exception, because the deployment accepted that a host
133
+ may repeat the call, and a host can resend a whole read program: such a tool runs here
134
+ unmarked, and a resent program can reach the vendor twice.
135
+
136
+ **Discovery lists writes, marked.** `connecta.search` and `connecta.describe` return
137
+ them with `callable: false`, and no other entry carries the field. Writes are not
138
+ omitted because omission reads as absence, and a program that reduces a page keeps
139
+ the marker on the entry it selects where it would drop a page-level note.
140
+ `search_tools` remains the unprompted way to find a write's address outside a program.
141
+
142
+ **Handles.** `connecta.result` works with the existing stash bindings: a `resultId`
143
+ from a direct read or a read program pages, or reads whole with `{ all: true }`, from a
144
+ read program on the same endpoint.
145
+ Follow-up text from `call_tool`, `search_tools` and both program routes names this route
146
+ on endpoints that list it, including paging a writing program's oversized return, so
147
+ paging costs no write approval and cannot resend the write.
148
+
149
+ **Off switch.** `execute: { readRoute: false }` in deployment code stops listing it
150
+ on every writable endpoint. It is the deployment's choice alone, and suits a deployment
151
+ that wants a person to see every program, given the limit below.
152
+
153
+ **Known limit: a read program can still send data out through a read.** A read program runs
154
+ without a prompt on hosts that auto-approve reads. Sandboxes deny egress, but a read
155
+ tool that takes a URL or a query can carry data out in its arguments, so data a program
156
+ reads can instruct an agent to do that. Connecta enforces by classification and cannot
157
+ see intent. `call_tool` has the same exposure one call at a time; a program adds
158
+ loops and joins. Keep such tools out of the pool, or set them to `"write"` or `"hidden"`
159
+ in `classification`, or turn this route off. The `read-route-injection` eval task
160
+ measures whether agents follow such an instruction.
161
+
162
+ Activity records calls and programs on this route with source `execute_read_code`.
163
+
99
164
  ## Routing between the call surfaces
100
165
 
101
166
  The route is chosen before discovery, and read-only work has exactly two:
102
- `call_tool` for one known address, `execute_code` for everything wider — an
167
+ `call_tool` for one known address, a program for everything wider —
168
+ `execute_read_code` where it is listed, otherwise `execute_code` — an
103
169
  unknown address, a result that will be reduced, a call whose arguments depend on
104
170
  an earlier result, or several operations. A program keeps discovery, calls, and
105
171
  reduction together when the schemas and result shapes suffice, and gives each
@@ -128,7 +194,8 @@ The agent reports the limitation and can ask the operator for a writable connect
128
194
  never pauses, replays, or approves a program.
129
195
 
130
196
  Both search routes return the flat page specified under
131
- [lexical discovery](#lexical-discovery), using the catalog's shared output schema.
197
+ [lexical discovery](#lexical-discovery), which the server validates against one
198
+ catalog page contract.
132
199
  Describe returns an ordered tool list without pagination. Optional row metadata
133
200
  is omitted in this example:
134
201
 
@@ -141,21 +208,28 @@ Live connector probing is not a fourth: the operator pages and `/health` own it.
141
208
 
142
209
  ## Discovery context
143
210
 
144
- The deployment-derived `execute_code` description carries a connector
145
- inventory before any catalog search — registry order and each canonical id,
211
+ The deployment-derived `execute_code` and `execute_read_code` descriptions carry
212
+ one connector inventory before any catalog search — registry order and each canonical id,
146
213
  with no second name minted for programs. An entry adds the configured title
147
214
  only when it says more than the id: a title whose words the id already spells,
148
- such as `Google Calendar` for `google_calendar`, is dropped. Titles normalize
149
- whitespace and cap at 48 UTF-8 bytes. A connector with a guide says how to
150
- fetch it: `read first: skills({name:"connector:<id>"})` for a required guide,
151
- `guide: skills({name:"connector:<id>"})` otherwise. For example:
152
- `Connectors: crm (Customer CRM; read first: skills({name:"connector:crm"})), tickets.`
153
-
154
- The complete line caps at 1,024 UTF-8 bytes, which names every connector of a
155
- typical deployment. Past the cap, detail degrades before names do: titles are
156
- dropped first, then optional guide pointers. Only when bare ids and required
157
- guide pointers still exceed it do whole entries give way, in registry order, to
158
- an exact `+N more` count. The inventory reads only the configured registry: no
215
+ such as `Google Calendar` for `google_calendar`, is dropped. It then adds the
216
+ connector's configured description as a one-line purpose, unless that too only
217
+ respells the id and title. Titles and purposes normalize whitespace; titles cap
218
+ at 48 UTF-8 bytes and purposes at 60, clipping on a whole token; text with no token
219
+ that fits is omitted. A connector whose guide is required
220
+ carries a `*`, and one legend follows when an entry shown has the mark. Optional
221
+ guides are not named here; search and describe name them. For example:
222
+ `Connectors: crm* (Customer CRM; Current accounts and contracts), tickets. *Read connecta.skill("connector:<id>") before its first call.`
223
+
224
+ Write each description so its first 60 bytes tell the connector apart from its
225
+ neighbours. Purposes stay in this inventory, never in the instructions, which
226
+ some hosts repeat on every tool.
227
+
228
+ The complete line, legend included, caps at 1,024 UTF-8 bytes, which names every
229
+ connector of a typical deployment. Past the cap, detail degrades before names do:
230
+ purposes are dropped first, from the end of registry order, then titles. Only
231
+ when bare ids still exceed it do whole entries give way, in registry order, to an
232
+ exact `+N more` count. The inventory reads only the configured registry: no
159
233
  catalog load, no credential probe, no live status, no capability, and no
160
234
  replacement for canonical discovery or addressing. Search pages carry the same
161
235
  bounded title once per connector in `connectors[<id>].title`, so choosing an
@@ -172,7 +246,7 @@ discovery. Both take the same arguments.
172
246
  | `safety` | `"readOnly"` selects stored `read` verdicts; `"approvalRequired"` selects stored `write` verdicts; omitted or `"all"` selects both. Each row carries `classification: "read" \| "write"`. These legacy filter names do not describe client approval policy; a read-only endpoint has no write rows. |
173
247
  | `limit` / `offset` | page the ranked results; omit `limit` initially so the default eight-result page stays small |
174
248
  | `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema, `"typescript"` for a function signature ([below](#typescript-signatures)) |
175
- | `fullDescriptions` | unabridged tool purposes, at the obvious cost |
249
+ | `fullDescriptions` | unabridged tool purposes instead of the one-sentence summary, at the obvious cost |
176
250
 
177
251
  Neither `connector` nor `safety` grants authority or changes invocation
178
252
  admission; they select what discovery shows and nothing else.
@@ -181,9 +255,10 @@ Main search also consults a standard hatch's construction-authored operation
181
255
  vocabulary (path, operation id, vendor summary and tag), without calling a tool
182
256
  or reading account data. A match returns the owning `_api_search` tool; call it
183
257
  with the original query to retrieve operations. Exact tool/address and complete
184
- authored-tool matches retain precedence. A vocabulary match requires every term
185
- in one operation, and the operation's hatch route must be visible and match the
186
- read/write filter. On `approvalRequired` searches the returned discovery tool
258
+ authored-tool matches retain precedence, and a vocabulary match never suppresses
259
+ lexical partial matches: both stay on the page, the hatch above partial tools.
260
+ A vocabulary match requires every term in one operation, and the operation's
261
+ hatch route must be visible and match the read/write filter. On `approvalRequired` searches the returned discovery tool
187
262
  is still a read. Hidden tools, missing grants and read-only restrictions remain
188
263
  in force. Browsing does not consult operation vocabulary.
189
264
 
@@ -276,9 +351,9 @@ directly; reserve `connecta.describe` for a search without schemas, an ambiguous
276
351
  compact shape, or exact constraints that need `format: "json"`.
277
352
 
278
353
  Compact search is a routing view, not a second copy of connector documentation,
279
- so it spends bytes on shape and none on prose: tool purposes cap at 160
280
- characters, connector descriptions and property prose are dropped, and required
281
- input fields render before optional ones. Both surfaces share one renderer, so
354
+ so it spends bytes on shape and little on prose: a tool's purpose is its summary
355
+ ([below](#lexical-discovery)), connector descriptions and property prose are
356
+ dropped, and required input fields render before optional ones. Both surfaces share one renderer, so
282
357
  its byte and work budgets, its truncation renderings, and its handling of enums,
283
358
  tuples, and conditional keywords are documented once under
284
359
  [`connecta.search`](./code-mode.md#connectasearch) and
@@ -340,13 +415,28 @@ A connector may attach a deployment-owned guide as markdown, keeping the
340
415
  original `usageGuide: string` configuration, or as
341
416
  `{ content, summary?, required? }` — which registers no connector and creates no
342
417
  shared runtime template. The skills catalog prepends Agent Skills frontmatter to
343
- `content` and preserves the guide body, including its whitespace.
418
+ `content` for native `skills/get` and `resources/read`, and preserves the guide
419
+ body, including its whitespace. The `skills` tool and `connecta.skill` return that
420
+ body without the frontmatter; a downstream skill's file keeps its exact bytes on
421
+ every reader. One read-only notice leads a guide: the endpoint's on a read-only
422
+ endpoint, otherwise the connection's on a read-only connector.
344
423
  `summary` is normalized and refuses construction over 120 characters; absent,
345
424
  Connecta derives the same bounded fallback the skills listing uses from the
346
425
  guide's first meaningful body paragraph. `required: true` is reserved for
347
426
  generic API wrappers and cross-operation conventions a complete downstream
348
427
  schema cannot express.
349
428
 
429
+ A maintained provider's guide states only what no schema, description or typed
430
+ refusal already tells the agent where it acts. Generic rules live at the point of
431
+ use: `cursor` and hatch `page` schemas for continuation, `fields` for narrowing,
432
+ withheld values and cuts, hatch descriptions and `withheld` hints for operation
433
+ discovery, and the `write_outcome_unknown` error for write-once. A hatch `page`
434
+ with `hasMore: true` and no `nextCursor` also adds a `warnings` notice that the
435
+ result is incomplete. Money units, account or environment selection, sensitive
436
+ data reduction and "absence does not prove" rules stay in the guide, as does any
437
+ disqualifier that no refusal enforces. A read-only connection is not served its
438
+ guide's `writes` blocks.
439
+
350
440
  Describe results carry a `guide: "connector:<id>"` pointer and a
351
441
  `guideSummary`; a search page carries both once per connector in
352
442
  `connectors[<id>]`. A matching tool row or description also carries
@@ -409,16 +499,28 @@ or other live status reaches `tools/list` or the instructions, so the
409
499
  always-loaded surface stays byte-stable for a client's prompt cache.
410
500
  Instructions reflect the surface and endpoint write policy, subject to connector
411
501
  restrictions. When all admitted connectors are read-only, instructions state that
412
- writes are unavailable. They also include pool scope: at
413
- `/mcp/<pool>` and `/agent/<pool>`, a pool that sets a
414
- [`description`](./auth.md#pools) has it placed first after `Use skills. `, so
415
- the model learns that endpoint's scope, for example
416
- `Use skills. Team tier: shared tools only. For someone's own mail or files, ask them to message you one-on-one.`
417
- The guide pointer precedes the scope so both appear in the first 512 characters
418
- for a description of up to 500 characters. The routing rules follow. Meta instructions use compact equivalent routing rules
419
- when a scope is present. Plain `/mcp` never carries one. The description changes no tool or tool
420
- description, and `initialize` and `server/discover` stay `cacheScope: "private"`,
421
- so a client keeps what it learned per endpoint. Connector
502
+ writes are unavailable. On the meta surface they open by saying what the endpoint
503
+ reaches, for example `Gateway to connected services: crm, tickets.`, and then give
504
+ the routes, the write policy and recovery through `nextAction`. A native pool omits
505
+ the ids, because its tool names show them.
506
+
507
+ The ids come only from the caller's admitted view, the same view the `execute_code`
508
+ inventory reads: configured ids in registry order, never a value from the request,
509
+ a catalog or a credential. A maintainer at plain `/mcp` sees every id; a person
510
+ routed there sees only their pool's, and an id outside the admitted view never
511
+ appears. Past 120 UTF-8 bytes, ids give way, in registry order, to an exact
512
+ `+N more` count, and the inventory carries the fuller list.
513
+
514
+ They also include pool scope: a pool that sets a
515
+ [`description`](./auth.md#pools) has it placed first at `/mcp/<pool>` and
516
+ `/agent/<pool>`, and for a person routed to that pool at plain `/mcp`, so the model
517
+ learns that endpoint's scope, for example
518
+ `Team tier: shared tools only. For someone's own mail or files, ask them to message you one-on-one.`
519
+ A maintainer at plain `/mcp` receives none. The description changes no tool or tool
520
+ description. Plain `/mcp` can serve different ids and scope to different callers at
521
+ one URL, as its tool list and inventory already do, so `initialize` and
522
+ `server/discover` stay `cacheScope: "private"`: a client keeps what it learned per
523
+ caller and endpoint and never shares it. Connector
422
524
  guides stay scoped to the deployment that listed them even when two deployments
423
525
  use identical content, and a deployment with no connector guides receives none of
424
526
  the short conditional guide pointers in its tool definitions.
@@ -524,7 +626,9 @@ dropping the text copy waits on host-forwarding measurements showing supported
524
626
  clients do not need it. Direct-call failures set `isError: true` and carry the
525
627
  same `{ ok: false, error, durationMs, attempts }` envelope in both forms, whether
526
628
  `resultMode` is omitted, `"mcp"`, or `"value"`. Plain-text guidance stays
527
- text-only. Successful direct calls with `resultMode` omitted or `"mcp"` return
629
+ text-only: a `skills` fetch returns the guide once, in `content`, and the tool
630
+ advertises no `outputSchema`, while its listing and errors keep `structuredContent`.
631
+ A program's `connecta.skill` returns its own `{ name, format, text }` value. Successful direct calls with `resultMode` omitted or `"mcp"` return
528
632
  their data only in `content`, with the downstream format in
529
633
  `_meta["dev.connecta/format"]`. They omit `structuredContent`, including on
530
634
  truncated results, so clients that prefer it cannot replace data or a preview
@@ -622,16 +726,106 @@ Completed writes remain successful and must not be repeated. JSON values still
622
726
  round-trip such strings through their escaped JSON representation.
623
727
 
624
728
  A per-call `timeoutMs` covers catalog resolution, admission, and connector
625
- execution under one deadline. The admission queue's own timeout may expire
729
+ execution under one deadline, cut to the response ceiling, `execute.watchdogMs`
730
+ (55 s by default) from the request's arrival, so the client gets a typed result
731
+ rather than its own transport timeout. A call with no time left is refused before it
732
+ resolves, as a retryable `timeout` at `details.stage: "response ceiling"` that says it
733
+ was not sent. A call whose time runs out during resolution, admission or auth gets the
734
+ same refusal from a check that is the last step before its connector. See [`L3`](./code-mode.md#cancellation-and-limits). The admission queue's own timeout may expire
626
735
  sooner but cannot extend the call deadline. Result processing happens after that
627
736
  deadline ends, because a completed downstream call must not turn into a
628
737
  retryable timeout while Connecta prepares its response.
629
738
 
739
+ ### Previews of reads over the cap
740
+
741
+ A result at or under its cap always returns whole. A successful direct read over
742
+ its cap (the connector's `maxResultBytes`, else `calls.maxResultBytes`, else 24,000
743
+ bytes), on a surface that lists a program tool, may show a sample instead of its
744
+ first bytes: the structure and whole rows, or the head and tail of its text, with a
745
+ retained copy of the result and a ready-to-edit program. Such a read is already cut
746
+ and already stashed, so a preview adds no storage cost, and valid JSON with a notice
747
+ saying what it holds replaces raw JSON sliced mid-value. The rule is generic and
748
+ needs nothing from a provider. It reads the result after redaction and value safety,
749
+ for API values and for a lone MCP text block.
750
+
751
+ **Rows.** A preview fires when all of these hold:
752
+
753
+ - its largest list holds at least 3 records or arrays, sits within 3 keys of the
754
+ root, and holds at least half the bytes;
755
+ - everything outside that list fits in 1,024 bytes;
756
+ - at least the first row can be shown whole, within 3,000 bytes, and at least one
757
+ row is left out;
758
+ - every number in the result keeps its exact value through `JSON.parse`.
759
+
760
+ ```text
761
+ {"truncated":true,"resultId":"…","totalBytes":48210,"rows":{"path":"items","count":200,"shown":18,"keys":["id","email","status"]},"hint":"Sample only: rows 1-18 of 200 at items follow, in the result's own shape, with every value outside the list; the other 182 are not shown. All of it is retained for 15 minutes: run nextAction in execute_code, editing its last line to filter, join or count the rows, instead of reading pages by hand. If execute_code is refused or unavailable, the rest can't be read here: narrow this read with its own filter, limit or fields arguments, or say what was not read.","nextOffset":0,"nextAction":{"tool":"execute_code","arguments":{"code":"async () => { let text = \"\", offset = 0, page; do { page = await connecta.result(\"…\", { offset }); text += page.text; offset = page.nextOffset; } while (page.hasMore); const rows = JSON.parse(text).items; return { count: rows.length, rows: rows.slice(0, 5) }; }"}}}
762
+ {"items":[{"id":"t_1","email":"alice@example.com","status":"open"},…],"has_more":true,"next_cursor":"c2"}
763
+ ```
764
+
765
+ The body after the first newline is valid JSON in the result's own shape: whole
766
+ rows from the start of the list up to 1,500 bytes, and every value outside it.
767
+ `rows.path` names the list as JavaScript would, so a key a dotted path cannot
768
+ spell reads `["a.b"].items`, and a root array reads `(root)`. `rows.keys` samples
769
+ up to 40 keys of at most 64 characters from the first 50 rows; `rows.count` and
770
+ `totalBytes` describe the whole result.
771
+
772
+ On an endpoint that lists `execute_read_code`, the hint and `nextAction` name it instead
773
+ of `execute_code`. A preview only ever follows a read, and its program reads only the
774
+ retained result, so it runs on either route.
775
+
776
+ **Text.** Text that is not JSON, or an API connector's text value, returns its line
777
+ count in `text.lines`, then its first 1,200 and last 800 bytes around a marker saying
778
+ how many bytes were left out, with `text.shownBytes` beside it. Cuts fall on
779
+ character boundaries. Its program reads the retained text and returns the lines
780
+ matching a pattern the agent can edit.
781
+
782
+ **Everything else keeps the route below:** results no program can reach whole within
783
+ `execute.maxHostCalls`, JSON objects with no such list, scalars, lists of scalars,
784
+ lists deeper than 3 keys or shorter than 3 rows, a first row over 3,000 bytes, text
785
+ with unpaired surrogates, several blocks, non-text blocks and value mode. So does any
786
+ number parsing would change: complete tokens are compared by exact value, including
787
+ fractions and exponents, so `9.007199254740993e15`, `1e999` and `-0` decline the view
788
+ wherever they appear, and `1e3` and `12.50` do not. Digits inside a string never count.
789
+ The offered program parses the same text, so a number past the sample declines too.
790
+
791
+ **A call that may have written never previews.** That covers every effective write
792
+ and a source write a deployment overrode to read: its result is its only copy, and
793
+ repeating it to look again is forbidden. It keeps its first bytes, and its notice says
794
+ the write already ran and offers no repeat.
795
+
796
+ A preview is presentation only. A program's value and the handle's pages are
797
+ byte-for-byte what they would have been: the retained text is the same redacted
798
+ text the read would have paged, and `format` keeps its meaning. A view drops no
799
+ continuation field, total, warning, status or `format`, shows no empty list in place
800
+ of a full one, keeps array positions, and shows `false` and `0` as they are.
801
+ `totalBytes` is the original size. Clients that parse a direct read should not assume
802
+ the body after a `truncated` notice is a prefix: `nextOffset` is 0 and the body is a
803
+ separate view. The per-connector and deployment `maxResultBytes` still bound what is
804
+ shown: a preview that does not fit the call's cap takes the route below, and a raised
805
+ cap returns more results whole. A preview counts as `result_too_large` friction, as
806
+ any result past its cap does. If the stash fails, the read keeps its first bytes and
807
+ its paging-unavailable notice, and the operator warning fires as usual.
808
+
809
+ **Recovery depends on the program tool.** `connecta.result` exists only inside
810
+ programs, and a host may prompt for or refuse `execute_code`, which is annotated as a
811
+ write on a writable endpoint. A refused program means the rows or text the preview left
812
+ out can't be read from this result, which is also true of the first-bytes route. So the
813
+ hint states the fallback: narrow the read with its own arguments, or say what was not
814
+ read. A view shows less of the result than first bytes would, 1,500 bytes of rows
815
+ against 24,000, so narrowing matters more on such a host.
816
+
817
+ Previews need a program tool, not only a paging tool. Page-first reading failed when an
818
+ agent read a 185 KB log in pages and missed the failing range, and a preview shows too
819
+ little to read by hand, so a native pool with `executeCode: false` keeps its first
820
+ bytes and its `read_result` notice. One value in `src/routes/mcp.ts` registers the
821
+ program tool for a request and names it in `nextAction` and the hint, so the text never
822
+ offers a tool the surface does not list.
823
+
630
824
  ### Truncated direct-call results
631
825
 
632
826
  A `call_tool` or `call_destructive_tool` result over its cap — the connector's
633
- `maxResultBytes`, else `calls.maxResultBytes`, else 24,000 bytes — comes back
634
- as one text block. Its first line is the truncation notice, one line of compact
827
+ `maxResultBytes`, else `calls.maxResultBytes`, else 24,000 bytes — that does not
828
+ get a [preview](#previews-of-reads-over-the-cap) comes back as one text block. Its first line is the truncation notice, one line of compact
635
829
  JSON; everything after the first newline is the preview. `structuredContent`
636
830
  is absent, so the notice cannot hide the preview in clients that prefer it:
637
831
 
@@ -685,16 +879,25 @@ notice is `data` itself, with no preview and a next action at offset 0.
685
879
  The hint says what to do next. For a call not explicitly annotated read-only it
686
880
  opens with “This write already ran: do not call it again to see its result.”
687
881
  The approved write happened; repeating it to look again is how one export
688
- becomes three. For an explicitly read-only call, repeating is the better route,
689
- so the hint offers it first: to find something specific, repeat the read inside
690
- `execute_code` with `connecta.call` and filter or search the result there; to
691
- read it in full, page with `connecta.result`. Inside a program the inline cap does
692
- not apply to a host call's result, only to what the program returns (`L6` in
693
- [code mode](./code-mode.md) bounds a host call far higher), so the reduction
694
- sees the whole result at once. When the hint named paging alone, the eval's
695
- weakest model read a 185 KB CI log in 24,000-byte pages, skipped the range
696
- holding the real failure, and named the flaky test near the top; before the
697
- notice was visible, it had reduced the log in a program and found the failure.
882
+ becomes three. For an explicitly read-only call, the hint offers reduction first:
883
+ to find something specific, loop `connecta.result` over the retained result in
884
+ one `execute_code` program, filter there and return the matches; to read
885
+ on, run `nextAction`. A reduction must see the whole value at once, because
886
+ page-by-page reading in the model misses things: when the hint named paging
887
+ alone, the eval's weakest model read a 185 KB CI log in 24,000-byte pages, skipped
888
+ the range holding the real failure, and named the flaky test near the top. The
889
+ loop keeps that whole-value reduction without fetching from the downstream a
890
+ second time. The hint names the result ID and that the result is retained for 15
891
+ minutes.
892
+
893
+ The loop is offered only when one program can finish it: every page at the
894
+ connector's cap (at most the 32 KiB bridge ceiling), plus two host calls spare,
895
+ must fit `execute.maxHostCalls`. At the defaults that is about 432 KB. Past that
896
+ a loop would end in `budget_exceeded` with nothing returned, so the hint says to
897
+ repeat the read inside `execute_code` with `connecta.call` and reduce it there;
898
+ a host call's result is not bound by the inline cap, only what the program returns
899
+ (`L6` in [code mode](./code-mode.md)). A value over `L6`'s host-call bound returns
900
+ a paged handle instead, whose own hint is sized the same way.
698
901
  When paging is available, `nextAction` is the page handle for either kind of call, because paging is
699
902
  the one next step connecta can spell out exactly, while a reduction is a
700
903
  program the agent has to write. `resultId` stays beside the exact `nextAction`,
@@ -709,17 +912,28 @@ no paging route.
709
912
 
710
913
  Meta endpoints page inside a program. Native pools without code list `read_result` for the same bound handles. Program calls also
711
914
  return handles for oversized values; see [program result limits](./code-mode.md#cancellation-and-limits).
915
+ A handle within the program's [read ceiling](./code-mode.md#connectaresult-and-connectaskill)
916
+ says to read it whole with `connecta.result(handle, { all: true })`, for one host call.
917
+ Past the ceiling, a read's handle says to loop `connecta.result(handle, { offset })`
918
+ until `!hasMore` only when every page at the connector's cap, plus two host calls
919
+ spare, fits the calls its program has left. At the default 20 calls and 24,000-byte
920
+ cap it doesn't: its hint then gives the pages
921
+ to read in this program and in each new one, and says to return the reduction so
922
+ far with `nextOffset` and continue from both, by `resultId`, inside the 15-minute
923
+ retention. A loop past the budget ends in `budget_exceeded` with nothing returned.
924
+ A write's handle says the write already ran and not to repeat it.
712
925
  `connecta.result(handle, { page: 0 })` accepts those handles alongside direct
713
926
  result IDs. Numbered pages are zero-based and mutually exclusive with offset.
714
927
  Keep maxBytes fixed while incrementing pages. A direct call's
715
928
  notice carries a `resultId`, `nextOffset`, and an executable `execute_code`
716
- recovery action. Read and reduce the result in one program:
929
+ recovery action. When the hint offers the loop, read and reduce the result in one
930
+ program. Leave `maxBytes` unset, because a smaller page spends more host calls:
717
931
 
718
932
  ```js
719
933
  async () => {
720
934
  let text = "", offset = 0, page;
721
935
  do {
722
- page = await connecta.result("result-id", { offset, maxBytes: 8_000 });
936
+ page = await connecta.result("result-id", { offset });
723
937
  text += page.text;
724
938
  offset = page.nextOffset;
725
939
  } while (page.hasMore);
@@ -742,6 +956,13 @@ a paging-unavailable notice, without a handle or recovery action. A write's
742
956
  notice still says it already ran. Activity records success; operator logs receive
743
957
  a fixed warning without storage error prose. Never repeat a write to recover output.
744
958
 
959
+ A result a program retained is readable from later requests only once it has passed
960
+ that run's final secret set. Otherwise it stays unreadable: paging answers
961
+ `result_not_retained` with `retryable: false`, telling a read to rerun and a write
962
+ not to repeat ([code mode `R4a`](./code-mode.md#results-and-projection)).
963
+ A direct call's stash passes its own request's set when stored; a later direct call
964
+ in another request shares no set with it.
965
+
745
966
  ## Lexical discovery
746
967
 
747
968
  `search_tools` and in-program `connecta.search` return the same flat discovery
@@ -790,9 +1011,87 @@ to 60 seconds: its entry keeps the recorded code with `retryable: true`,
790
1011
  lookup failed recently (CODE). Search with connector "X" to try it now.` A
791
1012
  scoped search always probes. Records are kept per owner, admitted endpoint and
792
1013
  described connector configuration. A healthy connector whose catalog always
793
- takes more than 5 seconds can stay out of unscoped search; search it with
794
- `connector`, or let any longer-deadline operation warm its complete catalog.
795
- See [Discovery health](./architecture.md#storage-credentials-and-connectors).
1014
+ takes more than 5 seconds and has no earlier complete catalog can stay out of
1015
+ unscoped search; search it with `connector`, or let any longer-deadline operation
1016
+ warm its complete catalog. See
1017
+ [Discovery health](./architecture.md#storage-credentials-and-connectors).
1018
+
1019
+ **Stale listings.** When a connector does not answer an unscoped search, because
1020
+ of a timeout, a skip after a recent one, the discovery budget, `unavailable`,
1021
+ `rate_limited` or a retryable downstream failure, the search lists that
1022
+ connector's last complete catalog if the same owner, credential and catalog
1023
+ generation have one no more than 24 hours past its expiry. Its `catalogErrors`
1024
+ entry then adds `stale: true` and `ageMs`, the milliseconds since the downstream
1025
+ last confirmed that catalog, and keeps `code`, `retryable` and any
1026
+ `retryAfterMs`. A downstream's own retry delay stays as it named it, and the
1027
+ message then says to wait rather than to search again. A skipped connector's
1028
+ `retryAfterMs` is when discovery next asks it, at most 60 seconds:
1029
+
1030
+ ```json
1031
+ {
1032
+ "connector": "tickets",
1033
+ "code": "timeout",
1034
+ "message": "Connector \"tickets\" did not answer discovery (timeout), so its tools are listed from its last complete catalog. Calls still check the live catalog. Search with connector \"tickets\" to try it now.",
1035
+ "retryable": true,
1036
+ "stale": true,
1037
+ "ageMs": 412000
1038
+ }
1039
+ ```
1040
+
1041
+ A stale listing grants nothing. A call, `connecta.describe` and a search with
1042
+ `connector` read the live catalog, so a tool the downstream removed fails with
1043
+ `unknown_tool` and a reclassified one runs under its current verdict. Deployment
1044
+ classification, hidden tools, pool grants and read-only endpoints apply to a
1045
+ stale listing as they do to a live one. Authorization and permission failures,
1046
+ refusals and malformed catalogs never list a stale catalog, and a partial catalog
1047
+ is never stored or listed. An entry without `stale` lists no tools for that
1048
+ connector. `queryAnalysis.catalogError` holds failures only.
1049
+
1050
+ An authorization or permission refusal also retires that credential's catalog
1051
+ for later requests, in every isolate: it is neither listed nor reused, fresh or
1052
+ stale, by discovery or by a call's five-minute fallback, until a listing
1053
+ succeeds. On a connector with a shared credential this covers everyone using
1054
+ that credential; on any other connector, only the caller refused. A credential
1055
+ change, OAuth connect or disconnect, or operator invalidation has the same effect
1056
+ at once for every owner. A stale listing is read again after its probe fails, so
1057
+ any of these during the probe also takes the tools out of that search.
1058
+
1059
+ A refusal retires the catalog of the credential, OAuth identity and catalog
1060
+ generation its attempt was dispatched under, whichever are current when it
1061
+ answers, so another credential's catalog keeps working. A listing lifts only the
1062
+ retirement it read before it asked the downstream; a refusal that lands while it
1063
+ runs stays until a later listing succeeds. A custom connector's answer is
1064
+ retained only if its credential did not change while it was listing. Connecta
1065
+ cannot see which credential a custom connector sent, so a refusal whose
1066
+ credential changed while it was listing, or cannot be read afterwards, retires
1067
+ that connector's catalogs for every credential and owner.
1068
+
1069
+ Three limits follow:
1070
+
1071
+ - A custom connector's credential is compared when its listing begins and when
1072
+ it is refused. A change back to the same credential between them goes unseen,
1073
+ as does a connector sending a credential it kept from earlier. The credential
1074
+ actually refused is then not retired: its catalog can be listed stale if it
1075
+ becomes current again, until its first answer.
1076
+ - A retirement that cannot be written is logged as `catalog fence failed` and the
1077
+ refusal is still returned. The catalog stays reusable until a later refusal is
1078
+ recorded, a listing replaces it or its stale limit passes.
1079
+ - A connector whose `tools/list` keeps answering 401 or 403 has no catalog to
1080
+ fall back on, so every call that must resolve a tool through its catalog fails
1081
+ with that refusal until a listing succeeds. This includes a credential that may
1082
+ call tools but not list them, and the five-minute fallback does not apply while
1083
+ the catalog is retired.
1084
+
1085
+ `total` counts this response's matches. Offsets are stateless, so it stays the
1086
+ same across a browse's pages while each catalog is unchanged, including one
1087
+ listed stale, and can change if a catalog refreshes between pages or a connector
1088
+ with no stale catalog starts answering.
1089
+
1090
+ A cached or authored catalog does not wait for a probe slot, so
1091
+ `discovery.concurrency` bounds the connectors actually being asked. A probe that
1092
+ runs out of its own deadline also starts one refresh in the runtime's tail
1093
+ (`ctx.waitUntil`, or the Node listener's tracked equivalent) under the full probe
1094
+ deadline, at most 20 seconds, and that refresh warms the next search.
796
1095
 
797
1096
  Tool rows carry the address, stored classification, and tool-specific
798
1097
  `guideRequired` and `guideRequiredReasons`. Facts shared by every row of one
@@ -807,17 +1106,44 @@ compact schema strings and TypeScript signatures. Compact schemas are text;
807
1106
  `inputSchema.properties` is meaningful only in JSON mode. Pagination counts
808
1107
  tools across connectors: `nextOffset` is present only when another page remains.
809
1108
 
810
- Search tokenizes tool names and descriptions at punctuation and camel-case
811
- boundaries. Whole-token matches and a small set of inflectional variants preserve
812
- recall without admitting arbitrary mid-word substrings. Document frequency weights
813
- rare domain terms above ubiquitous action terms. Exact tool names and canonical
814
- addresses rank first, so `get_issue` beats `get_issue_status` even when a
815
- connector is named `issue`. Exact configured connector IDs, full display titles,
816
- and recognized service names rank next. Complete lexical matches then precede
817
- partial matches. Within a tier, a query whose first word outside connector names
818
- is `list`, `get`, `find`, `search`, `show`, `read`, `fetch`, or `count` ranks
819
- read rows before write rows; writes stay on the page. Score and catalog order
820
- break the remaining ties.
1109
+ A row's `description` is a summary: the first complete sentence, joined to the
1110
+ following ones until it reaches 40 characters, within 160 UTF-16 code units. Text
1111
+ with no sentence end in that range is cut at a word and marked `…`. The row
1112
+ carries `descriptionTruncated: true` whenever text was left out;
1113
+ `fullDescriptions: true` returns the original text and no flag.
1114
+
1115
+ Search tokenizes tool names, titles and full descriptions at punctuation and
1116
+ camel-case boundaries. Whole-token matches, a small set of inflectional variants
1117
+ and a few unambiguous abbreviations (`config`/`configuration`, `db`/`database`,
1118
+ `doc`/`document`, `env`/`environment`, `org`/`organization`,
1119
+ `repo`/`repository`, `var`/`variable`) preserve recall without admitting
1120
+ arbitrary mid-word substrings. Document frequency weights rare domain terms above
1121
+ ubiquitous action terms. A term weighs most in the name, then the summary, then
1122
+ the title (`title` or `annotations.title`), and least in the rest of the
1123
+ description, which stays searchable but is weak evidence. A tool's score is then
1124
+ multiplied by the square of the share of the query's total rarity it covers in
1125
+ any zone, so rare words in a description's tail outrank a common verb in a name,
1126
+ and a tool covering every term is unaffected.
1127
+
1128
+ Exact tool names and canonical addresses rank first, so `get_issue` beats
1129
+ `get_issue_status` even when a connector is named `issue`. A tool named only by
1130
+ an action word, such as `search`, is exact when it is the whole query, not when a
1131
+ longer query contains that word. Exact configured connector IDs, full display
1132
+ titles, and recognized service names rank next. Three tiers follow:
1133
+
1134
+ 1. Complete matches: every term is in the name, title or summary. A tool on the
1135
+ connector the query or scope names need not repeat that connector's name.
1136
+ 2. A hatch whose operation vocabulary matched, and a tool whose name states a
1137
+ query word other than an action or the connector, with the remaining terms in
1138
+ the rest of its description.
1139
+ 3. Every other match.
1140
+
1141
+ When a complete match exists, tier 3 keeps only tools covering every term
1142
+ somewhere, covering at least two terms, or whose name states a term at least as
1143
+ rare as the query's average. Otherwise it keeps any match. Within a tier, a query
1144
+ whose first word outside connector names is `list`, `get`, `find`, `search`,
1145
+ `show`, `read`, `fetch`, or `count` ranks read rows before write rows; writes stay
1146
+ on the page. Score and catalog order break the remaining ties.
821
1147
 
822
1148
  A maintained provider may declare `searchSynonyms`: groups of single lowercase
823
1149
  words its users and its vendor use for the same record, such as `household`
@@ -842,8 +1168,9 @@ catalog is a catalog failure, never an absence. Absence records and catalog
842
1168
  errors use only the endpoint's tools; a connector the endpoint does not serve
843
1169
  is indistinguishable from an unconfigured one.
844
1170
 
845
- If no tool covers every search term, wider any-term fallback remains available
846
- for ordinary action/object searches and the page carries `matchMode: "partial"`.
1171
+ If no tool covers every search term, in any part of its text or through one
1172
+ operation of its vocabulary, wider any-term fallback remains available for
1173
+ ordinary action/object searches and the page carries `matchMode: "partial"`.
847
1174
  A service absence suppresses that fallback, so `GitHub get issue` cannot return
848
1175
  Linear issues or Supabase projects. Tool rows expose no scores or
849
1176
  per-result query coverage. `queryAnalysis` partitions at most eight terms of at
@@ -984,11 +1311,41 @@ behavior is unchanged.
984
1311
  The host echoes `requestState` and its bare `inputResponses.connecta_auth`
985
1312
  response on the original `tools/call`. Accept re-runs the original call; it
986
1313
  does not prove browser consent completed. Decline or cancel ends without
987
- dispatch, with `auth_declined` or `auth_cancelled`. Three prompts are allowed
1314
+ dispatch or another prompt, with `auth_declined` or `auth_cancelled`. A client
1315
+ can answer without showing anything, so neither code means a person refused, and
1316
+ Connecta cannot tell the difference. When this caller may still connect, the
1317
+ refusal carries the handoff this connector would get on a host without URL
1318
+ support (`connector`, `recovery`, `authorizationUrl` or `operatorUrl`,
1319
+ `instructions`), plus `nextAction: { url, purpose }`:
1320
+
1321
+ ```json
1322
+ {
1323
+ "code": "auth_declined",
1324
+ "message": "The client declined the connection prompt, possibly automatically, so the user may not have seen it. Nothing was retried.",
1325
+ "retryable": false,
1326
+ "connector": "service",
1327
+ "recovery": "oauth",
1328
+ "authorizationUrl": "https://connecta.example/connect/service?h=...",
1329
+ "nextAction": { "url": "https://connecta.example/connect/service?h=...", "purpose": "..." }
1330
+ }
1331
+ ```
1332
+
1333
+ Give that link to the user once, then stop. Do not prompt again, retry, or reach
1334
+ the service another way until they confirm. If they already refused this
1335
+ connection in the conversation, only mention that the link is available.
1336
+ A failed signing or storage step leaves the refusal without a link. Its message
1337
+ says so: unless the user refused this connection, repeat the request once for a
1338
+ new link, then give that link to them, and stop if that repeat also fails. Connecta
1339
+ keeps no record of which clients refuse, so each new prompt to such a client
1340
+ ends the same way. Three prompts are allowed
988
1341
  within one ten-minute retry window, then `auth_round_limit` ends the flow.
989
1342
  Each state can be consumed once; concurrent or repeated retries are refused
990
- before dispatch. A Continue link from a pending restart requires that restart
991
- to start successfully before the browser can consume it.
1343
+ before dispatch. Accept does not mean a browser has opened the link, so every link
1344
+ in a flow stays live. A `force: true` flow's links, including a refusal's, share one
1345
+ reset: the first verified visit performs it and later visits continue it. A visit
1346
+ during a running start returns 409 without spending its link, and says to start a new
1347
+ connection request if it persists, because a new call is a new flow. A failed start
1348
+ lets the flow's next visit begin again.
992
1349
  Host-owned invocation facts record the auth failure's origin and entry into
993
1350
  all calls, including each classification, catalog freshness and classification
994
1351
  digest. Raw fetches and custom connector transports cannot establish recovery
@@ -1038,11 +1395,10 @@ An OAuth call failure uses this envelope:
1038
1395
  "operation": "service.read",
1039
1396
  "recovery": "oauth",
1040
1397
  "authorizationUrl": "https://connecta.example/connect/service?h=...",
1041
- "instructions": "Copy authorizationUrl exactly as returned and sign in as the initiating user.",
1398
+ "instructions": "For the person opening this link: sign in as the user who requested this connection; ...",
1042
1399
  "nextAction": {
1043
- "tool": "authorize_connector",
1044
- "arguments": { "connector": "service" },
1045
- "operatorHandoff": "Give the URL and instructions it returns to the operator."
1400
+ "url": "https://connecta.example/connect/service?h=...",
1401
+ "purpose": "Give this link to the user, unchanged, then stop. Retry after they confirm."
1046
1402
  },
1047
1403
  "retry": "Retry service.read after the operator completes recovery."
1048
1404
  }
@@ -1054,7 +1410,26 @@ returns for the admitted user, connector, and pool. Direct calls include them in
1054
1410
  `error`; program `connecta.call` failures include them in `error.data` and
1055
1411
  `error.details`. Search `catalogErrors` and scoped `queryAnalysis.catalogError`,
1056
1412
  and describe `errorDetails`, carry them too. When an auth error includes a
1057
- handoff, give it to the user or operator. The class-specific handoff is:
1413
+ handoff, give it to the user or operator. An error that carries a handoff says
1414
+ so in `nextAction`, because calling `authorize_connector` would only return the
1415
+ same thing:
1416
+
1417
+ - a link: `{ url, purpose }`, with `url` equal to `authorizationUrl` or `operatorUrl`;
1418
+ - no link (`unavailable`): `{ action: "contact_operator", purpose }`, with the
1419
+ repair in `instructions`;
1420
+ - after a write of unknown outcome, either `purpose` says to re-read the write's
1421
+ target once they confirm, never to retry.
1422
+
1423
+ `message` drops its route text the same way. The typed fields are unchanged:
1424
+ `nextAction` repeats a link, it does not replace `authorizationUrl`, `operatorUrl`,
1425
+ `credential` or `instructions`. `nextAction` keeps
1426
+ `{ tool: "authorize_connector", arguments: { connector } }` in two cases: when no
1427
+ handoff could be attached, as for a request that has no handoff factory or whose
1428
+ signing failed, and in a program's terminal `write_outcome_unknown`-family result,
1429
+ which carries no handoff fields and so holds no link. A URL-elicitation host's
1430
+ call that falls back to a link gets the same `{ url, purpose }`.
1431
+
1432
+ The class-specific handoff is:
1058
1433
 
1059
1434
  - `oauth`: an `authorizationUrl` to connecta's `/connect/<connector>?h=...` route
1060
1435
  and instructions to sign in as the initiating user;
@@ -1104,7 +1479,9 @@ way to call a tool. On a writable endpoint, `call_tool` reaching an unannotated,
1104
1479
  write-capable, or destructive tool returns `destructive_tool_requires_approval`
1105
1480
  with `nextAction` for `call_destructive_tool` and the canonical address. On a
1106
1481
  read-only endpoint every write path returns `read_only_endpoint` with no
1107
- `nextAction`. Nothing is executed by these records.
1482
+ `nextAction`. Its message says no write route exists, not to try another route,
1483
+ to tell the user, and that only the operator can provide a writable connection.
1484
+ Nothing is executed by these records.
1108
1485
 
1109
1486
  `connecta.describe` keeps its failures inline instead, so one miss cannot
1110
1487
  discard the other schemas; each failed entry carries a human `error`, typed
@@ -1186,7 +1563,7 @@ Unsupported task, Skills, and resource capabilities use the non-retryable
1186
1563
  Unknown skill names and connectors without a guide retain their local lookup
1187
1564
  diagnostic with `code: not_found` and a `nextAction` calling `skills` with no
1188
1565
  name to discover available names. They do not request service restoration.
1189
- The search output schema describes action discriminators compactly; action
1566
+ The server's search page contract states action discriminators compactly; action
1190
1567
  advice is additive, while catalog error and authorization handoff fields retain
1191
1568
  their detailed contracts.
1192
1569
  A spent read continuation's fresh-call action takes precedence over generic
@@ -1294,9 +1671,16 @@ its received types and schema facts. A custom connector's own findings must
1294
1671
  follow the same rule. Operator records contain only checked failure facts,
1295
1672
  never findings or examples.
1296
1673
 
1297
- `nextAction` points to discovery scoped to the same connector and tool name when
1298
- the compact schema is needed, routed like any other miss: a program to
1299
- `connecta.search`, a top-level call to `search_tools`. `retry` says to correct
1674
+ When `issues` and `acceptedKeys` are present and nothing was truncated, the
1675
+ findings name the fix, so the envelope has no `nextAction` and `retry` leads.
1676
+ Otherwise `nextAction` points to discovery scoped to the same connector and tool
1677
+ name, with the purpose "Only if the validation above is not enough: inspect the
1678
+ schema.", routed like any other miss: a program to `connecta.search`, a top-level
1679
+ call to `search_tools`. `example` is synthesized from the published schema and
1680
+ checked against its validator; a plain required string reads as a named
1681
+ placeholder such as `"<id>"` rather than an empty string, which would pass local
1682
+ validation and reach the downstream. A schema that refuses the placeholder, and a
1683
+ `writeOnly` position, keep the plain sample. `retry` says to correct
1300
1684
  the listed arguments and reissue the original operation. What the local
1301
1685
  validator declines to evaluate is [code mode](./code-mode.md#errors)'s `E8`.
1302
1686
 
@@ -1309,6 +1693,15 @@ Unknown-address errors list `configuredConnectors` from the current endpoint's
1309
1693
  scoped registry, including an empty list when none are accessible. They never
1310
1694
  list connectors from another pool or identity's grants.
1311
1695
 
1696
+ An `unknown_tool` error from a direct call or a program carries `suggestions`
1697
+ when the connector has a tool with a near name: canonical addresses ranked as
1698
+ describe ranks them, and only reads on a read-only endpoint or connector.
1699
+
1700
+ A direct call whose arguments do not match `call_tool` or `call_destructive_tool`
1701
+ fails input validation before anything is resolved. The message names the shape:
1702
+ `Send { address, args }`, with the tool's own arguments inside `args`. Neither
1703
+ tool accepts MCP's `name` or `arguments` keys.
1704
+
1312
1705
  A write that times out after dispatch without complete auxiliary-only proof
1313
1706
  returns `write_outcome_unknown` with
1314
1707
  `retryable: false` and an agent-only `uncertainCall: { address, args?, argsOmitted?, argsRedacted?, recovery? }`.
@@ -1431,7 +1824,7 @@ resent. It is never retryable and carries no wait. Until the credential is
1431
1824
  renewed, the failure keeps its authorization code (`auth_required`, or
1432
1825
  `downstream_oauth_required` for an OAuth connector) with
1433
1826
  `writeOutcome: "unknown"`, `reconciliationRequired: true`, and the
1434
- `authorize_connector` handoff, whose `instructions` say to authorize and then
1827
+ authorization handoff, whose `instructions` and `nextAction` say to authorize and then
1435
1828
  re-read the target before any fresh call. That holds whatever renewal meets: a
1436
1829
  refused or unreachable token endpoint, a disconnect, or the caller leaving. It
1437
1830
  also holds whatever the call met after the 401: a body past the response
@@ -1476,7 +1869,16 @@ in `writes.unknown` even when the program caught the failure. When that write
1476
1869
  met an authorization verdict, the program's error is that verdict, as a direct
1477
1870
  call reports it: its code, `connector`, `recovery`, and `nextAction`, with
1478
1871
  `writeOutcome: "unknown"` and `reconciliationRequired: true`. Any other unknown
1479
- write makes it `write_outcome_unknown`.
1872
+ write makes it `write_outcome_unknown`. Either error's `unknownWrites` lists at
1873
+ most ten such writes with their `address`, `code` and redacted `message`, and
1874
+ `unknownWritesTruncated` marks a write left out or a message dropped. A write whose
1875
+ tool answered `isError` carries `toolAnswered: true`, on a direct call too: its
1876
+ message is the tool's own words, up to 4,096 UTF-8 bytes, cut only between tokens
1877
+ and marked `… [truncated]`. Its outcome stays unknown because its handler ran before
1878
+ it answered, and `retry` says to check the target and to give the user any approval
1879
+ link or request id. A program's `write_outcome_unknown` leads its `message` with
1880
+ those words instead of repeating them in the entry. A later call the tool's words
1881
+ ask for, such as one carrying a provider's approval token, is a new deliberate call.
1480
1882
 
1481
1883
  The classification served for the call decides this advice, including deployment
1482
1884
  `classification` overrides. For example, `classification: { crm: { get_person:
@@ -1502,7 +1904,8 @@ at the downstream boundary, even when the agent may see the error's message.
1502
1904
  A pool set to `surface: "native"` lists each downstream tool as its own MCP
1503
1905
  tool instead of the six meta-tools, for clients that already defer tools behind
1504
1906
  their own search (Eve, Claude Code with tool search). The meta surface stays
1505
- the default everywhere, and plain `/mcp` is always meta.
1907
+ the default everywhere. Plain `/mcp` is always meta for a maintainer; a person
1908
+ routed there by their one pool membership gets that pool's surface.
1506
1909
 
1507
1910
  ```ts
1508
1911
  pools: {
@@ -1516,13 +1919,13 @@ refuses it otherwise, and refuses any `surface` other than `"meta"` or
1516
1919
  `"native"`. Both pool endpoints, `/mcp/<pool>` and `/agent/<pool>`, serve the
1517
1920
  pool's surface.
1518
1921
 
1519
- | A native pool lists | Shape |
1520
- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1521
- | Each visible downstream tool | Named `<connector>__<tool>`; object input constraints and local references preserved; no `outputSchema`; `_meta["dev.connecta/address"]` is its address |
1522
- | `execute_code` | Unless `executeCode: false`. Each downstream tool's description then ends with `Program address: <connector>.<tool>`, the address `connecta.call` takes |
1523
- | `read_result` | Only with `executeCode: false`: `{ id, offset?, page?, maxBytes? }` pages a stashed result exactly as `connecta.result` does, with the same bindings and rechecks |
1524
- | `authorize_connector` | Always; requests the existing typed authorization handoff for a granted connector |
1525
- | `skills` | Always |
1922
+ | A native pool lists | Shape |
1923
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1924
+ | Each visible downstream tool | A read is `<connector>__<tool>`, a write `<connector>__WRITE__<tool>`; object input constraints and local references preserved; no `outputSchema`; `_meta["dev.connecta/address"]` is its address |
1925
+ | `execute_code` | Unless `executeCode: false`. Each downstream tool's description then ends with `Program address: <connector>.<tool>`, the address `connecta.call` takes |
1926
+ | `read_result` | Only with `executeCode: false`: `{ id, offset?, page?, maxBytes? }` pages a stashed result exactly as `connecta.result` does, with the same bindings and rechecks |
1927
+ | `authorize_connector` | Always; requests the existing typed authorization handoff for a granted connector |
1928
+ | `skills` | Always |
1526
1929
 
1527
1930
  `search_tools`, `call_tool`, and `call_destructive_tool` are not listed. Auth failures still carry their handoff in the error envelope.
1528
1931
 
@@ -1533,61 +1936,115 @@ name reserved for a built-in guide uses its canonical connector skill URI. This
1533
1936
  compact pointer repeats on each tool so host search results retain it; connectors
1534
1937
  without guides add nothing. It depends only on the admitted connector roster.
1535
1938
 
1536
- **Names.** A name is a pure function of the connector id and downstream tool
1537
- name, so it survives catalog refreshes. It is `<connector>__<tool>` when the
1538
- tool name uses only `[A-Za-z0-9_-]`, contains no `__`, does not start with `_`,
1539
- and the whole name fits 48 characters; otherwise it is that name's head plus
1540
- `_` and the first six hex digits of the SHA-256 of `<connector>.<tool>`. A
1541
- connector id that does not start with a letter is prefixed with `C`. 48
1542
- characters leave room for a client's own prefix under common 64-character
1543
- tool-name limits (Eve's `connecta__`, Claude Code's `mcp__connecta__`). Two
1544
- tools that would share a name are both left out of the list, with an operator
1545
- warning, and a call by that name is refused; both stay callable by address in
1546
- `execute_code`.
1939
+ **Names.** A name is a pure function of the connector id, the downstream tool
1940
+ name and its served classification, so it survives catalog refreshes until the
1941
+ classification changes. A read is `<connector>__<tool>` and a write
1942
+ `<connector>__WRITE__<tool>` when the tool name uses only `[A-Za-z0-9_-]`,
1943
+ contains no `__`, does not start with `_`, and the whole name fits 48 characters.
1944
+ Otherwise a read is that name's head plus `_` and the first six hex digits of the
1945
+ SHA-256 of `<connector>.<tool>`, and a write is up to 24 characters of the
1946
+ connector, `__WRITE__`, the head of the tool name, `_w` and the same six hex
1947
+ digits. A write whose plain name would end in `_` and six lowercase hex digits,
1948
+ such as a tool named `get_373b87` or `facade`, also takes this hashed form. A
1949
+ connector id that does not start with a letter is prefixed with `C`. 48 characters
1950
+ leave room for a client's own prefix under common 64-character tool-name limits
1951
+ (Eve's `connecta__`, Claude Code's `mcp__connecta__`).
1952
+
1953
+ A name a client holds for a read can never select a write. Every write's name
1954
+ contains `__WRITE__` and never ends in `_` and six lowercase hex digits. A read's
1955
+ name contains `__WRITE__` only when the tool's own name supplies it, and such a read
1956
+ is always hashed, so it ends that way and takes the read route. A tool's read name
1957
+ does not depend on its classification or on this rule, so names clients cached from
1958
+ earlier versions stay read names. Two tools that would share either name are both
1959
+ left out of the list, with an operator warning, and a call by either of their names
1960
+ is refused; both stay callable by address in `execute_code`.
1961
+
1962
+ Read a tool's name from `tools/list` rather than building it. Host approval rules,
1963
+ allowlists, prompts and recipes that name a native write must use its `__WRITE__`
1964
+ name, and a tool that changes classification changes name. Programs keep using the
1965
+ address, which never changes.
1547
1966
 
1548
1967
  **Annotations** come from the served classification only: a read is
1549
1968
  `{ readOnlyHint: true, destructiveHint: false, openWorldHint: true }` and a
1550
1969
  write `{ readOnlyHint: false, destructiveHint: true, openWorldHint: true }`,
1551
- whatever hints the downstream declared. A read-only endpoint or connector
1970
+ whatever hints the downstream declared. A deployment `"read"` override never
1971
+ produces read annotations for a tool whose source explicitly declares a write;
1972
+ `"read-repeatable"` does, and accepts that a host may repeat the call. See
1973
+ [deployment overrides](./integrating.md#another-remote-mcp-server). A read-only endpoint or connector
1552
1974
  leaves write tools out of the list, by the same rule search and describe use.
1553
1975
 
1554
- **Calls take the existing paths.** A call to a downstream tool is the
1555
- `call_tool` call for its address when the tool is currently a read, and the
1556
- `call_destructive_tool` call when it is a write, run by the same handler. So
1557
- classification, `read_only_endpoint` refusal, argument validation and repair,
1558
- admission, redaction, single-attempt writes, auth elicitation, downstream input,
1559
- result caps, and activity use the same enforcement. Activity records source
1560
- `native` with the canonical address and served classification. A name must
1561
- exactly match the definition's native name before dispatch. Unknown or ambiguous
1562
- names fail with `unknown_tool`; catalog failures retain their handoff. Success results are
1976
+ **Calls take the existing paths.** A name selects its route: a read's name is the
1977
+ `call_tool` call for its address, and a write's name is the
1978
+ `call_destructive_tool` call, run by the same handler. So classification,
1979
+ `read_only_endpoint` refusal, argument validation and repair, admission, redaction,
1980
+ single-attempt writes, auth elicitation, downstream input, result caps, and activity
1981
+ use the same enforcement. Activity records source `native` with the canonical
1982
+ address and served classification. Unknown or ambiguous names fail with
1983
+ `unknown_tool`; catalog failures retain their handoff. Success results are
1563
1984
  `call_tool`'s default mode: native `content`, never `structuredContent`, which
1564
1985
  is why no `outputSchema` is advertised. A truncated result's notice pages with
1565
1986
  `connecta.result` inside `execute_code`, or with `read_result` on a pool without
1566
1987
  `execute_code`.
1567
1988
 
1568
1989
  **Approval is the host's, enforcement is Connecta's.** The host approves from
1569
- the annotations of the list it cached. If a tool changes from read to write
1570
- between that list and a call (a downstream changes its hints, or the deployment
1571
- changes its classification), the call runs under Connecta's current verdict:
1572
- on the `call_destructive_tool` route, refused on a read-only endpoint, and never
1573
- resent, although the host approved what it saw as a read. To narrow that
1574
- window, a native `tools/list` carries a 300-second private cache hint, against
1575
- an hour on meta endpoints. This hint does not guarantee that a host refreshes
1576
- its cached annotations; the stale-approval race is accepted on writable pools.
1990
+ the annotations of the list it cached, and some hosts resend calls they believe
1991
+ are read-only when a connection fails. Those resends are new stateless requests:
1992
+ their JSON-RPC ids restart and they carry no session id, so Connecta cannot tell
1993
+ them from new calls. A name is therefore bound to its classification. If a tool
1994
+ changes from read to write between that list and a call (a downstream changes its
1995
+ hints, or the deployment changes its classification), the cached read name takes
1996
+ the `call_tool` route, which refuses before dispatch: `read_only_endpoint` on a
1997
+ read-only endpoint, otherwise `destructive_tool_requires_approval` with
1998
+ `nextAction.tool` set to the write's name. However often the host resends, nothing
1999
+ reaches the downstream. The write runs only by its own name, annotated destructive, after a refreshed
2000
+ list; Claude Code 2.1.318 and Codex 0.166.0 did not resend destructive calls when
2001
+ tested. In the other direction, a cached
2002
+ write name whose tool is now a read still runs, once, on the
2003
+ `call_destructive_tool` route. A native `tools/list` carries a 300-second private
2004
+ cache hint, against an hour on meta endpoints; hosts need not honor it. A listing
2005
+ that omits a connector or lists one stale carries 60 seconds instead, about when
2006
+ that connector is next asked.
2007
+
2008
+ The verdict is Connecta's current catalog: a downstream change takes effect when
2009
+ Connecta's catalog next loads it. A tool whose source declares a write and whose
2010
+ deployment classification overrides it to read has a read's name and takes the
2011
+ `call_tool` route, and a host may resend it; do not override a write to read unless
2012
+ repeating it is harmless. This also applies to `call_tool` on the meta surface.
2013
+
2014
+ **Known limit: a change Connecta has not seen.** Connecta enforces the catalog it
2015
+ holds. A vendor can change a tool from read to write before Connecta's catalog
2016
+ refreshes, and until then every read-annotated route treats that tool as a read:
2017
+ `call_tool`, a read-annotated native tool and programs on a read-only endpoint. A
2018
+ host may also resend such a call after a connection failure, so the vendor can
2019
+ receive it more than once. The catalog refresh closes this window, and nothing
2020
+ closes it sooner.
1577
2021
 
1578
2022
  **Listing.** `tools/list` is one page with no cursor (Eve's MCP client reads only the
1579
2023
  first page). A pool listing more than 1,000 tools fails `tools/list` with a
1580
2024
  JSON-RPC error asking the operator to narrow the pool; nothing is silently
1581
- dropped. A connector whose catalog cannot load is left out of that listing
1582
- whole, never partially, with an operator warning; its tools return with the
1583
- next listing that loads it. Listing never starts authorization. The server
1584
- still declares `tools.listChanged: false`.
2025
+ dropped. A connector that does not answer in time, is `unavailable` or
2026
+ `rate_limited`, or hits a retryable downstream failure stays listed from its last
2027
+ complete catalog, under the same limit and partitioning as
2028
+ [stale listings](#lexical-discovery). Such a connector gets the 5-second deadline, or
2029
+ `discovery.probeTimeoutMs` when shorter, and is skipped for up to 60 seconds after
2030
+ a failure; one with nothing to fall back on keeps the full deadline. Each of its
2031
+ tools carries `_meta["dev.connecta/stale"]` with `code`, `ageMs` and any
2032
+ `retryAfterMs`, and `readOnlyHint: false, destructiveHint: true` whatever its stored
2033
+ classification, because an old catalog cannot vouch for a read (INV-1). The `skills`
2034
+ tool's `dev.connecta/surface` metadata names these connectors in `staleCatalogs`. A
2035
+ `tools/call` always resolves a live catalog, under the verdict it finds.
2036
+
2037
+ A connector whose catalog cannot load and has no stale catalog, or that fails
2038
+ for authorization, permission or a malformed catalog, is left out of that listing
2039
+ whole, never partially, with an operator warning and a `catalogErrors` entry in the
2040
+ same metadata; its tools return with the next listing that loads it. Listing never
2041
+ starts authorization. The server still declares `tools.listChanged: false`.
1585
2042
 
1586
2043
  **Instructions** replace the meta routing text: call one known operation
1587
- directly; with `execute_code`, use one program for several, dependent, or
1588
- reduced calls; write tools are absent and refused on a read-only endpoint;
1589
- never repeat a write; page a truncated result as its notice says. A pool
1590
- `description` is appended as on meta endpoints.
2044
+ directly; with `execute_code`, combine several using each tool's `Program address`;
2045
+ state the endpoint's write policy; on a writable endpoint, never resend a write to
2046
+ see its output; follow `nextAction` on errors and truncated results. A pool
2047
+ `description` is placed first, as on meta endpoints.
1591
2048
 
1592
2049
  **Recovery.** `authorize_connector` remains listed even when a catalog fails.
1593
2050
  Its description lists all granted connector ids independently of catalog health.
@@ -1600,12 +2057,14 @@ Repair and validation fields retain the exact constraint details.
1600
2057
  Discovery `nextAction` points to the host's tool search (`host: "search"`), with
1601
2058
  only a `query` argument. Retry actions and retry text name the native tool,
1602
2059
  never a meta wrapper; a program on a native pool sees the same native wording
1603
- in a failure it catches. An unknown name is reported as sent. Auth actions carry `authorizationUrl` or `operatorUrl`;
1604
- give the returned URL and instructions to the user or operator. Capable modern
2060
+ in a failure it catches. An unknown name is reported as sent. Auth actions carry the link as `nextAction.url`,
2061
+ beside `authorizationUrl` or `operatorUrl`; give it and the instructions to the user or operator. Capable modern
1605
2062
  clients may resume native auth and downstream form/URL input with the same
1606
2063
  wire name, arguments and returned `requestState`. Changed names, targets or
1607
2064
  arguments, expired state and spent retry nonces are refused. Never resend a
1608
- write to recover output.
2065
+ write to recover output. A refusal of a stale read name says nothing was sent:
2066
+ refresh the host's tool list and call the write's own name only if the write is
2067
+ still wanted.
1609
2068
 
1610
2069
  **Schemas.** Ordinary `type: "object"` schemas are byte-identical. Object
1611
2070
  constraints without a top-level type, composed object schemas and local
@@ -1616,8 +2075,9 @@ connectors remain listed. Correct the schema or use the meta surface.
1616
2075
 
1617
2076
  **Diagnostics.** `connecta doctor --pool assistant` recognizes the configured
1618
2077
  surface and code setting from the local `skills` tool's
1619
- `_meta["dev.connecta/surface"]`. It checks the required local tools, reports
1620
- unavailable connector catalogs and listing-cap errors, and runs only a constant
2078
+ `_meta["dev.connecta/surface"]`. It checks the required local tools, fails on
2079
+ unavailable connector catalogs, listing-cap errors and connectors listed stale
2080
+ (`Native discovery degraded`), and runs only a constant
1621
2081
  program when code is enabled. It never dispatches a downstream tool. The
1622
2082
  metadata's `catalogErrors` exposes this request's failing connector ids,
1623
2083
  including connectors with unrepresentable schemas, to every client of the pool.
@@ -1635,7 +2095,47 @@ production catalogs near the cap remain unmeasured.
1635
2095
 
1636
2096
  Starting with 0.35.0, the Eve helper ships separately as `@zackbart/connecta-eve`.
1637
2097
  Connecta core supplies the native pool surface and has no Eve dependency.
1638
- Consult that package's documentation for its compatibility and setup.
2098
+ Consult that package's documentation for its compatibility and setup. The helper
2099
+ composes its Eve-specific guidance from [endpoint facts](#endpoint-facts) and
2100
+ places it before the server's own instructions; Connecta's text stays host-neutral.
2101
+
2102
+ ## Endpoint facts
2103
+
2104
+ `server/discover` and `initialize` both advertise the endpoint in
2105
+ `capabilities.extensions["dev.connecta/endpoint"]`, so a client composing its
2106
+ own guidance can read what this request reached:
2107
+
2108
+ ```json
2109
+ {
2110
+ "surface": "meta",
2111
+ "executeCode": true,
2112
+ "readOnly": false,
2113
+ "connectors": [
2114
+ { "id": "crm", "title": "Customer records", "readFirst": "connector:crm" },
2115
+ { "id": "tickets" }
2116
+ ]
2117
+ }
2118
+ ```
2119
+
2120
+ | Field | Meaning |
2121
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
2122
+ | `surface` | `"meta"` or `"native"`: the surface this request is served |
2123
+ | `executeCode` | Whether `execute_code` is listed. Always `true` on meta |
2124
+ | `readOnly` | The effective policy: the endpoint is read-only, or every connector it reaches is |
2125
+ | `connectors[].id` | Each canonical connector id the caller's scope reaches, in registry order |
2126
+ | `connectors[].title` | Present only when the title says more than the id; normalized and capped as in the [inventory](#discovery-context) |
2127
+ | `connectors[].readFirst` | Present only for a required guide: the name to pass to `skills` |
2128
+
2129
+ The facts come from the admitted scope and deployment code, never from request
2130
+ arguments or `_meta`: a person on plain `/mcp` sees their one pool's connectors,
2131
+ a maintainer sees the whole catalog, and a refused caller gets the usual 404
2132
+ with no facts. The roster is the one `execute_code`'s inventory describes for
2133
+ the same caller, without its byte cap. Reading it loads no catalog, probes no
2134
+ credential, and records no activity. It says nothing about connector health or
2135
+ authorization, and grants nothing.
2136
+
2137
+ A client that does not recognize the extension ignores it. An older server omits
2138
+ it; treat the surface as unknown rather than assuming one.
1639
2139
 
1640
2140
  ## Contributor reference
1641
2141
 
@@ -1649,6 +2149,8 @@ owns the supplied usage instructions, each endpoint's instructions
1649
2149
  (`endpointInstructions`), and connector guides. The native surface's names,
1650
2150
  listing, and call translation live in
1651
2151
  [native-tools.ts](https://github.com/zackbart/connecta/blob/main/src/native-tools.ts).
2152
+ [result-preview.ts](https://github.com/zackbart/connecta/blob/main/src/result-preview.ts)
2153
+ owns the rule behind previews of reads over the cap, as pure functions of redacted text.
1652
2154
 
1653
2155
  [Schema tests](https://github.com/zackbart/connecta/blob/main/test/meta-tool-schemas.test.ts),
1654
2156
  [search tests](https://github.com/zackbart/connecta/blob/main/test/meta-tools-search.test.ts),