@axonity-ai/mcp 0.6.0 → 0.8.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 (59) hide show
  1. package/README.md +251 -14
  2. package/dist/client.d.ts +15 -2
  3. package/dist/client.js +8 -1
  4. package/dist/client.js.map +1 -1
  5. package/dist/contract.d.ts +109 -0
  6. package/dist/contract.js +272 -0
  7. package/dist/contract.js.map +1 -0
  8. package/dist/errors.d.ts +15 -3
  9. package/dist/errors.js +27 -3
  10. package/dist/errors.js.map +1 -1
  11. package/dist/generated/listFilters.d.ts +19 -0
  12. package/dist/generated/listFilters.js +78 -0
  13. package/dist/generated/listFilters.js.map +1 -0
  14. package/dist/index.js +29 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/tools/authoringSpec.d.ts +76 -0
  17. package/dist/tools/authoringSpec.js +169 -0
  18. package/dist/tools/authoringSpec.js.map +1 -0
  19. package/dist/tools/company.d.ts +5 -2
  20. package/dist/tools/company.js +79 -2
  21. package/dist/tools/company.js.map +1 -1
  22. package/dist/tools/conventions.d.ts +16 -0
  23. package/dist/tools/conventions.js +299 -19
  24. package/dist/tools/conventions.js.map +1 -1
  25. package/dist/tools/credentials.d.ts +20 -0
  26. package/dist/tools/credentials.js +41 -0
  27. package/dist/tools/credentials.js.map +1 -1
  28. package/dist/tools/extras.d.ts +17 -0
  29. package/dist/tools/extras.js +151 -9
  30. package/dist/tools/extras.js.map +1 -1
  31. package/dist/tools/operations.d.ts +28 -0
  32. package/dist/tools/operations.js +160 -0
  33. package/dist/tools/operations.js.map +1 -0
  34. package/dist/tools/register.d.ts +27 -0
  35. package/dist/tools/register.js +19 -1
  36. package/dist/tools/register.js.map +1 -1
  37. package/dist/tools/runs.d.ts +24 -0
  38. package/dist/tools/runs.js +304 -26
  39. package/dist/tools/runs.js.map +1 -1
  40. package/dist/tools/secrets.d.ts +33 -0
  41. package/dist/tools/secrets.js +69 -0
  42. package/dist/tools/secrets.js.map +1 -0
  43. package/dist/tools/subworkflows.d.ts +20 -0
  44. package/dist/tools/subworkflows.js +42 -0
  45. package/dist/tools/subworkflows.js.map +1 -0
  46. package/dist/tools/toolboxes.d.ts +32 -0
  47. package/dist/tools/toolboxes.js +165 -0
  48. package/dist/tools/toolboxes.js.map +1 -0
  49. package/dist/tools/triggers.d.ts +8 -0
  50. package/dist/tools/triggers.js +65 -7
  51. package/dist/tools/triggers.js.map +1 -1
  52. package/dist/tools/validation.js +74 -4
  53. package/dist/tools/validation.js.map +1 -1
  54. package/dist/tools/versions.d.ts +9 -0
  55. package/dist/tools/versions.js +36 -0
  56. package/dist/tools/versions.js.map +1 -1
  57. package/dist/tools/workflowMutations.js +53 -12
  58. package/dist/tools/workflowMutations.js.map +1 -1
  59. package/package.json +3 -1
package/README.md CHANGED
@@ -40,10 +40,16 @@ tenant + scope on every call, so the connector is not a trust boundary.
40
40
 
41
41
  ## Tools
42
42
 
43
- 200+ tools total. `axonity_conventions` (read this first) covers the authoring
43
+ 220+ tools total. `axonity_conventions` (read this first) covers the authoring
44
44
  rules — drafts vs live, optimistic locking, per-entity fields, delete/restore,
45
45
  and how to tell a retryable error from one that will never succeed.
46
46
 
47
+ What the connector does **not** state is as deliberate as what it does: the
48
+ mutation commands, the step types, the trigger types, the schedule-rule shapes
49
+ and the three value vocabularies are all read live from
50
+ `get_workflow_authoring_spec`, because every one of those lists drifted while it
51
+ was kept here. A conformance test asserts their absence.
52
+
47
53
  ### The generic entity family
48
54
 
49
55
  Ten entities — **workflow, agent, tool, skill, policy, reference_doc, persona,
@@ -65,12 +71,45 @@ entity gets every verb (see the per-entity notes below for the exceptions):
65
71
  Exceptions: `persona` has no `create_persona` (create only via
66
72
  `create_agent_persona`).
67
73
 
74
+ Some list routes narrow in the query, which is where narrowing belongs — the
75
+ backend applies it before the rows come back:
76
+ `list_workflows({ stageId?, capabilityId? })`,
77
+ `list_agents({ includeSystem? })`,
78
+ `list_policies({ scope?, ownerId? })`,
79
+ `list_reference_docs({ scope?, ownerId? })`,
80
+ `list_prompt_snippets({ deleted? })`.
81
+
82
+ That table is **generated** from the pinned schema's own query parameters
83
+ (`npm run generate:filters` → `src/generated/listFilters.ts`), not hand-listed.
84
+ A filter the backend adds is exposed as soon as the snapshot is refreshed, and
85
+ a test fails if the two have parted. The argument names are camelCase; the wire
86
+ keeps whatever spelling each route declares, which is not consistent between
87
+ them and is not something a caller should have to know.
88
+
68
89
  Plus:
90
+ - `get_workflow_authoring_spec` — everything **this deploy** can be built from,
91
+ read live from the server (`GET /workflows/operations`): the mutation
92
+ `operations`, the `triggerTypes`, the `stepTypes` (including the ones you may
93
+ not author, each with the reason and what to write instead), the
94
+ `scheduleRuleKinds` (each with an example the backend round-trips through its
95
+ own parser as it serves it), and **three vocabularies for a value's type that
96
+ are not interchangeable** — `parameterTypes` (a trigger parameter's or
97
+ workflow constant's `type`), `outputKinds` (a step output's or input's
98
+ `kind` — narrower, *different key*) and `schemaFieldKinds` (inside a field's
99
+ `schema`, which wins where present). Getting that last group wrong is silent:
100
+ a `kind` written on a trigger parameter is not a 422, it is a key nothing
101
+ reads, so the value falls back to text. Every list is generated from the
102
+ registry that *enforces* it, so the connector states none of them and a new
103
+ value is discoverable without a release here. `operations` is an index by
104
+ default; pass `types` for a command's live payload schema. `rulesVersion` is
105
+ a content hash over all seven — same hash, nothing to re-fetch.
69
106
  - `apply_workflow_mutations` for structural workflow edits (add steps, connect
70
107
  edges) via mutation commands, sequenced and version-threaded for you.
71
108
  - `replace_workflow_document` for one-shot full-document replacement in a single
72
109
  atomic PUT.
73
- - `read_workflow_trigger_parameters` — what input a workflow's triggers expect.
110
+ - `read_workflow_trigger_parameters` — how to start the workflow:
111
+ `{ triggers, constants }`, every start with its own parameters and a `pinned`
112
+ flag marking the values the author owns.
74
113
  - `bulk_delete_workflows` — soft-delete several at once (each with its own
75
114
  `expectedVersion`).
76
115
 
@@ -87,7 +126,9 @@ For the ten versioned entities (including `flow`):
87
126
  | `list_deleted_<entity>_versions` | Restore candidates for the row above. |
88
127
  | `restore_deleted_<entity>_version` | Undo the delete above. No version check. |
89
128
  | `read_<entity>_published` | The live snapshot, as opposed to the draft. |
90
- | `name_<entity>_major_version` | Give a major version a name (label a release). |
129
+ | `create_<entity>_major_version` | Cut a new **named** major version — "Save As" on the current draft. |
130
+ | `ensure_<entity>_major_version` | Make sure a working draft major version exists. Idempotent. |
131
+ | `name_<entity>_major_version` | Rename an existing major version (label a release). |
91
132
 
92
133
  `{version}` (an int) and `{versionId}` (a UUID) are two different identifiers
93
134
  across these routes — the tool parameter names say which.
@@ -102,12 +143,32 @@ across these routes — the tool parameter names say which.
102
143
  `update_connector` — `authConfig` must be placeholders only; a human fills real
103
144
  secrets in Axonity. (`create_tool`/`update_tool` carry the same guard, so a
104
145
  connector authored either way is covered.)
146
+ - **Toolboxes** (the group a tool is filed under): `list_toolboxes`,
147
+ `create_toolbox`, `update_toolbox`, `delete_toolbox`, `set_toolbox_tools`,
148
+ `assign_tool_toolbox`, `set_toolbox_auth`, `list_toolbox_dependent_tools`.
149
+ Read `list_toolboxes` before `create_tool` and pass `toolboxId` — a tool made
150
+ without one is ungrouped. Three things worth knowing before you write:
151
+ `set_toolbox_tools` **declares** the membership (anything you leave out is
152
+ evicted — `assign_tool_toolbox` moves a single tool and takes `null` to
153
+ ungroup); a box never changes what an agent may *call*, only how a tool is
154
+ *advertised* (agents link to individual tools, never to a box); and deleting a
155
+ box leaves its tools alive, ungrouped. `set_toolbox_auth` sets the credential a
156
+ box's tools share and carries the same placeholder guard as a connector.
105
157
  - **Attach / detach memory**: `attach_skill_to_agent`,
106
158
  `attach_skill_to_workflow`, `attach_policy_to_agent`,
107
159
  `attach_reference_to_agent`, and a `detach_*_from_*` for each. Detaching
108
160
  removes the link only — the skill or policy itself is untouched. Read the
109
161
  links back with `list_agent_skills`, `list_agent_policies`,
110
- `list_agent_reference_docs`.
162
+ `list_agent_reference_docs` and `list_workflow_skills`. The three agent
163
+ read-backs take an optional `workflowId` for the **composed runtime view** —
164
+ what the agent's prompt actually assembles inside that workflow, with a
165
+ `linkSource` per row saying why each item is there.
166
+ - **What uses this?**: `list_workflows_using({ entityKind, entityId })` names
167
+ the workflows that reference a tool, agent, flow, output schema or workflow,
168
+ **and the steps they reference it in**, with `draft`/`published` per hit.
169
+ `list_dependent_agents({ entityKind, entityId })` is the same question for a
170
+ skill, policy or reference doc. Ask before editing anything shared — the
171
+ alternative is validating every workflow in the tenant.
111
172
  - **Prompt elements (placement)**: a `prompt_snippet` is a library item; it only
112
173
  takes effect once placed into a flow step's prompt stack.
113
174
  `read_workflow_prompt_stacks` / `read_flow_prompt_stacks` resolve a
@@ -119,13 +180,46 @@ across these routes — the tool parameter names say which.
119
180
  - **Company** (the tenant's single company document — a singleton, no id):
120
181
  `read_company`, `update_company` (whole-document save with `expectedVersion`),
121
182
  `list_company_versions`, `read_company_version`, `restore_company_version`,
122
- `name_company_major_version`, `read_company_published`, and
183
+ `name_company_major_version`, `create_company_major_version`,
184
+ `ensure_company_major_version`, `read_company_published`,
185
+ `apply_company_mutation` (one validated, version-safe command — preferred
186
+ over the whole-document `update_company`, the same way
187
+ `apply_workflow_mutations` is preferred for a workflow), and
123
188
  `request_publish_company` (takes no id — the
124
189
  server resolves your tenant's one company; direct company publish is closed to
125
190
  service tokens).
191
+ - **Subworkflows**: `list_callable_workflows` — which workflows a `subprocess`
192
+ step may call, each with its `parameters` and `outcomes`, and a
193
+ `blockedReason` for the ones that cannot (never published, or no
194
+ `subprocess-invocation` trigger). Authoring both halves is ordinary
195
+ `apply_workflow_mutations` work — a callable workflow's signature goes into
196
+ the `add_trigger` call itself. `validate_workflow` **does** check a subprocess
197
+ target now (missing, self-call, deleted, unpublished, not callable), but
198
+ `list_callable_workflows` is still what you run first: it is how you pick a
199
+ target and read the interface you are binding to.
200
+ - **The deploy, the tenant and its queues** (read-only): `read_deploy_contract`
201
+ (what this backend actually mounts — read it when a call fails in a way that
202
+ smells like a version mismatch), `list_users` (where an `ownerId` comes from),
203
+ `list_audit_events` (pass `actorKind: "service_token"` to read back what
204
+ external agents — including you — changed), `read_queue_overview`,
205
+ `list_in_flight_runs`, `read_task_queue_summary`, `list_task_queue`,
206
+ `read_task_queue_item`, `export_task_queue`, plus `read_model_tier_map` (what
207
+ `capabilityTier` resolves to), `read_concurrency_status`,
208
+ `read_concurrent_run_cap` and `read_for_each_rate`. Together these answer *why
209
+ is my run not moving* without asking a human to look at a screen. Every write
210
+ in these families — purging or replaying queue work, changing a cap or the
211
+ tier map, importing a tenant bundle, reading someone's notifications — is
212
+ deliberately absent and recorded in `test/exclusions.test.ts`.
213
+ - **Secrets** (read-only): `list_secrets`, `read_secret` — the catalogue a
214
+ connector's `authConfig.secretId` points at. Values are never returned by any
215
+ Axonity route; `valueKeys` says which keys a human has filled in, so you can
216
+ tell an unfinished secret from a finished one before wiring to it. Creating or
217
+ changing a secret is a human act in Axonity (#39).
126
218
  - **Catalog & cloning**: `list_system_tools` (read-only catalog — enabling one
127
219
  for an agent is `update_agent` with the id added to `systemToolIds`),
128
- `clone_flow`, `clone_prompt_snippet`.
220
+ `clone_flow`, `clone_prompt_snippet`, `list_tool_packages` (the import
221
+ allowlist `validate_tool_code` judges against), `list_templates` /
222
+ `read_template`.
129
223
  - `list_deleted_prompt_snippets` calls `/api/v1/prompt-snippets/deleted`; the
130
224
  backend returns it as `{ items: [... ] }`, and the tool forwards that response
131
225
  unchanged.
@@ -148,6 +242,24 @@ across these routes — the tool parameter names say which.
148
242
  (plus `update_conditional_trigger`). Trigger deletes are **hard** deletes with
149
243
  no restore, and a webhook token is shown **once** at create or rotate.
150
244
 
245
+ **A schedule is a claim you can now check.** `run_cron_schedule_now` fires one
246
+ immediately *without* moving `nextFireAt` — testing a schedule must not consume
247
+ the run it was going to make. Before it existed, "every weekday at 07:00" could
248
+ only be tested by coming back tomorrow, and what is usually wrong is not the
249
+ timing but whether it starts anything at all.
250
+
251
+ **To pause a schedule, disarm it** — `set_cron_schedule_enabled`, not
252
+ `delete_cron_schedule`. Deleting throws away the rules the author wrote and
253
+ makes "stop this for a week" indistinguishable from "we do not do this any
254
+ more". `list_all_cron_schedules` answers *what runs tonight?* across the tenant;
255
+ `reconcile_cron_schedules` answers *is that actually what runs?* — it reports
256
+ rather than tidying silently, and never arms something someone switched off.
257
+
258
+ `create_cron_schedule` takes either `cronExpr` or the richer `rules`, and the
259
+ trigger must exist in the **published** document. Rule shapes come from
260
+ `get_workflow_authoring_spec` → `scheduleRuleKinds`; this connector names none
261
+ of its own.
262
+
151
263
  ### Runs — evaluating what you built
152
264
 
153
265
  `start_workflow_run` (test a workflow you built — it runs the **published**
@@ -157,14 +269,66 @@ workflow and really executes), `cancel_run`, `delete_run`, `list_runs`,
157
269
  `bulk_delete_runs`. There is no findings endpoint — evaluation means reading a
158
270
  run's validator verdicts and its trace.
159
271
 
160
- `list_workflow_runs({ workflowId, archivedOnly?, limit?, cursor? })` returns one
272
+ **Which start, and what it wants.** `read_workflow_trigger_parameters` answers
273
+ `{ triggers, constants }` — every way the workflow can be started, each with its
274
+ own parameters. Pass the one you mean to `start_workflow_run` as `triggerId`;
275
+ omitting it fires the first, which on a workflow with a button *and* a schedule
276
+ is an arbitrary choice. A parameter marked `pinned` is one the **author** owns:
277
+ it is overwritten on every run, so a caller must not send it.
278
+
279
+ **`read_run` omits the workflow snapshot by default.** It is immutable, it is
280
+ never the answer to a question about the run, and it measured 81% of one real
281
+ response — an oversized response turns a call that succeeded into an error.
282
+ Pass `includeSnapshot: true` when you actually want to see what executed.
283
+
284
+ **Start from the outline.** `read_run_outline` is the run's table of contents —
285
+ the run, its steps, and the items a fan-out handed out, flat with parent
286
+ pointers. It carries no bodies, so its size follows the run's *shape* rather
287
+ than its content: a launch over four thousand items costs about what one over
288
+ four costs. `itemCap` bounds the items listed per fan-out step and the remainder
289
+ is counted in `counts.truncated`, never dropped silently. Then open only what
290
+ you want: `read_run_value` for one large step value (by the digest in
291
+ `stepStates`) and `read_run_invocation_messages` for one agent's transcript (by
292
+ the id in `agentInvocations`). Reading a whole run to find one message is the
293
+ habit these replace.
294
+
295
+ **A run can park rather than finish.** `read_run_waiting_on` says what it is
296
+ waiting for; `answer_run_question` answers an `ask_user` step and
297
+ `send_run_message` sends a turn to a conversation run. Both record the input as
298
+ a person's, so use them on runs you started. Deciding a **plan approval** and
299
+ restarting a stuck run are deliberately absent — the first is the human review
300
+ the step exists to get, the second is `require_admin` and a service token is
301
+ always `role="member"`. Both are recorded in `test/exclusions.test.ts`, together
302
+ with stopping runs in bulk (admin), the tenant's storage footprint (admin), an
303
+ inbound channel reply (authenticated by the email/WhatsApp adapter, not by a
304
+ service token) and the retired `workflow-memory` placeholder.
305
+
306
+ **`list_todo_steps`** is the same question across the whole tenant: every step
307
+ waiting on a human, in any run. It is paged over the waiting **runs**, so
308
+ `items` can be longer than `pageSize` — one run may park several steps. Follow
309
+ `nextCursor` to the end before concluding anything about how much is waiting.
310
+
311
+ **What an agent wrote to itself.** `list_run_session_memory` lists the files an
312
+ agent left during a run (metadata only, up to 200 per run) and
313
+ `read_run_session_memory_file` opens one. When the trace shows a decision but
314
+ not what it was reading, the reason is usually here. Workflow-bound reference
315
+ material is not — that lives in `reference_docs`.
316
+
317
+ **Inside a launch**: `read_run_items_summary` is the roll-up ("4,415 processed ·
318
+ 12 failed"), `list_run_items({ outcome })` the paged rows — filter in the query,
319
+ because the failures are scattered and sifting page one finds none of them.
320
+ `list_run_tasks` and `read_run_for_each_progress` cover the children a run set
321
+ in motion.
322
+
323
+ `list_workflow_runs({ workflowId, status?, archivedOnly?, limit?, cursor? })` returns one
161
324
  **page** — `{ items, nextCursor, pageSize, hasMore }`, 20 by default and 200 at
162
325
  most — so follow `nextCursor` while `hasMore` is true rather than treating the
163
326
  first page as the answer. It also lists **launches**, not runs: the per-item runs
164
327
  a FOR EACH creates stay inside their launch, so a launch over 4,415 people is one
165
- entry carrying `forEachProgress`. `list_runs` is the tenant-wide list and is
166
- *not* paged this way — it answers with a bare array, capped at 200, walked with
167
- `offset`.
328
+ entry carrying `forEachProgress`. `list_runs` is the tenant-wide list and is now
329
+ paged the same way — `{ items, nextCursor, pageSize, hasMore }`, walked with
330
+ `cursor`. It stopped answering with a bare array when the backend converted the
331
+ route; the `offset` it used to take is no longer read.
168
332
 
169
333
  ### Approvals
170
334
 
@@ -173,6 +337,23 @@ entry carrying `forEachProgress`. `list_runs` is the tenant-wide list and is
173
337
  `request_publish_*` was approved or rejected. Approving and rejecting are
174
338
  human-only actions in Axonity.
175
339
 
340
+ `request_publish_release({ workflowId, changeSummary? })` proposes a **release**:
341
+ a workflow *and everything its run needs* — the agents it runs, their tools and
342
+ personas, the flows it pins, the memory scoped to those agents — as ONE approval.
343
+ Prefer it over a request per entity. Taking a tenant live entity-by-entity means
344
+ a hundred-odd approvals, none of which means anything on its own, and a human
345
+ asked that many times is not reviewing. `list_publish_releases` and
346
+ `get_publish_release` read one back — the release's members, and its readiness
347
+ recomputed as of now.
348
+
349
+ Unlike `request_publish_bulk`, a release is **all-or-nothing and in dependency
350
+ order**: approving it publishes every member or none, so a workflow can never go
351
+ live calling a tool that did not. The response carries the bundle's verdict —
352
+ `ready`, `changedCount` of `totalCount` (unchanged members are already live and
353
+ ride along), `members` with why each is there, and `blockers` that name the
354
+ member in the way. Requesting is ours; deciding stays human, like everywhere
355
+ else here.
356
+
176
357
  ## What this is for — and what it is not
177
358
 
178
359
  **Authoring.** An agent composing an entity from intent: drafting a workflow,
@@ -202,19 +383,34 @@ These are enforced by the backend, not merely by convention:
202
383
  - **Secrets never pass through the agent.** A connector's `authConfig` accepts
203
384
  placeholders only; a write carrying something that looks like a real
204
385
  credential is rejected before it leaves the connector. Tenant secrets
205
- (`/api/v1/secrets`) aren't wrapped at all — the backend refuses any service
206
- token there outright.
386
+ (`/api/v1/secrets`) are **readable and unwritable**: `list_secrets` /
387
+ `read_secret` give the catalogue and `valueKeys` (which keys are filled, never
388
+ their values) so an agent can point `authConfig.secretId` at the right entry,
389
+ and no tool can create, change or delete one — the backend refuses a service
390
+ token there too. A secret's `metadata` is stored unencrypted and readable
391
+ tenant-wide (axonity-flow#908), so credential-shaped entries in it are
392
+ withheld on read and listed under `metadataRedacted`.
207
393
  - **Errors carry a machine-readable `code`, not just prose.** A 409 can mean a
208
394
  stale write (retry) or a live reference conflict (don't — see
209
395
  `axonity_conventions`); the connector tells them apart by `code`, never by
210
396
  matching the message text.
211
397
  - **No tool crosses the authority boundary.** A test drives the whole registered
212
- surface and fails the build if any tool targets a publish / approve / secret /
213
- service-token / deploy route (`test/exclusions.test.ts`).
398
+ surface and fails the build if any tool targets a publish / approve /
399
+ secret-write / service-token / deploy route (`test/exclusions.test.ts`). The
400
+ rules are method-aware: `GET /api/v1/secrets` is allowed, every write verb on
401
+ it is not.
214
402
  - **Guidance can't silently drift from the backend.** The field/enum facts the
215
403
  connector states are pinned to a vendored snapshot of the backend OpenAPI
216
404
  schema; `test/conformance.test.ts` fails if a route or documented enum
217
405
  diverges (see `test/fixtures/README.md`).
406
+ - **Every backend route is decided, and the build says so.**
407
+ `test/completeness.test.ts` partitions all 444 operations in the pinned
408
+ snapshot into *covered by a tool* or *excluded by a rule that carries a
409
+ written reason*, and fails on anything in neither. A route nobody has decided
410
+ about is indistinguishable from one somebody is still working on, which is
411
+ what made "is this connector finished?" a question you could only answer with
412
+ an audit. It is now a build status: a new backend route arrives as a red build
413
+ asking **cover it, or exclude it with a reason?**
218
414
 
219
415
  **One known exception, not enforced:** a framework-provided `flow` is meant to
220
416
  be read-only to a tenant, but the backend does not actually block
@@ -236,6 +432,18 @@ Publishing runs from a maintainer's machine, not from CI. npm restricts tokens
236
432
  that bypass 2FA for direct publishing, so a stored `NPM_TOKEN` cannot ship this
237
433
  package; `npm login` is the supported path.
238
434
 
435
+ **Check the contract first.** Shipping with a stale snapshot is how the
436
+ connector fell nine operations behind the backend without any test noticing:
437
+
438
+ ```bash
439
+ npm run check:contract # expects ../axonity-flow; override with AXONITY_FLOW_PATH
440
+ ```
441
+
442
+ It dumps the schema from a local `axonity-flow` checkout and diffs it against
443
+ `test/fixtures/openapi.snapshot.json`, naming every operation that moved. It
444
+ runs here rather than in the release workflow because the publish itself runs
445
+ here — a gate in CI cannot stop a local `npm publish`.
446
+
239
447
  Run `npm login` in a real terminal — it prints a URL and waits for you to finish
240
448
  in the browser, so it needs a session that stays attached (an editor's 2-minute
241
449
  command timeout will kill it mid-flow).
@@ -257,3 +465,32 @@ was cut from a state CI cannot install.
257
465
 
258
466
  Keep npm 11 locally: Node 20 bundles npm 10, whose resolver writes an
259
467
  incompatible lockfile tree. CI pins npm 11 for the same reason.
468
+
469
+ ## Staying level with the backend
470
+
471
+ The snapshot every drift guard reads is only as fresh as the last time someone
472
+ regenerated it — and once it wasn't: it sat nine operations behind the backend
473
+ while the whole suite stayed green, because a snapshot that has not seen a route
474
+ cannot report it missing.
475
+
476
+ **Before a release, check it** (see §Releasing): `npm run check:contract` dumps
477
+ the schema from a local `axonity-flow` checkout and names every operation that
478
+ moved. It compares the contract surface — query parameters, request body,
479
+ response shape — and not descriptions, so backend docstring churn does not cry
480
+ wolf. This is the gate that counts, because the publish runs here too.
481
+
482
+ **At startup, the connector asks the deploy what it mounts.** It calls
483
+ `GET /api/v1/contract` once before accepting its first tool call and, if this
484
+ backend lacks a route this build needs, says which on stderr — instead of
485
+ failing on the twentieth call, mid-task. It is a diagnostic and never a
486
+ dependency: a backend too old to serve `/contract`, an unreachable one, or a
487
+ slow one all degrade to the previous behaviour and the connector starts
488
+ normally. The result is cached on the `contractHash` the route returns, so an
489
+ unchanged deploy costs one request.
490
+
491
+ **Not here: a scheduled job.** Watching for drift on a timer belongs in
492
+ `axonity-flow`, not in this repository. The schema lives there, its CI already
493
+ has the backend's dependencies installed, and it can read this repository's
494
+ pinned snapshot over plain HTTPS because this repository is public — where the
495
+ reverse needs a credential for a private repo, with an approval policy and an
496
+ expiry behind it. Tracked in axonity-mcp#48.
package/dist/client.d.ts CHANGED
@@ -8,8 +8,20 @@
8
8
  */
9
9
  import type { AxonityConfig } from "./config.js";
10
10
  export type Json = Record<string, unknown>;
11
- /** Query-string values a route may take. `undefined` entries are omitted. */
12
- export type Query = Record<string, string | number | boolean | undefined>;
11
+ /** One scalar a query parameter may carry. */
12
+ type QueryScalar = string | number | boolean;
13
+ /**
14
+ * Query-string values a route may take. `undefined` entries are omitted.
15
+ *
16
+ * An ARRAY is serialised the way FastAPI reads a `list[…]` query parameter:
17
+ * the key REPEATED, once per element (`?status=pending&status=running`). Not
18
+ * every list-shaped filter on the backend is spelled that way — `GET /runs`
19
+ * takes its statuses as one comma-separated string while
20
+ * `GET /workflows/{id}/runs` repeats the key — so the caller picks the shape
21
+ * its route actually declares. An empty array contributes nothing, which is
22
+ * what "no filter" means.
23
+ */
24
+ export type Query = Record<string, QueryScalar | QueryScalar[] | undefined>;
13
25
  export declare class AxonityClient {
14
26
  private readonly config;
15
27
  constructor(config: AxonityConfig);
@@ -31,3 +43,4 @@ export declare class AxonityClient {
31
43
  */
32
44
  del<T = unknown>(path: string, query?: Query): Promise<T>;
33
45
  }
46
+ export {};
package/dist/client.js CHANGED
@@ -18,8 +18,15 @@ export class AxonityClient {
18
18
  return base;
19
19
  const params = new URLSearchParams();
20
20
  for (const [key, value] of Object.entries(query)) {
21
- if (value !== undefined)
21
+ if (value === undefined)
22
+ continue;
23
+ if (Array.isArray(value)) {
24
+ for (const item of value)
25
+ params.append(key, String(item));
26
+ }
27
+ else {
22
28
  params.set(key, String(value));
29
+ }
23
30
  }
24
31
  const qs = params.toString();
25
32
  return qs ? `${base}?${qs}` : base;
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Asking the deploy what it can do, once, at startup (axonity-mcp#48 M2).
3
+ *
4
+ * `src/errors.ts` already turns a 404/405 into a "backend version skew" message
5
+ * — but only AFTER the call has failed, which means a user is mid-task on the
6
+ * one call that mattered. And it decides which 404s count from `ROUTES_ADDED_IN`,
7
+ * a list this repository keeps by hand about routes the server owns. That is the
8
+ * shape of #32 and #44, and it has the same failure mode: a route added here
9
+ * without an entry there reports as "no such id" forever.
10
+ *
11
+ * `GET /api/v1/contract` (axonity-flow#806) is the proactive replacement. It
12
+ * returns every mounted route, derived from the running app so it cannot drift,
13
+ * plus a hash to cache on. It is authenticated but TENANT-AGNOSTIC — a
14
+ * read-only service token is enough, so this check never needs write scope.
15
+ *
16
+ * WHAT THIS DOES NOT DO: fail. A connector that refuses to start because it
17
+ * could not reach a diagnostic endpoint is worse than the drift it reports —
18
+ * every path here degrades to today's behaviour, including a backend too old to
19
+ * serve `/contract` at all. Everything it says goes to STDERR, because stdout is
20
+ * the MCP channel and a stray byte there breaks the protocol.
21
+ */
22
+ import type { AxonityClient } from "./client.js";
23
+ /** `GET /api/v1/contract`. `routes` are "METHOD /path/{template}" strings. */
24
+ export interface ContractResponse {
25
+ buildVersion?: unknown;
26
+ environment?: unknown;
27
+ routes?: unknown;
28
+ contractHash?: unknown;
29
+ }
30
+ /** The minimal `.tool()` surface the registrar needs — same shape as index.ts. */
31
+ export interface ProbeServer {
32
+ tool: (name: string, description: string, schema: Record<string, unknown>, handler: (args: never) => Promise<unknown>) => void;
33
+ }
34
+ /**
35
+ * One filled-in value per argument name any tool takes, so a blind replay
36
+ * reaches as much of the route surface as possible.
37
+ *
38
+ * This is how the connector knows which routes it needs WITHOUT keeping a list
39
+ * of them: it runs its own tools against a recording client and watches. A
40
+ * hand-written route list would be the very thing #48 M2 exists to delete.
41
+ *
42
+ * Shared with `conformance.test.ts` and `exclusions.test.ts` deliberately. They
43
+ * each carried their own copy, the copies had to be edited in lockstep, and a
44
+ * probe fixture that has drifted silently narrows what the sweep reaches.
45
+ */
46
+ export declare const PROBE_ARGS: Record<string, unknown>;
47
+ /**
48
+ * Argument overlays the replay runs IN ADDITION to `PROBE_ARGS` alone.
49
+ *
50
+ * A blind sweep with one fixed argument set is a LOWER bound on what the tools
51
+ * reach, and the gap is not evenly spread: a handful of tools pick their route
52
+ * FROM an argument, so one probe pass sees one of their routes and misses the
53
+ * rest. `list_templates` chooses between four catalogue paths on `kind`;
54
+ * `list_dependent_agents` and `list_workflows_using` choose on `entityKind`;
55
+ * `validate_workflow` takes EXACTLY ONE of `workflowId`/`document`, so the base
56
+ * set — which supplies both — makes it fire nothing at all.
57
+ *
58
+ * That under-reporting is not theoretical. It made a coverage audit name three
59
+ * template routes as uncovered when `list_templates` had reached them all
60
+ * along. A measurement that quietly reads low is worse than no measurement,
61
+ * because it generates work that did not need doing.
62
+ *
63
+ * Each overlay is merged over `PROBE_ARGS` and replayed as its own pass. An
64
+ * `undefined` value REMOVES an argument, which is how the two shapes of
65
+ * `validate_workflow` are reached.
66
+ */
67
+ export declare const PROBE_VARIANTS: Record<string, unknown>[];
68
+ /** A route a tool actually called, as observed by the recording client. */
69
+ export interface CalledRoute {
70
+ method: string;
71
+ path: string;
72
+ }
73
+ /**
74
+ * Every route this build's tools call, found by replaying them.
75
+ *
76
+ * Nothing leaves the process: the client is a recorder. A handler that rejects
77
+ * the probe arguments is simply skipped — it contributes no route, which makes
78
+ * this a LOWER bound on what the connector needs and therefore safe: the check
79
+ * can miss a route, but it cannot invent one.
80
+ */
81
+ export declare function collectCalledRoutes(register: (server: ProbeServer, client: AxonityClient) => void): Promise<CalledRoute[]>;
82
+ /** Does a concrete path match a "METHOD /a/{b}/c" contract entry? */
83
+ export declare function matchesRoute(call: CalledRoute, entry: string): boolean;
84
+ /**
85
+ * Routes this build needs that the deploy does not mount, as "METHOD /path".
86
+ *
87
+ * Deduplicated by template rather than by concrete path: `read_workflow` and
88
+ * `update_workflow` both hit `/workflows/{id}`, and reporting one absence twice
89
+ * makes a short list look like an emergency.
90
+ */
91
+ export declare function missingFromContract(calls: CalledRoute[], routes: string[]): string[];
92
+ export interface ContractCheckOptions {
93
+ /** Base URL, used to key the cache so two tenants do not share a verdict. */
94
+ apiUrl: string;
95
+ /** Where to write the report. Defaults to stderr — stdout is the MCP channel. */
96
+ log?: (message: string) => void;
97
+ /** How long to wait before giving up on the check. Defaults to 5s. */
98
+ timeoutMs?: number;
99
+ /** Skip the cache. Used by tests, and by a caller that wants a fresh verdict. */
100
+ ignoreCache?: boolean;
101
+ }
102
+ /**
103
+ * Ask the deploy what it mounts, and say — once, at startup — whether this
104
+ * build needs anything it does not have.
105
+ *
106
+ * Returns what it concluded, so a caller (and a test) can tell "checked, all
107
+ * present" from "could not check". Never throws.
108
+ */
109
+ export declare function reportContractSkew(client: AxonityClient, register: (server: ProbeServer, client: AxonityClient) => void, options: ContractCheckOptions): Promise<"ok" | "missing" | "unavailable" | "cached" | "timeout">;