@axonity-ai/mcp 0.5.0 → 0.7.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 (50) hide show
  1. package/README.md +181 -10
  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 +88 -0
  6. package/dist/contract.js +227 -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 +25 -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 +56 -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 +199 -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 +139 -9
  30. package/dist/tools/extras.js.map +1 -1
  31. package/dist/tools/register.d.ts +27 -0
  32. package/dist/tools/register.js +19 -1
  33. package/dist/tools/register.js.map +1 -1
  34. package/dist/tools/runs.d.ts +11 -0
  35. package/dist/tools/runs.js +211 -17
  36. package/dist/tools/runs.js.map +1 -1
  37. package/dist/tools/secrets.d.ts +33 -0
  38. package/dist/tools/secrets.js +69 -0
  39. package/dist/tools/secrets.js.map +1 -0
  40. package/dist/tools/subworkflows.d.ts +20 -0
  41. package/dist/tools/subworkflows.js +42 -0
  42. package/dist/tools/subworkflows.js.map +1 -0
  43. package/dist/tools/validation.js +74 -4
  44. package/dist/tools/validation.js.map +1 -1
  45. package/dist/tools/versions.d.ts +9 -0
  46. package/dist/tools/versions.js +36 -0
  47. package/dist/tools/versions.js.map +1 -1
  48. package/dist/tools/workflowMutations.js +44 -12
  49. package/dist/tools/workflowMutations.js.map +1 -1
  50. 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.
@@ -107,7 +148,16 @@ across these routes — the tool parameter names say which.
107
148
  `attach_reference_to_agent`, and a `detach_*_from_*` for each. Detaching
108
149
  removes the link only — the skill or policy itself is untouched. Read the
109
150
  links back with `list_agent_skills`, `list_agent_policies`,
110
- `list_agent_reference_docs`.
151
+ `list_agent_reference_docs` and `list_workflow_skills`. The three agent
152
+ read-backs take an optional `workflowId` for the **composed runtime view** —
153
+ what the agent's prompt actually assembles inside that workflow, with a
154
+ `linkSource` per row saying why each item is there.
155
+ - **What uses this?**: `list_workflows_using({ entityKind, entityId })` names
156
+ the workflows that reference a tool, agent, flow, output schema or workflow,
157
+ **and the steps they reference it in**, with `draft`/`published` per hit.
158
+ `list_dependent_agents({ entityKind, entityId })` is the same question for a
159
+ skill, policy or reference doc. Ask before editing anything shared — the
160
+ alternative is validating every workflow in the tenant.
111
161
  - **Prompt elements (placement)**: a `prompt_snippet` is a library item; it only
112
162
  takes effect once placed into a flow step's prompt stack.
113
163
  `read_workflow_prompt_stacks` / `read_flow_prompt_stacks` resolve a
@@ -119,13 +169,33 @@ across these routes — the tool parameter names say which.
119
169
  - **Company** (the tenant's single company document — a singleton, no id):
120
170
  `read_company`, `update_company` (whole-document save with `expectedVersion`),
121
171
  `list_company_versions`, `read_company_version`, `restore_company_version`,
122
- `name_company_major_version`, `read_company_published`, and
172
+ `name_company_major_version`, `create_company_major_version`,
173
+ `ensure_company_major_version`, `read_company_published`,
174
+ `apply_company_mutation` (one validated, version-safe command — preferred
175
+ over the whole-document `update_company`, the same way
176
+ `apply_workflow_mutations` is preferred for a workflow), and
123
177
  `request_publish_company` (takes no id — the
124
178
  server resolves your tenant's one company; direct company publish is closed to
125
179
  service tokens).
180
+ - **Subworkflows**: `list_callable_workflows` — which workflows a `subprocess`
181
+ step may call, each with its `parameters` and `outcomes`, and a
182
+ `blockedReason` for the ones that cannot (never published, or no
183
+ `subprocess-invocation` trigger). Authoring both halves is ordinary
184
+ `apply_workflow_mutations` work — a callable workflow's signature goes into
185
+ the `add_trigger` call itself. `validate_workflow` **does** check a subprocess
186
+ target now (missing, self-call, deleted, unpublished, not callable), but
187
+ `list_callable_workflows` is still what you run first: it is how you pick a
188
+ target and read the interface you are binding to.
189
+ - **Secrets** (read-only): `list_secrets`, `read_secret` — the catalogue a
190
+ connector's `authConfig.secretId` points at. Values are never returned by any
191
+ Axonity route; `valueKeys` says which keys a human has filled in, so you can
192
+ tell an unfinished secret from a finished one before wiring to it. Creating or
193
+ changing a secret is a human act in Axonity (#39).
126
194
  - **Catalog & cloning**: `list_system_tools` (read-only catalog — enabling one
127
195
  for an agent is `update_agent` with the id added to `systemToolIds`),
128
- `clone_flow`, `clone_prompt_snippet`.
196
+ `clone_flow`, `clone_prompt_snippet`, `list_tool_packages` (the import
197
+ allowlist `validate_tool_code` judges against), `list_templates` /
198
+ `read_template`.
129
199
  - `list_deleted_prompt_snippets` calls `/api/v1/prompt-snippets/deleted`; the
130
200
  backend returns it as `{ items: [... ] }`, and the tool forwards that response
131
201
  unchanged.
@@ -157,6 +227,42 @@ workflow and really executes), `cancel_run`, `delete_run`, `list_runs`,
157
227
  `bulk_delete_runs`. There is no findings endpoint — evaluation means reading a
158
228
  run's validator verdicts and its trace.
159
229
 
230
+ **Which start, and what it wants.** `read_workflow_trigger_parameters` answers
231
+ `{ triggers, constants }` — every way the workflow can be started, each with its
232
+ own parameters. Pass the one you mean to `start_workflow_run` as `triggerId`;
233
+ omitting it fires the first, which on a workflow with a button *and* a schedule
234
+ is an arbitrary choice. A parameter marked `pinned` is one the **author** owns:
235
+ it is overwritten on every run, so a caller must not send it.
236
+
237
+ **`read_run` omits the workflow snapshot by default.** It is immutable, it is
238
+ never the answer to a question about the run, and it measured 81% of one real
239
+ response — an oversized response turns a call that succeeded into an error.
240
+ Pass `includeSnapshot: true` when you actually want to see what executed.
241
+
242
+ **A run can park rather than finish.** `read_run_waiting_on` says what it is
243
+ waiting for; `answer_run_question` answers an `ask_user` step and
244
+ `send_run_message` sends a turn to a conversation run. Both record the input as
245
+ a person's, so use them on runs you started. Deciding a **plan approval** and
246
+ restarting a stuck run are deliberately absent — the first is the human review
247
+ the step exists to get, the second is `require_admin` and a service token is
248
+ always `role="member"`. Both are recorded in `test/exclusions.test.ts`.
249
+
250
+ **Inside a launch**: `read_run_items_summary` is the roll-up ("4,415 processed ·
251
+ 12 failed"), `list_run_items({ outcome })` the paged rows — filter in the query,
252
+ because the failures are scattered and sifting page one finds none of them.
253
+ `list_run_tasks` and `read_run_for_each_progress` cover the children a run set
254
+ in motion.
255
+
256
+ `list_workflow_runs({ workflowId, status?, archivedOnly?, limit?, cursor? })` returns one
257
+ **page** — `{ items, nextCursor, pageSize, hasMore }`, 20 by default and 200 at
258
+ most — so follow `nextCursor` while `hasMore` is true rather than treating the
259
+ first page as the answer. It also lists **launches**, not runs: the per-item runs
260
+ a FOR EACH creates stay inside their launch, so a launch over 4,415 people is one
261
+ entry carrying `forEachProgress`. `list_runs` is the tenant-wide list and is now
262
+ paged the same way — `{ items, nextCursor, pageSize, hasMore }`, walked with
263
+ `cursor`. It stopped answering with a bare array when the backend converted the
264
+ route; the `offset` it used to take is no longer read.
265
+
160
266
  ### Approvals
161
267
 
162
268
  `list_publish_approvals({ status?, limit?, offset? })` and
@@ -164,6 +270,23 @@ run's validator verdicts and its trace.
164
270
  `request_publish_*` was approved or rejected. Approving and rejecting are
165
271
  human-only actions in Axonity.
166
272
 
273
+ `request_publish_release({ workflowId, changeSummary? })` proposes a **release**:
274
+ a workflow *and everything its run needs* — the agents it runs, their tools and
275
+ personas, the flows it pins, the memory scoped to those agents — as ONE approval.
276
+ Prefer it over a request per entity. Taking a tenant live entity-by-entity means
277
+ a hundred-odd approvals, none of which means anything on its own, and a human
278
+ asked that many times is not reviewing. `list_publish_releases` and
279
+ `get_publish_release` read one back — the release's members, and its readiness
280
+ recomputed as of now.
281
+
282
+ Unlike `request_publish_bulk`, a release is **all-or-nothing and in dependency
283
+ order**: approving it publishes every member or none, so a workflow can never go
284
+ live calling a tool that did not. The response carries the bundle's verdict —
285
+ `ready`, `changedCount` of `totalCount` (unchanged members are already live and
286
+ ride along), `members` with why each is there, and `blockers` that name the
287
+ member in the way. Requesting is ours; deciding stays human, like everywhere
288
+ else here.
289
+
167
290
  ## What this is for — and what it is not
168
291
 
169
292
  **Authoring.** An agent composing an entity from intent: drafting a workflow,
@@ -193,15 +316,22 @@ These are enforced by the backend, not merely by convention:
193
316
  - **Secrets never pass through the agent.** A connector's `authConfig` accepts
194
317
  placeholders only; a write carrying something that looks like a real
195
318
  credential is rejected before it leaves the connector. Tenant secrets
196
- (`/api/v1/secrets`) aren't wrapped at all — the backend refuses any service
197
- token there outright.
319
+ (`/api/v1/secrets`) are **readable and unwritable**: `list_secrets` /
320
+ `read_secret` give the catalogue and `valueKeys` (which keys are filled, never
321
+ their values) so an agent can point `authConfig.secretId` at the right entry,
322
+ and no tool can create, change or delete one — the backend refuses a service
323
+ token there too. A secret's `metadata` is stored unencrypted and readable
324
+ tenant-wide (axonity-flow#908), so credential-shaped entries in it are
325
+ withheld on read and listed under `metadataRedacted`.
198
326
  - **Errors carry a machine-readable `code`, not just prose.** A 409 can mean a
199
327
  stale write (retry) or a live reference conflict (don't — see
200
328
  `axonity_conventions`); the connector tells them apart by `code`, never by
201
329
  matching the message text.
202
330
  - **No tool crosses the authority boundary.** A test drives the whole registered
203
- surface and fails the build if any tool targets a publish / approve / secret /
204
- service-token / deploy route (`test/exclusions.test.ts`).
331
+ surface and fails the build if any tool targets a publish / approve /
332
+ secret-write / service-token / deploy route (`test/exclusions.test.ts`). The
333
+ rules are method-aware: `GET /api/v1/secrets` is allowed, every write verb on
334
+ it is not.
205
335
  - **Guidance can't silently drift from the backend.** The field/enum facts the
206
336
  connector states are pinned to a vendored snapshot of the backend OpenAPI
207
337
  schema; `test/conformance.test.ts` fails if a route or documented enum
@@ -227,6 +357,18 @@ Publishing runs from a maintainer's machine, not from CI. npm restricts tokens
227
357
  that bypass 2FA for direct publishing, so a stored `NPM_TOKEN` cannot ship this
228
358
  package; `npm login` is the supported path.
229
359
 
360
+ **Check the contract first.** Shipping with a stale snapshot is how the
361
+ connector fell nine operations behind the backend without any test noticing:
362
+
363
+ ```bash
364
+ npm run check:contract # expects ../axonity-flow; override with AXONITY_FLOW_PATH
365
+ ```
366
+
367
+ It dumps the schema from a local `axonity-flow` checkout and diffs it against
368
+ `test/fixtures/openapi.snapshot.json`, naming every operation that moved. It
369
+ runs here rather than in the release workflow because the publish itself runs
370
+ here — a gate in CI cannot stop a local `npm publish`.
371
+
230
372
  Run `npm login` in a real terminal — it prints a URL and waits for you to finish
231
373
  in the browser, so it needs a session that stays attached (an editor's 2-minute
232
374
  command timeout will kill it mid-flow).
@@ -248,3 +390,32 @@ was cut from a state CI cannot install.
248
390
 
249
391
  Keep npm 11 locally: Node 20 bundles npm 10, whose resolver writes an
250
392
  incompatible lockfile tree. CI pins npm 11 for the same reason.
393
+
394
+ ## Staying level with the backend
395
+
396
+ The snapshot every drift guard reads is only as fresh as the last time someone
397
+ regenerated it — and once it wasn't: it sat nine operations behind the backend
398
+ while the whole suite stayed green, because a snapshot that has not seen a route
399
+ cannot report it missing.
400
+
401
+ **Before a release, check it** (see §Releasing): `npm run check:contract` dumps
402
+ the schema from a local `axonity-flow` checkout and names every operation that
403
+ moved. It compares the contract surface — query parameters, request body,
404
+ response shape — and not descriptions, so backend docstring churn does not cry
405
+ wolf. This is the gate that counts, because the publish runs here too.
406
+
407
+ **At startup, the connector asks the deploy what it mounts.** It calls
408
+ `GET /api/v1/contract` once before accepting its first tool call and, if this
409
+ backend lacks a route this build needs, says which on stderr — instead of
410
+ failing on the twentieth call, mid-task. It is a diagnostic and never a
411
+ dependency: a backend too old to serve `/contract`, an unreachable one, or a
412
+ slow one all degrade to the previous behaviour and the connector starts
413
+ normally. The result is cached on the `contractHash` the route returns, so an
414
+ unchanged deploy costs one request.
415
+
416
+ **Not here: a scheduled job.** Watching for drift on a timer belongs in
417
+ `axonity-flow`, not in this repository. The schema lives there, its CI already
418
+ has the backend's dependencies installed, and it can read this repository's
419
+ pinned snapshot over plain HTTPS because this repository is public — where the
420
+ reverse needs a credential for a private repo, with an approval policy and an
421
+ 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,88 @@
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
+ /** A route a tool actually called, as observed by the recording client. */
48
+ export interface CalledRoute {
49
+ method: string;
50
+ path: string;
51
+ }
52
+ /**
53
+ * Every route this build's tools call, found by replaying them.
54
+ *
55
+ * Nothing leaves the process: the client is a recorder. A handler that rejects
56
+ * the probe arguments is simply skipped — it contributes no route, which makes
57
+ * this a LOWER bound on what the connector needs and therefore safe: the check
58
+ * can miss a route, but it cannot invent one.
59
+ */
60
+ export declare function collectCalledRoutes(register: (server: ProbeServer, client: AxonityClient) => void): Promise<CalledRoute[]>;
61
+ /** Does a concrete path match a "METHOD /a/{b}/c" contract entry? */
62
+ export declare function matchesRoute(call: CalledRoute, entry: string): boolean;
63
+ /**
64
+ * Routes this build needs that the deploy does not mount, as "METHOD /path".
65
+ *
66
+ * Deduplicated by template rather than by concrete path: `read_workflow` and
67
+ * `update_workflow` both hit `/workflows/{id}`, and reporting one absence twice
68
+ * makes a short list look like an emergency.
69
+ */
70
+ export declare function missingFromContract(calls: CalledRoute[], routes: string[]): string[];
71
+ export interface ContractCheckOptions {
72
+ /** Base URL, used to key the cache so two tenants do not share a verdict. */
73
+ apiUrl: string;
74
+ /** Where to write the report. Defaults to stderr — stdout is the MCP channel. */
75
+ log?: (message: string) => void;
76
+ /** How long to wait before giving up on the check. Defaults to 5s. */
77
+ timeoutMs?: number;
78
+ /** Skip the cache. Used by tests, and by a caller that wants a fresh verdict. */
79
+ ignoreCache?: boolean;
80
+ }
81
+ /**
82
+ * Ask the deploy what it mounts, and say — once, at startup — whether this
83
+ * build needs anything it does not have.
84
+ *
85
+ * Returns what it concluded, so a caller (and a test) can tell "checked, all
86
+ * present" from "could not check". Never throws.
87
+ */
88
+ export declare function reportContractSkew(client: AxonityClient, register: (server: ProbeServer, client: AxonityClient) => void, options: ContractCheckOptions): Promise<"ok" | "missing" | "unavailable" | "cached" | "timeout">;
@@ -0,0 +1,227 @@
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 { readFileSync, writeFileSync } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { AxonityApiError, ROUTES_ADDED_IN } from "./errors.js";
26
+ /**
27
+ * One filled-in value per argument name any tool takes, so a blind replay
28
+ * reaches as much of the route surface as possible.
29
+ *
30
+ * This is how the connector knows which routes it needs WITHOUT keeping a list
31
+ * of them: it runs its own tools against a recording client and watches. A
32
+ * hand-written route list would be the very thing #48 M2 exists to delete.
33
+ *
34
+ * Shared with `conformance.test.ts` and `exclusions.test.ts` deliberately. They
35
+ * each carried their own copy, the copies had to be edited in lockstep, and a
36
+ * probe fixture that has drifted silently narrows what the sweep reaches.
37
+ */
38
+ export const PROBE_ARGS = {
39
+ id: "x", workflowId: "x", agentId: "x", toolId: "x", runId: "x", approvalId: "x",
40
+ flowId: "x", snippetId: "x", flowStepId: "x", linkId: "x", webhookId: "x",
41
+ scheduleId: "x", triggerId: "x", secretId: "x", versionId: "x", version: 1,
42
+ majorVersion: 1, expectedVersion: 1, displayOrder: 0, name: "x",
43
+ cronExpr: "0 0 * * *", conditionText: "x", repeatIntervalMinutes: 5,
44
+ target: "system", confirm: true, document: {}, fields: {},
45
+ mutations: [{ type: "add_step", payload: {} }], snippetIds: ["a"], runIds: ["a"],
46
+ workflows: [{ id: "x", expectedVersion: 1 }],
47
+ functions: [{ name: "f", code: "def f(): pass" }], code: "x",
48
+ requests: [{ entityType: "tool", entityId: "x" }],
49
+ entityKind: "skill", entityId: "x", batchId: "x", stepId: "x", answer: "x",
50
+ message: "x", templateId: "x", releaseId: "x", payload: {},
51
+ };
52
+ /**
53
+ * Every route this build's tools call, found by replaying them.
54
+ *
55
+ * Nothing leaves the process: the client is a recorder. A handler that rejects
56
+ * the probe arguments is simply skipped — it contributes no route, which makes
57
+ * this a LOWER bound on what the connector needs and therefore safe: the check
58
+ * can miss a route, but it cannot invent one.
59
+ */
60
+ export async function collectCalledRoutes(register) {
61
+ const calls = [];
62
+ const record = (method) => async (path) => {
63
+ calls.push({ method, path });
64
+ return {};
65
+ };
66
+ const recorder = {
67
+ get: record("GET"),
68
+ post: record("POST"),
69
+ put: record("PUT"),
70
+ patch: record("PATCH"),
71
+ del: record("DELETE"),
72
+ };
73
+ const handlers = [];
74
+ register({ tool: (_n, _d, _s, handler) => handlers.push(handler) }, recorder);
75
+ for (const handler of handlers) {
76
+ try {
77
+ await handler(PROBE_ARGS);
78
+ }
79
+ catch {
80
+ /* Arg-shape mismatch is fine — only routes that fired are collected. */
81
+ }
82
+ }
83
+ return calls;
84
+ }
85
+ /** Split a path into segments, dropping any query string. */
86
+ function segments(path) {
87
+ return path.split("?")[0].replace(/^\/+|\/+$/g, "").split("/");
88
+ }
89
+ /** Does a concrete path match a "METHOD /a/{b}/c" contract entry? */
90
+ export function matchesRoute(call, entry) {
91
+ const space = entry.indexOf(" ");
92
+ if (space < 0)
93
+ return false;
94
+ if (entry.slice(0, space).toUpperCase() !== call.method.toUpperCase())
95
+ return false;
96
+ const template = segments(entry.slice(space + 1));
97
+ const actual = segments(call.path);
98
+ return (template.length === actual.length &&
99
+ template.every((part, i) => (part.startsWith("{") && part.endsWith("}")) || part === actual[i]));
100
+ }
101
+ /**
102
+ * Routes this build needs that the deploy does not mount, as "METHOD /path".
103
+ *
104
+ * Deduplicated by template rather than by concrete path: `read_workflow` and
105
+ * `update_workflow` both hit `/workflows/{id}`, and reporting one absence twice
106
+ * makes a short list look like an emergency.
107
+ */
108
+ export function missingFromContract(calls, routes) {
109
+ const missing = new Set();
110
+ for (const call of calls) {
111
+ if (routes.some((entry) => matchesRoute(call, entry)))
112
+ continue;
113
+ missing.add(`${call.method} ${call.path}`);
114
+ }
115
+ return [...missing].sort();
116
+ }
117
+ /**
118
+ * What added a route, when this repository happens to know.
119
+ *
120
+ * `ROUTES_ADDED_IN` stops being a source of truth here and becomes ANNOTATION:
121
+ * the contract check has already established that a route is missing, and this
122
+ * only enriches the sentence. An entry that is absent costs a clause, not a
123
+ * wrong answer — which is the whole difference from using it to DECIDE whether
124
+ * a 404 counts.
125
+ */
126
+ function attribute(route) {
127
+ const path = route.slice(route.indexOf(" ") + 1);
128
+ const known = ROUTES_ADDED_IN.find((entry) => entry.pattern.test(path));
129
+ return known ? `${route} — added by ${known.addedBy}` : route;
130
+ }
131
+ /** Where the "already checked this contract" marker lives, per API URL. */
132
+ function cachePath(apiUrl) {
133
+ // Not a security boundary and not precious: a lost cache costs one replay.
134
+ const key = Buffer.from(apiUrl).toString("base64url").slice(0, 40);
135
+ return join(tmpdir(), `axonity-mcp-contract-${key}.json`);
136
+ }
137
+ function readCachedHash(apiUrl) {
138
+ try {
139
+ const raw = JSON.parse(readFileSync(cachePath(apiUrl), "utf8"));
140
+ return typeof raw.contractHash === "string" ? raw.contractHash : undefined;
141
+ }
142
+ catch {
143
+ return undefined;
144
+ }
145
+ }
146
+ function writeCachedHash(apiUrl, contractHash) {
147
+ try {
148
+ writeFileSync(cachePath(apiUrl), JSON.stringify({ contractHash }));
149
+ }
150
+ catch {
151
+ /* A cache that cannot be written is a slower startup, not a failure. */
152
+ }
153
+ }
154
+ /** Resolve `promise`, or `undefined` if it takes longer than `ms`. */
155
+ async function withTimeout(promise, ms) {
156
+ let timer;
157
+ try {
158
+ return await Promise.race([
159
+ promise,
160
+ new Promise((resolve) => {
161
+ timer = setTimeout(() => resolve(undefined), ms);
162
+ // Do not hold the process open for a check that is already too late.
163
+ timer.unref?.();
164
+ }),
165
+ ]);
166
+ }
167
+ finally {
168
+ if (timer)
169
+ clearTimeout(timer);
170
+ }
171
+ }
172
+ /**
173
+ * Ask the deploy what it mounts, and say — once, at startup — whether this
174
+ * build needs anything it does not have.
175
+ *
176
+ * Returns what it concluded, so a caller (and a test) can tell "checked, all
177
+ * present" from "could not check". Never throws.
178
+ */
179
+ export async function reportContractSkew(client, register, options) {
180
+ const log = options.log ?? ((message) => console.error(message));
181
+ let contract;
182
+ try {
183
+ contract = await withTimeout(client.get("/api/v1/contract"), options.timeoutMs ?? 5_000);
184
+ }
185
+ catch (cause) {
186
+ // A backend too old to serve /contract must degrade to today's behaviour,
187
+ // not announce a problem it cannot substantiate. 404/405 is exactly that
188
+ // backend; anything else (401, network) will surface on the first real call
189
+ // with a better message than this check could write.
190
+ const status = cause instanceof AxonityApiError ? cause.status : 0;
191
+ if (status !== 404 && status !== 405 && status !== 0) {
192
+ log(`Axonity: could not read the backend contract (${status}). ` +
193
+ "Continuing — this is a diagnostic, not a dependency.");
194
+ }
195
+ return "unavailable";
196
+ }
197
+ if (contract === undefined)
198
+ return "timeout";
199
+ const routes = Array.isArray(contract.routes)
200
+ ? contract.routes.filter((entry) => typeof entry === "string")
201
+ : [];
202
+ if (routes.length === 0)
203
+ return "unavailable";
204
+ const hash = typeof contract.contractHash === "string" ? contract.contractHash : "";
205
+ if (!options.ignoreCache && hash && readCachedHash(options.apiUrl) === hash) {
206
+ return "cached";
207
+ }
208
+ const missing = missingFromContract(await collectCalledRoutes(register), routes);
209
+ if (missing.length === 0) {
210
+ // Only a CLEAN result is cached. Caching a problem would report it once and
211
+ // then go quiet, which is worse than never having checked.
212
+ if (hash)
213
+ writeCachedHash(options.apiUrl, hash);
214
+ return "ok";
215
+ }
216
+ const build = typeof contract.buildVersion === "string" ? contract.buildVersion : "unknown";
217
+ const environment = typeof contract.environment === "string" ? contract.environment : "unknown";
218
+ log(`Axonity: this backend does not have ${missing.length} route(s) this ` +
219
+ `connector needs.\n` +
220
+ ` Backend: ${build} (${environment}) at ${options.apiUrl}\n` +
221
+ missing.map((route) => ` - ${attribute(route)}`).join("\n") +
222
+ `\nThe backend is OLDER than this connector. Tools that use those routes ` +
223
+ `will fail; everything else works. Tell your human which Axonity ` +
224
+ `environment this token points at, or pin an older connector.`);
225
+ return "missing";
226
+ }
227
+ //# sourceMappingURL=contract.js.map