@zackbart/connecta 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/bin/connecta.mjs +26 -17
  3. package/bin/meta-tool-names.d.mts +3 -1
  4. package/bin/meta-tool-names.mjs +4 -1
  5. package/bin/version.mjs +1 -1
  6. package/dist/activity.d.ts +32 -5
  7. package/dist/activity.js +32 -6
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-observation.js +24 -2
  10. package/dist/agent-routes.js +33 -44
  11. package/dist/auth/clerk.d.ts +6 -1
  12. package/dist/auth/clerk.js +16 -0
  13. package/dist/auth/cloudflare-access.js +49 -7
  14. package/dist/auth/downstream-oauth/consent.js +79 -29
  15. package/dist/auth/downstream-oauth/grant-store.js +58 -7
  16. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  17. package/dist/auth/downstream-oauth/refresh-records.js +6 -5
  18. package/dist/auth/downstream-oauth.js +15 -4
  19. package/dist/auth/oauth-request-not-sent.js +4 -0
  20. package/dist/auth/static-oauth.js +77 -39
  21. package/dist/auth-elicitation.js +118 -61
  22. package/dist/authorization-handoff.js +36 -3
  23. package/dist/catalog-cache.js +352 -43
  24. package/dist/catalog-freshness.js +25 -0
  25. package/dist/catalog-service.js +408 -78
  26. package/dist/catalog.js +216 -62
  27. package/dist/config-defaults.d.ts +2 -1
  28. package/dist/config-defaults.js +4 -1
  29. package/dist/config.d.ts +53 -21
  30. package/dist/config.js +56 -17
  31. package/dist/connectors/remote-mcp/call.js +1 -1
  32. package/dist/connectors/remote-mcp/catalog.js +52 -28
  33. package/dist/connectors/remote-mcp/connection.js +113 -22
  34. package/dist/connectors/remote-mcp/oauth.js +2 -2
  35. package/dist/connectors/remote-mcp/resources.js +5 -2
  36. package/dist/connectors/remote-mcp/transport.js +24 -6
  37. package/dist/connectors/remote-mcp.js +14 -5
  38. package/dist/credential-contract.d.ts +3 -1
  39. package/dist/describe-config.d.ts +4 -2
  40. package/dist/describe-config.js +1 -0
  41. package/dist/discovery-health.js +26 -11
  42. package/dist/downstream-input.js +11 -2
  43. package/dist/errors.d.ts +60 -4
  44. package/dist/errors.js +88 -3
  45. package/dist/execute.js +491 -202
  46. package/dist/executor-admission.js +2 -0
  47. package/dist/executor-contract.js +6 -0
  48. package/dist/executor-result.js +3 -3
  49. package/dist/executors/quickjs-child.js +14 -5
  50. package/dist/executors/quickjs-runtime.d.ts +2 -0
  51. package/dist/executors/quickjs-runtime.js +37 -20
  52. package/dist/executors/quickjs.js +24 -7
  53. package/dist/guarded-transport.js +2 -0
  54. package/dist/guest-functions.js +5 -4
  55. package/dist/guest-runtime.js +37 -1
  56. package/dist/guest-types.d.ts +18 -0
  57. package/dist/http/client.js +14 -2
  58. package/dist/http/errors.js +9 -4
  59. package/dist/http/fields.js +17 -5
  60. package/dist/http/hatch.js +11 -3
  61. package/dist/http/node-transport.js +110 -4
  62. package/dist/http/oauth.js +17 -6
  63. package/dist/http/paging.js +1 -1
  64. package/dist/http/provider.js +2 -6
  65. package/dist/http/vendor.js +6 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.js +15 -21
  68. package/dist/invocation-failure.js +28 -0
  69. package/dist/invocation.js +167 -64
  70. package/dist/membership.d.ts +1 -1
  71. package/dist/meta-output.js +24 -18
  72. package/dist/meta-tool-names.d.ts +1 -1
  73. package/dist/meta-tool-names.js +1 -1
  74. package/dist/meta-tools.js +286 -73
  75. package/dist/module-contracts.d.ts +3 -1
  76. package/dist/native-recovery.js +4 -1
  77. package/dist/native-tools.js +177 -55
  78. package/dist/node.d.ts +7 -0
  79. package/dist/node.js +85 -4
  80. package/dist/oauth-handoff.js +53 -43
  81. package/dist/operation-activity.js +104 -0
  82. package/dist/operation-vocabulary.js +6 -0
  83. package/dist/operator-record.d.ts +9 -1
  84. package/dist/operator-record.js +16 -0
  85. package/dist/operator-ui/generated.js +2 -2
  86. package/dist/program-result.js +2 -0
  87. package/dist/program-source.js +1 -1
  88. package/dist/program-writes.js +112 -19
  89. package/dist/provider.d.ts +1 -14
  90. package/dist/provider.js +0 -3
  91. package/dist/providers/_shared/google/kit.js +2 -2
  92. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  93. package/dist/providers/breeze/api.js +2 -2
  94. package/dist/providers/breeze/index.js +1 -1
  95. package/dist/providers/breeze/skill.generated.js +1 -1
  96. package/dist/providers/ccb/api.js +2 -2
  97. package/dist/providers/ccb/index.js +1 -1
  98. package/dist/providers/ccb/skill.generated.js +1 -1
  99. package/dist/providers/cloudflare/api.js +2 -2
  100. package/dist/providers/cloudflare/index.js +3 -1
  101. package/dist/providers/cloudflare/skill.generated.js +1 -1
  102. package/dist/providers/docs/index.js +1 -1
  103. package/dist/providers/docs/skill.generated.js +1 -1
  104. package/dist/providers/drive/index.js +1 -1
  105. package/dist/providers/drive/skill.generated.js +1 -1
  106. package/dist/providers/forms/index.js +2 -1
  107. package/dist/providers/forms/skill.generated.js +1 -1
  108. package/dist/providers/github/api.js +98 -6
  109. package/dist/providers/github/index.js +8 -3
  110. package/dist/providers/github/lib/auth.d.ts +3 -1
  111. package/dist/providers/github/lib/auth.js +43 -12
  112. package/dist/providers/github/lib/discovery.js +11 -1
  113. package/dist/providers/github/lib/operations.js +8 -0
  114. package/dist/providers/github/safety.js +39 -0
  115. package/dist/providers/github/skill.generated.js +1 -1
  116. package/dist/providers/github/tools.js +12 -5
  117. package/dist/providers/gmail/index.js +2 -1
  118. package/dist/providers/gmail/skill.generated.js +1 -1
  119. package/dist/providers/granola/index.js +1 -1
  120. package/dist/providers/granola/skill.generated.js +1 -1
  121. package/dist/providers/overflow/index.js +3 -1
  122. package/dist/providers/overflow/skill.generated.js +1 -1
  123. package/dist/providers/planning-center/index.js +12 -6
  124. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  125. package/dist/providers/planning-center/skill.generated.js +1 -1
  126. package/dist/providers/quickbooks/api.js +28 -25
  127. package/dist/providers/quickbooks/index.js +3 -6
  128. package/dist/providers/quickbooks/lib/writes.js +55 -0
  129. package/dist/providers/quickbooks/safety.js +17 -5
  130. package/dist/providers/quickbooks/skill.generated.js +1 -1
  131. package/dist/providers/sheets/index.js +2 -1
  132. package/dist/providers/sheets/skill.generated.js +1 -1
  133. package/dist/providers/slides/index.js +3 -3
  134. package/dist/providers/slides/skill.generated.js +1 -1
  135. package/dist/providers/stripe/index.js +2 -2
  136. package/dist/providers/stripe/skill.generated.js +1 -1
  137. package/dist/providers/tithely/index.js +2 -1
  138. package/dist/providers/tithely/skill.generated.js +1 -1
  139. package/dist/providers/vercel/api.js +3 -2
  140. package/dist/providers/vercel/skill.generated.js +1 -1
  141. package/dist/registry.d.ts +36 -4
  142. package/dist/registry.js +142 -30
  143. package/dist/resource-read.js +21 -2
  144. package/dist/result-preview.js +210 -0
  145. package/dist/result-seal.js +168 -0
  146. package/dist/route-names.js +53 -0
  147. package/dist/routes/activity-context.js +1 -0
  148. package/dist/routes/activity-disclosure.js +26 -4
  149. package/dist/routes/activity.js +2 -1
  150. package/dist/routes/connect.js +21 -13
  151. package/dist/routes/mcp.js +83 -35
  152. package/dist/routes/shared.d.ts +6 -0
  153. package/dist/routes/ui-config.js +1 -1
  154. package/dist/routes/ui-data.js +3 -2
  155. package/dist/runtime/call.js +15 -10
  156. package/dist/runtime/native-storage.js +12 -0
  157. package/dist/runtime/request-reads.js +66 -14
  158. package/dist/runtime/request-scope.js +38 -2
  159. package/dist/runtime/run.js +4 -1
  160. package/dist/sent-secrets.d.ts +20 -0
  161. package/dist/sent-secrets.js +49 -1
  162. package/dist/skill-origin.js +16 -0
  163. package/dist/skills.js +213 -57
  164. package/dist/storage/d1-driver.js +33 -5
  165. package/dist/storage/keys.js +16 -7
  166. package/dist/storage/sql-native.js +60 -3
  167. package/dist/storage/sql.js +6 -0
  168. package/dist/timing-trace.js +84 -0
  169. package/dist/tool-safety.d.ts +33 -0
  170. package/dist/tool-safety.js +51 -15
  171. package/dist/types.d.ts +36 -0
  172. package/dist/url-safety.js +38 -20
  173. package/dist/usage-guide.js +14 -13
  174. package/dist/utility-recovery.js +33 -0
  175. package/dist/validate.js +29 -10
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/dist/worker.js +60 -15
  179. package/documentation/architecture.md +152 -43
  180. package/documentation/auth.md +160 -53
  181. package/documentation/code-mode.md +396 -119
  182. package/documentation/deploying.md +35 -10
  183. package/documentation/hosted-mcp-migration.md +9 -3
  184. package/documentation/integrating.md +27 -5
  185. package/documentation/meta-tools.md +761 -157
  186. package/documentation/operating.md +91 -4
  187. package/documentation/operator-ui.md +51 -2
  188. package/documentation/providers.md +34 -20
  189. package/examples/worker/README.md +3 -3
  190. package/guides/how-it-works.md +2 -1
  191. package/guides/safety-and-trust.md +1 -3
  192. package/package.json +10 -8
  193. package/templates/node/AGENTS.md +3 -3
  194. package/templates/node/README.md +1 -1
  195. package/templates/node/package.json +1 -1
@@ -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
@@ -120,13 +186,16 @@ read-only endpoint nothing writes: `search_tools`, `connecta.search`, and
120
186
  `connecta.describe` omit write tools as if the connector lacked them, and
121
187
  every write call through `call_tool`, `call_destructive_tool`, or a program is
122
188
  refused before validation or dispatch with `read_only_endpoint`,
123
- `retryable: false`, and no `nextAction`. Connector guides are not filtered and
124
- may still name such a write. No route on that endpoint writes; the agent
125
- reports it. Connecta
189
+ `retryable: false`, and an explicit statement that no write route exists here.
190
+ Bootstrap instructions state the serving endpoint’s actual write policy. Connector
191
+ guides preserve their authored Markdown and prepend the effective read-only
192
+ endpoint or connection restriction: steps that change data will be refused.
193
+ The agent reports the limitation and can ask the operator for a writable connection. Connecta
126
194
  never pauses, replays, or approves a program.
127
195
 
128
196
  Both search routes return the flat page specified under
129
- [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.
130
199
  Describe returns an ordered tool list without pagination. Optional row metadata
131
200
  is omitted in this example:
132
201
 
@@ -139,21 +208,28 @@ Live connector probing is not a fourth: the operator pages and `/health` own it.
139
208
 
140
209
  ## Discovery context
141
210
 
142
- The deployment-derived `execute_code` description carries a connector
143
- 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,
144
213
  with no second name minted for programs. An entry adds the configured title
145
214
  only when it says more than the id: a title whose words the id already spells,
146
- such as `Google Calendar` for `google_calendar`, is dropped. Titles normalize
147
- whitespace and cap at 48 UTF-8 bytes. A connector with a guide says how to
148
- fetch it: `read first: skills({name:"connector:<id>"})` for a required guide,
149
- `guide: skills({name:"connector:<id>"})` otherwise. For example:
150
- `Connectors: crm (Customer CRM; read first: skills({name:"connector:crm"})), tickets.`
151
-
152
- The complete line caps at 1,024 UTF-8 bytes, which names every connector of a
153
- typical deployment. Past the cap, detail degrades before names do: titles are
154
- dropped first, then optional guide pointers. Only when bare ids and required
155
- guide pointers still exceed it do whole entries give way, in registry order, to
156
- 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
157
233
  catalog load, no credential probe, no live status, no capability, and no
158
234
  replacement for canonical discovery or addressing. Search pages carry the same
159
235
  bounded title once per connector in `connectors[<id>].title`, so choosing an
@@ -170,7 +246,7 @@ discovery. Both take the same arguments.
170
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. |
171
247
  | `limit` / `offset` | page the ranked results; omit `limit` initially so the default eight-result page stays small |
172
248
  | `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema, `"typescript"` for a function signature ([below](#typescript-signatures)) |
173
- | `fullDescriptions` | unabridged tool purposes, at the obvious cost |
249
+ | `fullDescriptions` | unabridged tool purposes instead of the one-sentence summary, at the obvious cost |
174
250
 
175
251
  Neither `connector` nor `safety` grants authority or changes invocation
176
252
  admission; they select what discovery shows and nothing else.
@@ -179,9 +255,10 @@ Main search also consults a standard hatch's construction-authored operation
179
255
  vocabulary (path, operation id, vendor summary and tag), without calling a tool
180
256
  or reading account data. A match returns the owning `_api_search` tool; call it
181
257
  with the original query to retrieve operations. Exact tool/address and complete
182
- authored-tool matches retain precedence. A vocabulary match requires every term
183
- in one operation, and the operation's hatch route must be visible and match the
184
- 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
185
262
  is still a read. Hidden tools, missing grants and read-only restrictions remain
186
263
  in force. Browsing does not consult operation vocabulary.
187
264
 
@@ -274,9 +351,9 @@ directly; reserve `connecta.describe` for a search without schemas, an ambiguous
274
351
  compact shape, or exact constraints that need `format: "json"`.
275
352
 
276
353
  Compact search is a routing view, not a second copy of connector documentation,
277
- so it spends bytes on shape and none on prose: tool purposes cap at 160
278
- characters, connector descriptions and property prose are dropped, and required
279
- 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
280
357
  its byte and work budgets, its truncation renderings, and its handling of enums,
281
358
  tuples, and conditional keywords are documented once under
282
359
  [`connecta.search`](./code-mode.md#connectasearch) and
@@ -338,13 +415,28 @@ A connector may attach a deployment-owned guide as markdown, keeping the
338
415
  original `usageGuide: string` configuration, or as
339
416
  `{ content, summary?, required? }` — which registers no connector and creates no
340
417
  shared runtime template. The skills catalog prepends Agent Skills frontmatter to
341
- `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.
342
423
  `summary` is normalized and refuses construction over 120 characters; absent,
343
424
  Connecta derives the same bounded fallback the skills listing uses from the
344
425
  guide's first meaningful body paragraph. `required: true` is reserved for
345
426
  generic API wrappers and cross-operation conventions a complete downstream
346
427
  schema cannot express.
347
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
+
348
440
  Describe results carry a `guide: "connector:<id>"` pointer and a
349
441
  `guideSummary`; a search page carries both once per connector in
350
442
  `connectors[<id>]`. A matching tool row or description also carries
@@ -373,7 +465,7 @@ even when a connector has that id, whose guide stays at `connector:usage` or
373
465
  `Connector "<id>" has no usage guide.`; any other unknown name fails as an
374
466
  unknown skill. Names containing `:`, including URIs, never fall back to a bare
375
467
  id. MCP `skills/get` resolves names the same way; `resources/read` accepts only
376
- document URIs.
468
+ document URIs. Canonical names and document/root URIs take priority over compatibility aliases. An alias shared by multiple guides fails as ambiguous; use the canonical URI.
377
469
 
378
470
  A connector-scoped lexical miss keeps that connector's guide metadata under
379
471
  `queryAnalysis`. This matters for generic wrappers whose broad tool name
@@ -405,31 +497,66 @@ connector inventory, budgets, the endpoint's read-only setting, and whether any
405
497
  visible connector has a guide. No catalog outcome, call, authorization state,
406
498
  or other live status reaches `tools/list` or the instructions, so the
407
499
  always-loaded surface stays byte-stable for a client's prompt cache.
408
- The instructions are the same on every endpoint except one addition: at
409
- `/mcp/<pool>` and `/agent/<pool>`, a pool that sets a
410
- [`description`](./auth.md#pools) has it appended after `This endpoint: `, so
411
- the model learns that endpoint's scope, for example
412
- `This endpoint: Team tier: shared tools only. For someone's own mail or files, ask them to message you one-on-one.`
413
- Plain `/mcp` never carries one. The description changes no tool or tool
414
- description, and `initialize` and `server/discover` stay `cacheScope: "private"`,
415
- so a client keeps what it learned per endpoint. Connector
500
+ Instructions reflect the surface and endpoint write policy, subject to connector
501
+ restrictions. When all admitted connectors are read-only, instructions state that
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
416
524
  guides stay scoped to the deployment that listed them even when two deployments
417
525
  use identical content, and a deployment with no connector guides receives none of
418
526
  the short conditional guide pointers in its tool definitions.
419
527
 
420
528
  ### Native Skills extension
421
529
 
530
+ The [Skills extension](https://github.com/modelcontextprotocol/ext-skills) is Beta under
531
+ [SEP-3392](https://modelcontextprotocol.io/seps/3392-extension-lifecycle): breaking
532
+ revisions can reuse its ID. The released settings define no `revision`; Connecta
533
+ refuses downstream declarations with an explicit revision rather than enabling
534
+ unknown semantics. Upward handlers remain Connecta-owned to preserve caller views.
535
+
422
536
  Connecta declares `resources: {}` and `io.modelcontextprotocol/skills`, then
423
537
  serves `skills/list`, `skills/get`, `resources/list` and `resources/read` from
424
538
  the same registry as `skills` and `connecta.skill`. These methods return
425
- `resultType: "complete"`, `ttlMs: 0` and `cacheScope: "private"`. Listing pages
539
+ `resultType: "complete"` and `cacheScope: "private"`. Local-only caller views
540
+ advertise `ttlMs: 300000` (five minutes); any visible downstream skill source
541
+ keeps `ttlMs: 0`, including reads of local guides on that view. Clients must key
542
+ private caches by endpoint and authenticated caller. No request-bound catalog or
543
+ digest promise is retained across requests. Listing pages
426
544
  contain at most five complete skills: usage first, required connector guides
427
545
  next, other connector guides, downstream skills, then the investigation guide.
428
546
  Subsequent pages use `cursor`/`nextCursor`. Local lookup works without listing.
429
547
 
430
548
  The local document URIs are `skill://connecta/usage/SKILL.md` and
431
549
  `skill://connecta/connectors/<slug>/SKILL.md`. A valid Agent Skills connector ID
432
- is its slug; other IDs use `connector-` followed by a stable SHA-256 prefix.
550
+ is its slug; other IDs use a readable normalized ID slug plus a 12-character
551
+ SHA-256 suffix. Generated slugs reserve every configured connector ID and append
552
+ a deterministic numeric suffix on collision. Avoid a connector ID equal to another
553
+ connector's generated name: allocation depends on the IDs a catalog can see, so
554
+ adding one moves the other guide to a suffixed name.
555
+ The former `connector-<48-character-hash>` names, roots and file URIs remain
556
+ resolvable aliases. Construction warns, without rejecting a guide, when its
557
+ summary has no English task verb or operation noun from the advisory heuristic
558
+ (for example, a credential-only scope statement). Set `usageGuide.summary`
559
+ to say when to load it; non-English prose may produce a false-positive warning.
433
560
  The suffix makes each
434
561
  document a valid Agent Skills file; the roots `skill://connecta/usage` and
435
562
  `skill://connecta/connectors/<connector-id>` also resolve. Provider guides are
@@ -459,6 +586,19 @@ from skill manifests; neither skill catalogs nor file bodies enter its response
459
586
  cache. A failed or malformed opted-in listing fails the entire skills catalog,
460
587
  with no partial cache. Known local guides remain readable during an outage.
461
588
 
589
+ On `skills` and program `connecta.skill` reads, downstream text is prefixed with
590
+ a fixed third-party origin sentence naming the connector (bounded to 128 ID
591
+ characters). This prefix has no closing delimiter. `skills` listings carry the
592
+ same sentence before each downstream description, collapsed to one line, in text
593
+ and structured output. Program `connecta.read` applies it to text contents whose
594
+ URI uses the `skill:` scheme or ends in `SKILL.md`; a downstream's other resources
595
+ are returned as ordinary data, and binary contents are not labeled. Maintained and
596
+ deployment guides remain unlabeled. Native `skills/get` metadata and `resources/read`
597
+ contents stay unchanged so original manifest digests remain verifiable; adding a
598
+ label there would break integrity checks. A native host must attach its own origin
599
+ label when it loads verified content into model context, as the Skills extension
600
+ requires.
601
+
462
602
  Downstream files are proxied byte-exact, including CRLF and binary supporting
463
603
  files, within 512 files and 16 MiB per skill. Catalog metadata is bounded to
464
604
  1,024 entries and 8 MiB. The existing request-scoped agent boundary redacts
@@ -486,7 +626,9 @@ dropping the text copy waits on host-forwarding measurements showing supported
486
626
  clients do not need it. Direct-call failures set `isError: true` and carry the
487
627
  same `{ ok: false, error, durationMs, attempts }` envelope in both forms, whether
488
628
  `resultMode` is omitted, `"mcp"`, or `"value"`. Plain-text guidance stays
489
- 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
490
632
  their data only in `content`, with the downstream format in
491
633
  `_meta["dev.connecta/format"]`. They omit `structuredContent`, including on
492
634
  truncated results, so clients that prefer it cannot replace data or a preview
@@ -584,16 +726,106 @@ Completed writes remain successful and must not be repeated. JSON values still
584
726
  round-trip such strings through their escaped JSON representation.
585
727
 
586
728
  A per-call `timeoutMs` covers catalog resolution, admission, and connector
587
- 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
588
735
  sooner but cannot extend the call deadline. Result processing happens after that
589
736
  deadline ends, because a completed downstream call must not turn into a
590
737
  retryable timeout while Connecta prepares its response.
591
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
+
592
824
  ### Truncated direct-call results
593
825
 
594
826
  A `call_tool` or `call_destructive_tool` result over its cap — the connector's
595
- `maxResultBytes`, else `calls.maxResultBytes`, else 24,000 bytes — comes back
596
- 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
597
829
  JSON; everything after the first newline is the preview. `structuredContent`
598
830
  is absent, so the notice cannot hide the preview in clients that prefer it:
599
831
 
@@ -647,16 +879,25 @@ notice is `data` itself, with no preview and a next action at offset 0.
647
879
  The hint says what to do next. For a call not explicitly annotated read-only it
648
880
  opens with “This write already ran: do not call it again to see its result.”
649
881
  The approved write happened; repeating it to look again is how one export
650
- becomes three. For an explicitly read-only call, repeating is the better route,
651
- so the hint offers it first: to find something specific, repeat the read inside
652
- `execute_code` with `connecta.call` and filter or search the result there; to
653
- read it in full, page with `connecta.result`. Inside a program the inline cap does
654
- not apply to a host call's result, only to what the program returns (`L6` in
655
- [code mode](./code-mode.md) bounds a host call far higher), so the reduction
656
- sees the whole result at once. When the hint named paging alone, the eval's
657
- weakest model read a 185 KB CI log in 24,000-byte pages, skipped the range
658
- holding the real failure, and named the flaky test near the top; before the
659
- 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.
660
901
  When paging is available, `nextAction` is the page handle for either kind of call, because paging is
661
902
  the one next step connecta can spell out exactly, while a reduction is a
662
903
  program the agent has to write. `resultId` stays beside the exact `nextAction`,
@@ -671,17 +912,28 @@ no paging route.
671
912
 
672
913
  Meta endpoints page inside a program. Native pools without code list `read_result` for the same bound handles. Program calls also
673
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.
674
925
  `connecta.result(handle, { page: 0 })` accepts those handles alongside direct
675
926
  result IDs. Numbered pages are zero-based and mutually exclusive with offset.
676
927
  Keep maxBytes fixed while incrementing pages. A direct call's
677
928
  notice carries a `resultId`, `nextOffset`, and an executable `execute_code`
678
- 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:
679
931
 
680
932
  ```js
681
933
  async () => {
682
934
  let text = "", offset = 0, page;
683
935
  do {
684
- page = await connecta.result("result-id", { offset, maxBytes: 8_000 });
936
+ page = await connecta.result("result-id", { offset });
685
937
  text += page.text;
686
938
  offset = page.nextOffset;
687
939
  } while (page.hasMore);
@@ -704,6 +956,13 @@ a paging-unavailable notice, without a handle or recovery action. A write's
704
956
  notice still says it already ran. Activity records success; operator logs receive
705
957
  a fixed warning without storage error prose. Never repeat a write to recover output.
706
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
+
707
966
  ## Lexical discovery
708
967
 
709
968
  `search_tools` and in-program `connecta.search` return the same flat discovery
@@ -752,9 +1011,87 @@ to 60 seconds: its entry keeps the recorded code with `retryable: true`,
752
1011
  lookup failed recently (CODE). Search with connector "X" to try it now.` A
753
1012
  scoped search always probes. Records are kept per owner, admitted endpoint and
754
1013
  described connector configuration. A healthy connector whose catalog always
755
- takes more than 5 seconds can stay out of unscoped search; search it with
756
- `connector`, or let any longer-deadline operation warm its complete catalog.
757
- 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.
758
1095
 
759
1096
  Tool rows carry the address, stored classification, and tool-specific
760
1097
  `guideRequired` and `guideRequiredReasons`. Facts shared by every row of one
@@ -769,17 +1106,44 @@ compact schema strings and TypeScript signatures. Compact schemas are text;
769
1106
  `inputSchema.properties` is meaningful only in JSON mode. Pagination counts
770
1107
  tools across connectors: `nextOffset` is present only when another page remains.
771
1108
 
772
- Search tokenizes tool names and descriptions at punctuation and camel-case
773
- boundaries. Whole-token matches and a small set of inflectional variants preserve
774
- recall without admitting arbitrary mid-word substrings. Document frequency weights
775
- rare domain terms above ubiquitous action terms. Exact tool names and canonical
776
- addresses rank first, so `get_issue` beats `get_issue_status` even when a
777
- connector is named `issue`. Exact configured connector IDs, full display titles,
778
- and recognized service names rank next. Complete lexical matches then precede
779
- partial matches. Within a tier, a query whose first word outside connector names
780
- is `list`, `get`, `find`, `search`, `show`, `read`, `fetch`, or `count` ranks
781
- read rows before write rows; writes stay on the page. Score and catalog order
782
- 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.
783
1147
 
784
1148
  A maintained provider may declare `searchSynonyms`: groups of single lowercase
785
1149
  words its users and its vendor use for the same record, such as `household`
@@ -804,8 +1168,9 @@ catalog is a catalog failure, never an absence. Absence records and catalog
804
1168
  errors use only the endpoint's tools; a connector the endpoint does not serve
805
1169
  is indistinguishable from an unconfigured one.
806
1170
 
807
- If no tool covers every search term, wider any-term fallback remains available
808
- 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"`.
809
1174
  A service absence suppresses that fallback, so `GitHub get issue` cannot return
810
1175
  Linear issues or Supabase projects. Tool rows expose no scores or
811
1176
  per-result query coverage. `queryAnalysis` partitions at most eight terms of at
@@ -946,11 +1311,41 @@ behavior is unchanged.
946
1311
  The host echoes `requestState` and its bare `inputResponses.connecta_auth`
947
1312
  response on the original `tools/call`. Accept re-runs the original call; it
948
1313
  does not prove browser consent completed. Decline or cancel ends without
949
- 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
950
1341
  within one ten-minute retry window, then `auth_round_limit` ends the flow.
951
1342
  Each state can be consumed once; concurrent or repeated retries are refused
952
- before dispatch. A Continue link from a pending restart requires that restart
953
- 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.
954
1349
  Host-owned invocation facts record the auth failure's origin and entry into
955
1350
  all calls, including each classification, catalog freshness and classification
956
1351
  digest. Raw fetches and custom connector transports cannot establish recovery
@@ -1000,11 +1395,10 @@ An OAuth call failure uses this envelope:
1000
1395
  "operation": "service.read",
1001
1396
  "recovery": "oauth",
1002
1397
  "authorizationUrl": "https://connecta.example/connect/service?h=...",
1003
- "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; ...",
1004
1399
  "nextAction": {
1005
- "tool": "authorize_connector",
1006
- "arguments": { "connector": "service" },
1007
- "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."
1008
1402
  },
1009
1403
  "retry": "Retry service.read after the operator completes recovery."
1010
1404
  }
@@ -1016,7 +1410,26 @@ returns for the admitted user, connector, and pool. Direct calls include them in
1016
1410
  `error`; program `connecta.call` failures include them in `error.data` and
1017
1411
  `error.details`. Search `catalogErrors` and scoped `queryAnalysis.catalogError`,
1018
1412
  and describe `errorDetails`, carry them too. When an auth error includes a
1019
- 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:
1020
1433
 
1021
1434
  - `oauth`: an `authorizationUrl` to connecta's `/connect/<connector>?h=...` route
1022
1435
  and instructions to sign in as the initiating user;
@@ -1066,7 +1479,9 @@ way to call a tool. On a writable endpoint, `call_tool` reaching an unannotated,
1066
1479
  write-capable, or destructive tool returns `destructive_tool_requires_approval`
1067
1480
  with `nextAction` for `call_destructive_tool` and the canonical address. On a
1068
1481
  read-only endpoint every write path returns `read_only_endpoint` with no
1069
- `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.
1070
1485
 
1071
1486
  `connecta.describe` keeps its failures inline instead, so one miss cannot
1072
1487
  discard the other schemas; each failed entry carries a human `error`, typed
@@ -1129,11 +1544,47 @@ Activity records each of these refusals with the coarse `friction` class derived
1129
1544
  from its typed error code — `tool_not_found`, `schema_retry`,
1130
1545
  `destructive_reroute`, `auth_required` — and clamps the caller-authored
1131
1546
  `connectorId`, `toolName`, and `address` it keeps, both under
1132
- [code mode](./code-mode.md#activity)'s `V2` and `V3`. A `call_tool` result too
1547
+ [code mode](./code-mode.md#activity)'s `V2` and `V3`. Optional
1548
+ [operation history](./operator-ui.md#opt-in-to-operation-history) records
1549
+ search, skill and result reads with fixed names, without arguments or payloads;
1550
+ it is off by default and separate from downstream-call counts. A `call_tool` result too
1133
1551
  large to return inline is the one exception: it is friction
1134
1552
  (`result_too_large`) on a call whose `outcome` is still `"success"`, so it
1135
1553
  carries no `errorCode`.
1136
1554
 
1555
+ Recovery actions outside the tool namespace use `nextAction.action` and a
1556
+ `purpose`: `reconnect_host` repairs inbound HTTP authentication (separate from
1557
+ connector OAuth), `contact_operator` reports catalog or utility availability,
1558
+ `inspect_capabilities` requests a supporting client/operation, and
1559
+ `recover_result` requests a retained handle or an honest missing-output report.
1560
+ Unsupported task, Skills, and resource capabilities use the non-retryable
1561
+ `unsupported_capability` code with `inspect_capabilities`. Missing or expired handles have `code: not_found`; storage outages retain
1562
+ `code: unavailable`. Neither action authorizes anything or replays writes.
1563
+ Unknown skill names and connectors without a guide retain their local lookup
1564
+ diagnostic with `code: not_found` and a `nextAction` calling `skills` with no
1565
+ name to discover available names. They do not request service restoration.
1566
+ The server's search page contract states action discriminators compactly; action
1567
+ advice is additive, while catalog error and authorization handoff fields retain
1568
+ their detailed contracts.
1569
+ A spent read continuation's fresh-call action takes precedence over generic
1570
+ retry advice, while auth and provider permission repair prerequisites and unknown-write reconciliation remain
1571
+ in force. HTTP 401/403 responses preserve the adapter status and challenge. JSON responses
1572
+ carry the host recovery envelope. Custom non-JSON responses preserve their body
1573
+ and lifetime, with the same typed facts in `Connecta-Error-Code`,
1574
+ `Connecta-Recovery`, and JSON-encoded `Connecta-Next-Action` headers (also exposed
1575
+ to cross-origin hosts).
1576
+
1577
+ ## Recovery limits
1578
+
1579
+ Typed `invalid_args` failures without validation findings point to scoped schema
1580
+ inspection. `not_found` asks the caller to verify the resource id, account and
1581
+ visibility. `unavailable` asks the operator to restore connector availability.
1582
+ These use `nextAction` for an executable action and `retry` for explicit limits.
1583
+ An uncertain write has no safe replay action: read its target to reconcile its
1584
+ outcome before deciding on any fresh operation. Host authentication requires
1585
+ reconnecting the host; downstream authorization handoffs repair credentials;
1586
+ provider permission denial requires the resource owner to grant access.
1587
+
1137
1588
  ## Argument recovery
1138
1589
 
1139
1590
  Standard API hatches refuse marker containment in request paths and query
@@ -1220,9 +1671,16 @@ its received types and schema facts. A custom connector's own findings must
1220
1671
  follow the same rule. Operator records contain only checked failure facts,
1221
1672
  never findings or examples.
1222
1673
 
1223
- `nextAction` points to discovery scoped to the same connector and tool name when
1224
- the compact schema is needed, routed like any other miss: a program to
1225
- `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
1226
1684
  the listed arguments and reissue the original operation. What the local
1227
1685
  validator declines to evaluate is [code mode](./code-mode.md#errors)'s `E8`.
1228
1686
 
@@ -1235,6 +1693,15 @@ Unknown-address errors list `configuredConnectors` from the current endpoint's
1235
1693
  scoped registry, including an empty list when none are accessible. They never
1236
1694
  list connectors from another pool or identity's grants.
1237
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
+
1238
1705
  A write that times out after dispatch without complete auxiliary-only proof
1239
1706
  returns `write_outcome_unknown` with
1240
1707
  `retryable: false` and an agent-only `uncertainCall: { address, args?, argsOmitted?, argsRedacted?, recovery? }`.
@@ -1357,7 +1824,7 @@ resent. It is never retryable and carries no wait. Until the credential is
1357
1824
  renewed, the failure keeps its authorization code (`auth_required`, or
1358
1825
  `downstream_oauth_required` for an OAuth connector) with
1359
1826
  `writeOutcome: "unknown"`, `reconciliationRequired: true`, and the
1360
- `authorize_connector` handoff, whose `instructions` say to authorize and then
1827
+ authorization handoff, whose `instructions` and `nextAction` say to authorize and then
1361
1828
  re-read the target before any fresh call. That holds whatever renewal meets: a
1362
1829
  refused or unreachable token endpoint, a disconnect, or the caller leaving. It
1363
1830
  also holds whatever the call met after the 401: a body past the response
@@ -1402,7 +1869,16 @@ in `writes.unknown` even when the program caught the failure. When that write
1402
1869
  met an authorization verdict, the program's error is that verdict, as a direct
1403
1870
  call reports it: its code, `connector`, `recovery`, and `nextAction`, with
1404
1871
  `writeOutcome: "unknown"` and `reconciliationRequired: true`. Any other unknown
1405
- 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.
1406
1882
 
1407
1883
  The classification served for the call decides this advice, including deployment
1408
1884
  `classification` overrides. For example, `classification: { crm: { get_person:
@@ -1428,7 +1904,8 @@ at the downstream boundary, even when the agent may see the error's message.
1428
1904
  A pool set to `surface: "native"` lists each downstream tool as its own MCP
1429
1905
  tool instead of the six meta-tools, for clients that already defer tools behind
1430
1906
  their own search (Eve, Claude Code with tool search). The meta surface stays
1431
- 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.
1432
1909
 
1433
1910
  ```ts
1434
1911
  pools: {
@@ -1442,71 +1919,132 @@ refuses it otherwise, and refuses any `surface` other than `"meta"` or
1442
1919
  `"native"`. Both pool endpoints, `/mcp/<pool>` and `/agent/<pool>`, serve the
1443
1920
  pool's surface.
1444
1921
 
1445
- | A native pool lists | Shape |
1446
- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1447
- | Each visible downstream tool | Named `<connector>__<tool>`; object input constraints and local references preserved; no `outputSchema`; `_meta["dev.connecta/address"]` is its address |
1448
- | `execute_code` | Unless `executeCode: false`. Each downstream tool's description then ends with `Program address: <connector>.<tool>`, the address `connecta.call` takes |
1449
- | `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 |
1450
- | `authorize_connector` | Always; requests the existing typed authorization handoff for a granted connector |
1451
- | `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 |
1452
1929
 
1453
1930
  `search_tools`, `call_tool`, and `call_destructive_tool` are not listed. Auth failures still carry their handoff in the error envelope.
1454
1931
 
1455
- **Names.** A name is a pure function of the connector id and downstream tool
1456
- name, so it survives catalog refreshes. It is `<connector>__<tool>` when the
1457
- tool name uses only `[A-Za-z0-9_-]`, contains no `__`, does not start with `_`,
1458
- and the whole name fits 48 characters; otherwise it is that name's head plus
1459
- `_` and the first six hex digits of the SHA-256 of `<connector>.<tool>`. A
1460
- connector id that does not start with a letter is prefixed with `C`. 48
1461
- characters leave room for a client's own prefix under common 64-character
1462
- tool-name limits (Eve's `connecta__`, Claude Code's `mcp__connecta__`). Two
1463
- tools that would share a name are both left out of the list, with an operator
1464
- warning, and a call by that name is refused; both stay callable by address in
1465
- `execute_code`.
1932
+ Native tool descriptions append `Guide: skills <name>` when their connector has
1933
+ guidance. Call `skills` with `{name:"<name>"}` to read it. The name is the same
1934
+ allocated readable name listed by `skills`, including collision suffixes; a
1935
+ name reserved for a built-in guide uses its canonical connector skill URI. This
1936
+ compact pointer repeats on each tool so host search results retain it; connectors
1937
+ without guides add nothing. It depends only on the admitted connector roster.
1938
+
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.
1466
1966
 
1467
1967
  **Annotations** come from the served classification only: a read is
1468
1968
  `{ readOnlyHint: true, destructiveHint: false, openWorldHint: true }` and a
1469
1969
  write `{ readOnlyHint: false, destructiveHint: true, openWorldHint: true }`,
1470
- 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
1471
1974
  leaves write tools out of the list, by the same rule search and describe use.
1472
1975
 
1473
- **Calls take the existing paths.** A call to a downstream tool is the
1474
- `call_tool` call for its address when the tool is currently a read, and the
1475
- `call_destructive_tool` call when it is a write, run by the same handler. So
1476
- classification, `read_only_endpoint` refusal, argument validation and repair,
1477
- admission, redaction, single-attempt writes, auth elicitation, downstream input,
1478
- result caps, and activity use the same enforcement. Activity records source
1479
- `native` with the canonical address and served classification. A name must
1480
- exactly match the definition's native name before dispatch. Unknown or ambiguous
1481
- 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
1482
1984
  `call_tool`'s default mode: native `content`, never `structuredContent`, which
1483
1985
  is why no `outputSchema` is advertised. A truncated result's notice pages with
1484
1986
  `connecta.result` inside `execute_code`, or with `read_result` on a pool without
1485
1987
  `execute_code`.
1486
1988
 
1487
1989
  **Approval is the host's, enforcement is Connecta's.** The host approves from
1488
- the annotations of the list it cached. If a tool changes from read to write
1489
- between that list and a call (a downstream changes its hints, or the deployment
1490
- changes its classification), the call runs under Connecta's current verdict:
1491
- on the `call_destructive_tool` route, refused on a read-only endpoint, and never
1492
- resent, although the host approved what it saw as a read. To narrow that
1493
- window, a native `tools/list` carries a 300-second private cache hint, against
1494
- an hour on meta endpoints. This hint does not guarantee that a host refreshes
1495
- 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.
1496
2021
 
1497
2022
  **Listing.** `tools/list` is one page with no cursor (Eve's MCP client reads only the
1498
2023
  first page). A pool listing more than 1,000 tools fails `tools/list` with a
1499
2024
  JSON-RPC error asking the operator to narrow the pool; nothing is silently
1500
- dropped. A connector whose catalog cannot load is left out of that listing
1501
- whole, never partially, with an operator warning; its tools return with the
1502
- next listing that loads it. Listing never starts authorization. The server
1503
- 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`.
1504
2042
 
1505
2043
  **Instructions** replace the meta routing text: call one known operation
1506
- directly; with `execute_code`, use one program for several, dependent, or
1507
- reduced calls; write tools are absent and refused on a read-only endpoint;
1508
- never repeat a write; page a truncated result as its notice says. A pool
1509
- `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.
1510
2048
 
1511
2049
  **Recovery.** `authorize_connector` remains listed even when a catalog fails.
1512
2050
  Its description lists all granted connector ids independently of catalog health.
@@ -1519,12 +2057,14 @@ Repair and validation fields retain the exact constraint details.
1519
2057
  Discovery `nextAction` points to the host's tool search (`host: "search"`), with
1520
2058
  only a `query` argument. Retry actions and retry text name the native tool,
1521
2059
  never a meta wrapper; a program on a native pool sees the same native wording
1522
- in a failure it catches. An unknown name is reported as sent. Auth actions carry `authorizationUrl` or `operatorUrl`;
1523
- 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
1524
2062
  clients may resume native auth and downstream form/URL input with the same
1525
2063
  wire name, arguments and returned `requestState`. Changed names, targets or
1526
2064
  arguments, expired state and spent retry nonces are refused. Never resend a
1527
- 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.
1528
2068
 
1529
2069
  **Schemas.** Ordinary `type: "object"` schemas are byte-identical. Object
1530
2070
  constraints without a top-level type, composed object schemas and local
@@ -1535,8 +2075,9 @@ connectors remain listed. Correct the schema or use the meta surface.
1535
2075
 
1536
2076
  **Diagnostics.** `connecta doctor --pool assistant` recognizes the configured
1537
2077
  surface and code setting from the local `skills` tool's
1538
- `_meta["dev.connecta/surface"]`. It checks the required local tools, reports
1539
- 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
1540
2081
  program when code is enabled. It never dispatches a downstream tool. The
1541
2082
  metadata's `catalogErrors` exposes this request's failing connector ids,
1542
2083
  including connectors with unrepresentable schemas, to every client of the pool.
@@ -1554,7 +2095,47 @@ production catalogs near the cap remain unmeasured.
1554
2095
 
1555
2096
  Starting with 0.35.0, the Eve helper ships separately as `@zackbart/connecta-eve`.
1556
2097
  Connecta core supplies the native pool surface and has no Eve dependency.
1557
- 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.
1558
2139
 
1559
2140
  ## Contributor reference
1560
2141
 
@@ -1568,6 +2149,8 @@ owns the supplied usage instructions, each endpoint's instructions
1568
2149
  (`endpointInstructions`), and connector guides. The native surface's names,
1569
2150
  listing, and call translation live in
1570
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.
1571
2154
 
1572
2155
  [Schema tests](https://github.com/zackbart/connecta/blob/main/test/meta-tool-schemas.test.ts),
1573
2156
  [search tests](https://github.com/zackbart/connecta/blob/main/test/meta-tools-search.test.ts),
@@ -1576,3 +2159,24 @@ listing, and call translation live in
1576
2159
  and [native surface tests](https://github.com/zackbart/connecta/blob/main/test/native-tools.test.ts)
1577
2160
  check the published contract. Guest parity and clause evidence are in
1578
2161
  [code-mode verification](./code-mode.md#verification).
2162
+
2163
+ ### Skills conformance (contributor)
2164
+
2165
+ On 2026-11-03, the official [conformance runner](https://github.com/modelcontextprotocol/conformance)
2166
+ at `c37eec888e1c6ff140af79987a40008548b7cc5f` passed enumeration (32 checks)
2167
+ and manifest (6 checks), with no warnings, against Connecta's HTTP handlers
2168
+ and a six-connector local fixture. Directory checks correctly skipped because
2169
+ `directoryRead` is not advertised; its wire-schema check passed. This is not
2170
+ full MCP conformance or a downstream-provider conformance claim.
2171
+
2172
+ To rerun, first read the runner's package scripts, CLI, connection code and
2173
+ `src/scenarios/server/skills/` as untrusted code in a temporary directory
2174
+ outside the checkout. Pin the commit above, install there with
2175
+ `pnpm install --ignore-scripts`, and keep its packages and outputs there.
2176
+ In the Connecta checkout, run `pnpm run build:operator-ui` and
2177
+ `pnpm exec tsx scripts/skills-conformance.ts`. From the runner directory, run
2178
+ `pnpm exec tsx src/index.ts server --url http://127.0.0.1:39743/mcp --scenario sep-2640-skills-enumeration --spec-version 2026-07-28 --force`;
2179
+ repeat with `sep-2640-skills-manifest` and `sep-2640-skills-directory`.
2180
+ Stop the fixture when done. `--force` selects extension scenarios outside the
2181
+ base protocol timeline. This remains an optional contributor check: the
2182
+ separately installed runner and dependency graph are not pinned in Connecta CI.