@opvs-ai/mcp 2.6.1 → 2.7.1

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @opvs-ai/mcp
2
2
 
3
- OPVS MCP server for the **on-demand model** — 10 always-on tools that give any MCP host (Claude Desktop, Cursor, Antigravity, Windsurf, Claude Code) the same surface OPVS-hosted agents see. Everything else is reached through `tool_call` indirection so the package fits under the ~100-tool cap every current MCP client enforces.
3
+ OPVS MCP server for the **on-demand model** — 16 tools that give any MCP host (Claude Desktop, Cursor, Antigravity, Windsurf, Claude Code) the same surface OPVS-hosted agents see: 10 always-on marketplace tools, `sandbox_chat` (a vendor test mode), and 5 **fleet** tools for machines and slots. Everything else is reached through `tool_call` indirection so the package fits under the ~100-tool cap every current MCP client enforces.
4
4
 
5
5
  | Tool | Wraps | Purpose |
6
6
  |------|-------|---------|
@@ -13,7 +13,53 @@ OPVS MCP server for the **on-demand model** — 10 always-on tools that give any
13
13
  | `tool_install` | `/marketplace/packages/{id}/install` | Add a package with explicit consent |
14
14
  | `tool_uninstall` | `DELETE /marketplace/installed/{id}` | Remove a package |
15
15
  | `whoami` | `/auth/me` | Session probe — brand, agent, workspace |
16
- | `board_as_markdown` | `/board/boards/{id}/export?format=md` | A whole AgentBoard as a Markdown plan in one call (`detail` summary/standard/full, `columns` filter) |
16
+ | `board_as_markdown` | `/board/boards/{id}/export?format=md` | A whole AgentBoard as a Markdown plan in one call (`detail` summary/standard/full, `columns` filter). A board over the server's 1 MiB export ceiling is **refused, not truncated** — the refusal carries `actual_bytes`/`limit_bytes`/`retry_with` as fields. It does **not** silently retry at a lower detail: the tool promises these exact bytes, so substituting a different document is the caller's decision |
17
+
18
+ ### Fleet — machines and slots
19
+
20
+ The environment layer had **no agent-reachable surface at all** until these landed, so
21
+ "dashboard or MCP" was dashboard-only. They are native tools rather than `tool_call` routes
22
+ for the same reason `board_as_markdown` is: there is no marketplace skill for environments,
23
+ so there is no `schema.yaml` to route through.
24
+
25
+ | Tool | Wraps | Purpose |
26
+ |------|-------|---------|
27
+ | `fleet_list` | `/environments` + `/environments/allowance` | Every slot this brand owns + the machine allowance it spends, grouped by machine. `detail` compact/full |
28
+ | `slot_get` | `/environments/{ref}` → `/workstations/{uuid}` | One slot + its per-step onboarding checklist, and `next_action` — the single step to act on |
29
+ | `slot_health` | `/environments/{ref}/health` | **Live** sidecar probe (reaches the box over WireGuard) |
30
+ | `fleet_logins` | `/environments/credentials/reauth-status` | Lapsed logins on both credential lanes, reported separately |
31
+ | `slot_order` | `POST /workstations` | Order a slot — **writes**: spends the allowance, mints a one-shot install PAT |
32
+
33
+ #### Vocabulary
34
+
35
+ At the product surface there are exactly two nouns, and the tool names use them
36
+ ([`MACHINE-AND-SLOT.md`](../../docs/planning/opvsRunner/MACHINE-AND-SLOT.md) §3, owner ruling
37
+ 2026-09-07):
38
+
39
+ - **MACHINE** — the physical host. What a tunnel terminates at. Keyed by `machine_id`.
40
+ - **SLOT** — the worker at a desk on it. What a card is claimed by, what a terminal opens into.
41
+
42
+ **On the wire it is still `environments`**, and that disagreement is deliberate — renaming a
43
+ live table, its router and its TS type buys nothing a reader needs. The words *box* and *desk*
44
+ are retired from new writing but appear throughout the older docs. Every tool description names
45
+ the synonyms, so an agent told "list my environments" or "check my box" still finds the tool.
46
+
47
+ #### Three things that surprise people
48
+
49
+ 1. **Any ref shape works on `slot_get` and `slot_health`.** Full UUID, an 8-or-more hex prefix,
50
+ or the `env_…` short code. `slot_get` resolves the ref *first* and then uses the canonical
51
+ UUID for the checklist hop — because `/workstations/{id}` accepts **only** a UUID and 404s a
52
+ short code, which would otherwise read as "your slot does not exist".
53
+ 2. **A `null` is never a green light.** `allowance: null` is paired with `allowance_error`;
54
+ `onboarding: null` with `onboarding_error`. Both mean *unknown*, never *fine* — do not tell
55
+ an operator they have room, or that nothing is outstanding, on the strength of a null.
56
+ 3. **`fleet_logins` reports two lanes and never sums them.** `lease` is the build credential
57
+ (lapsed → builds fall back to metered pricing); `desk` is what the box's terminal opens
58
+ (lapsed → the desk will not start). A box can be perfect on one and unusable on the other,
59
+ so a single total could not say which broke. Desk state is four-valued —
60
+ `healthy | not_applicable | unknown | lapsed` — and "nobody asked OPVS to manage this desk"
61
+ is a different sentence from "OPVS was asked and cannot vouch for it".
62
+
17
63
 
18
64
  All tool names match `^[a-zA-Z0-9_-]+$` (the OpenAI tool-name regex; dots break Lola/Roni/planner on first call).
19
65
 
@@ -149,7 +195,7 @@ Every tool returns a single text-content block whose payload is JSON with:
149
195
  - **Optional flat sibling `_findings: [...]`** — only when the server emits one (e.g. `audit_level=all`).
150
196
  - **`telemetry_signal`** in search results — closed enum: `absent | cold | warm | hot`.
151
197
 
152
- The contract was locked with SpiderIQ on 2026-05-15 PM. See [`docs/planning/Marketplace/architecture/skills-system-reference.md`](../../docs/planning/Marketplace/architecture/skills-system-reference.md#skills-meta) for the gateway-side semantics this MCP mirrors.
198
+ The contract was locked with SpiderIQ on 2026-05-15 PM. See [`docs/planning/Marketplace/CLAUDE.md`](../../docs/planning/Marketplace/CLAUDE.md#architecture) for the gateway-side semantics this MCP mirrors.
153
199
 
154
200
  ## tool_pin status
155
201
 
@@ -175,6 +221,28 @@ The on-demand model (10 always-on + `tool_call` indirection) reaches the same sk
175
221
 
176
222
  Versions ≤ 0.7.4 are end-of-life. There is no `@opvs-ai/mcp@0.x → 1.0` migration code path — replace the old install with a fresh one.
177
223
 
224
+ ## Development
225
+
226
+ ```bash
227
+ cd ../core && npm install && npx tsc # REQUIRED FIRST — see below
228
+ cd ../mcp && npm install
229
+ npx vitest run # unit tests
230
+ npm run build && OPVS_TOKEN=smoke-test npm run list-tools
231
+ ```
232
+
233
+ Two things bite from a clean checkout:
234
+
235
+ - **Build `@opvs-ai/core` first.** Its `main` points at `dist/`, so without that step the
236
+ suite dies at *collection* with `Failed to resolve entry for package "@opvs-ai/core"` —
237
+ a missing build, not a broken suite.
238
+ - **`npm run list-tools` is a real gate, not a printout.** It drives the actual stdio
239
+ protocol against the built server and exits non-zero on a tool name that fails
240
+ `^[a-zA-Z0-9_-]+$` (exit 2) or on going over the tool-count ceiling (exit 3). A unit test
241
+ of the `TOOLS` array cannot prove a tool is *registered*; this can. Both run in CI under
242
+ **TS Library Tests**.
243
+
244
+ Counts move. Measure them at your SHA rather than citing a number from here.
245
+
178
246
  ## Related packages
179
247
 
180
248
  - [`@opvs-ai/cli`](../cli) — terminal access (no MCP layer)
@@ -0,0 +1,9 @@
1
+ {
2
+ "stamp_version": 1,
3
+ "package": "@opvs-ai/mcp",
4
+ "version": "2.7.1",
5
+ "source_digest": "1655ead73749e2e1b930a8d31f87653be62fad744fd5aefceb2fe6ab20178f50",
6
+ "source_files": 6,
7
+ "built_at": "2026-09-15T21:57:10.606Z",
8
+ "node": "v22.22.0"
9
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Board tools — `board_as_markdown` (Topic 1 / S1.3).
3
+ *
4
+ * Lives outside `index.ts` because that file ends in a top-level
5
+ * `await server.connect(transport)` — importing it to unit-test a handler would
6
+ * start a server. See `http.ts` and `fleet.ts` for the same reasoning (OSB-119).
7
+ *
8
+ * Carved out at OSB-207, whose whole subject is what this tool does when the
9
+ * export is REFUSED, and which could not be tested where it was.
10
+ */
11
+ import { type ToolResult } from "./http.js";
12
+ export declare const BOARD_TOOLS: {
13
+ name: string;
14
+ description: string;
15
+ inputSchema: {
16
+ type: string;
17
+ properties: {
18
+ board_id: {
19
+ type: string;
20
+ description: string;
21
+ };
22
+ detail: {
23
+ type: string;
24
+ enum: string[];
25
+ description: string;
26
+ };
27
+ columns: {
28
+ type: string;
29
+ description: string;
30
+ };
31
+ };
32
+ required: string[];
33
+ };
34
+ }[];
35
+ /** The shape `@opvs-ai/core` throws, plus the parsed body it now carries. */
36
+ type UpstreamError = {
37
+ detail?: string;
38
+ status?: number;
39
+ body?: unknown;
40
+ };
41
+ /** The structured half of AgentBoard's 413, when it sent one. */
42
+ type ExportRefusal = {
43
+ error?: unknown;
44
+ message?: unknown;
45
+ actual_bytes?: unknown;
46
+ limit_bytes?: unknown;
47
+ detail_level?: unknown;
48
+ retry_with?: unknown;
49
+ };
50
+ /**
51
+ * Pull AgentBoard's structured refusal out of a thrown error, if it sent one.
52
+ *
53
+ * Returns `null` for the PROSE shape — which is not a failure to handle but the
54
+ * ordinary state during a deploy window: this package and AgentBoard ship on
55
+ * different schedules, so a rebuilt MCP will spend some hours talking to a
56
+ * server that still answers `{"detail": "<sentence>"}`. The caller must stay
57
+ * useful on that path, so nothing here parses the sentence for numbers — an
58
+ * absent field is honest, a number scraped out of English is a guess.
59
+ */
60
+ export declare function exportRefusalFields(e: UpstreamError): ExportRefusal | null;
61
+ export declare function handleBoardAsMarkdown(args: {
62
+ board_id: string;
63
+ detail?: string;
64
+ columns?: string;
65
+ }): Promise<ToolResult>;
66
+ export {};
package/dist/board.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Board tools — `board_as_markdown` (Topic 1 / S1.3).
3
+ *
4
+ * Lives outside `index.ts` because that file ends in a top-level
5
+ * `await server.connect(transport)` — importing it to unit-test a handler would
6
+ * start a server. See `http.ts` and `fleet.ts` for the same reasoning (OSB-119).
7
+ *
8
+ * Carved out at OSB-207, whose whole subject is what this tool does when the
9
+ * export is REFUSED, and which could not be tested where it was.
10
+ */
11
+ import { err, errFields, getText, ok } from "./http.js";
12
+ export const BOARD_TOOLS = [
13
+ {
14
+ name: "board_as_markdown",
15
+ description: "Read a WHOLE AgentBoard (or a chosen subset of its columns) as a single " +
16
+ "Markdown plan/PRD document — one call instead of N task fetches. Cheaper on " +
17
+ "tokens and gives narrative context, not raw rows. Returns the rendered " +
18
+ "Markdown string verbatim (read-only, deterministic — same board state → same " +
19
+ "bytes, each task row carries an HTML comment with its UUID for diffing). " +
20
+ "Brand is pinned server-side from the PAT — never pass a brand_id. Pick `detail`: " +
21
+ "`summary` = compact one-row-per-task table (token-cheapest); `standard` " +
22
+ "(default) = the nested plan format with task bodies, subtasks and stages; " +
23
+ "`full` = the exhaustive dump including comments. " +
24
+ "SIZE: a big board can exceed the server's 1 MiB export ceiling at `standard` " +
25
+ "or `full` and is then REFUSED (413) rather than truncated — the refusal is " +
26
+ "machine-readable (`actual_bytes`, `limit_bytes`, `retry_with`) and this tool " +
27
+ "does NOT silently retry at a lower detail, because it promises these exact " +
28
+ "bytes. On a board you know is large, ask for `detail='summary'` up front: it " +
29
+ "is the one narrowing that drops no card.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ board_id: {
34
+ type: "string",
35
+ description: "The board UUID to render.",
36
+ },
37
+ detail: {
38
+ type: "string",
39
+ enum: ["summary", "standard", "full"],
40
+ description: "Render depth. Default `standard` (the nested plan/PRD shape). " +
41
+ "`summary` is the compact table; `full` adds comments.",
42
+ },
43
+ columns: {
44
+ type: "string",
45
+ description: "Optional comma-separated column filter — names OR ids, mixed. Each token " +
46
+ "is resolved name-first-then-id; an unknown token fails loud (400). Omit to " +
47
+ "render every column.",
48
+ },
49
+ },
50
+ required: ["board_id"],
51
+ },
52
+ },
53
+ ];
54
+ /**
55
+ * Pull AgentBoard's structured refusal out of a thrown error, if it sent one.
56
+ *
57
+ * Returns `null` for the PROSE shape — which is not a failure to handle but the
58
+ * ordinary state during a deploy window: this package and AgentBoard ship on
59
+ * different schedules, so a rebuilt MCP will spend some hours talking to a
60
+ * server that still answers `{"detail": "<sentence>"}`. The caller must stay
61
+ * useful on that path, so nothing here parses the sentence for numbers — an
62
+ * absent field is honest, a number scraped out of English is a guess.
63
+ */
64
+ export function exportRefusalFields(e) {
65
+ const body = e.body;
66
+ const detail = body?.detail;
67
+ if (!detail || typeof detail !== "object" || Array.isArray(detail))
68
+ return null;
69
+ const d = detail;
70
+ return d.error === "markdown_export_too_large" ? d : null;
71
+ }
72
+ function asNumber(v) {
73
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
74
+ }
75
+ /**
76
+ * Turn a 413 into a refusal an agent can act on without reading English.
77
+ *
78
+ * DECISION (OSB-207): this does NOT retry at `summary` itself.
79
+ *
80
+ * The tool's contract is "the rendered Markdown string verbatim — same board
81
+ * state → same bytes", which is what makes the output safe to commit and diff.
82
+ * There is no envelope on the success path to announce a substitution in, so
83
+ * auto-retrying would have to either corrupt those bytes with a banner or hand
84
+ * back a compact table where task bodies were asked for — and an agent that
85
+ * receives a table silently has no way to tell "the cards have no descriptions"
86
+ * from "I was served a different document than I requested". The cost of not
87
+ * retrying is one round-trip; the cost of retrying is a wrong conclusion that
88
+ * looks like data. The numbers and the exact next call go in the refusal
89
+ * instead, so that round-trip is always correct.
90
+ */
91
+ function tooLargeResult(boardId, requested, e) {
92
+ const fields = exportRefusalFields(e);
93
+ const actual = asNumber(fields?.actual_bytes);
94
+ const limit = asNumber(fields?.limit_bytes);
95
+ // The server reports the level it refused; fall back to the one WE asked
96
+ // for, which is known here and is not a guess. Undefined only when the
97
+ // caller omitted `detail` AND the server sent prose — we do not assert the
98
+ // server's default on its behalf.
99
+ const level = typeof fields?.detail_level === "string" ? fields.detail_level : requested;
100
+ const sizes = actual != null && limit != null
101
+ ? ` — ${actual.toLocaleString("en-US")} bytes, over the ${limit.toLocaleString("en-US")}-byte limit`
102
+ : "";
103
+ const at = level ? ` at detail='${level}'` : "";
104
+ // `summary` is exempt from the ceiling server-side, so this branch is
105
+ // unreachable today. It exists because the alternative is guidance that
106
+ // names the level that just failed — the "a ceiling that can reject its own
107
+ // remedy is a dead end" shape, one layer out. Cheap to be right about.
108
+ const summaryAlsoRefused = level === "summary";
109
+ const guidance = summaryAlsoRefused
110
+ ? "#audit.error:too-large — even detail='summary' exceeds the server's " +
111
+ "Markdown export ceiling for this board, so there is no lossless export " +
112
+ "of the whole board. This is NOT an auth, brand or board_id problem. " +
113
+ `#next:board_as_markdown(board_id='${boardId}', detail='summary', ` +
114
+ "columns='<name,name>') — a SUBSET, lossy: cards outside the named " +
115
+ "columns are absent from the document entirely. Use " +
116
+ "tool_call(@opvs-ai/agentboard, list_columns) for the valid names."
117
+ : "#audit.error:too-large — the board exceeds the server's Markdown export " +
118
+ "ceiling at this detail level. This is NOT an auth, brand or board_id " +
119
+ "problem, and retrying the identical call will fail identically. " +
120
+ `#next:board_as_markdown(board_id='${boardId}', detail='summary') — a ` +
121
+ "compact one-row-per-task table over EVERY card, each keeping its " +
122
+ "<!-- task: uuid --> anchor, so no card is dropped. Pass " +
123
+ "columns='<name,name>' instead to export a SUBSET — lossy: cards outside " +
124
+ "the named columns are absent from the document entirely.";
125
+ return errFields(`board_as_markdown: board '${boardId}' is too large to export${at}${sizes}.`, {
126
+ code: "markdown_export_too_large",
127
+ ...(actual != null ? { actual_bytes: actual } : {}),
128
+ ...(limit != null ? { limit_bytes: limit } : {}),
129
+ ...(level != null ? { detail_level: level } : {}),
130
+ // The one narrowing that drops no card, as a parameter not a sentence —
131
+ // omitted when it is the thing that was just refused.
132
+ ...(summaryAlsoRefused ? {} : { retry_with: { detail: "summary" } }),
133
+ lossy_alternative: { columns: "<name,name>" },
134
+ }, guidance);
135
+ }
136
+ export async function handleBoardAsMarkdown(args) {
137
+ if (!args.board_id || typeof args.board_id !== "string") {
138
+ return err("board_id is required", "#audit.error:input=missing — board_as_markdown needs the board UUID. " +
139
+ "Use tool_call(@opvs-ai/agentboard, list_boards) to find one.");
140
+ }
141
+ if (args.detail != null &&
142
+ !["summary", "standard", "full"].includes(args.detail)) {
143
+ return err(`invalid detail '${args.detail}'`, "#audit.error:input=bad-detail — detail must be summary | standard | full. " +
144
+ "#next:omit it for the default (standard).");
145
+ }
146
+ // Mirror the export contract: format=md is fixed; detail/columns are forwarded
147
+ // only when the caller set them (don't synthesize defaults — let the backend
148
+ // own them; LEARNINGS #38, the silent-drop tax cuts both ways).
149
+ const q = new URLSearchParams({ format: "md" });
150
+ if (args.detail)
151
+ q.set("detail", args.detail);
152
+ if (args.columns)
153
+ q.set("columns", args.columns);
154
+ const path = `/api/v1/board/boards/${encodeURIComponent(args.board_id)}/export?${q.toString()}`;
155
+ try {
156
+ const md = await getText(path);
157
+ return ok(md);
158
+ }
159
+ catch (e) {
160
+ const error = e;
161
+ if (error.status === 413) {
162
+ return tooLargeResult(args.board_id, args.detail, error);
163
+ }
164
+ if (error.status === 400) {
165
+ return err(`board_as_markdown: ${error.detail ?? "bad request"}`, "#audit.error:bad-columns — an unknown column name/id was passed. " +
166
+ "#next:tool_call(@opvs-ai/agentboard, list_columns) to see valid columns, or omit `columns`.");
167
+ }
168
+ if (error.status === 404) {
169
+ return err(`board '${args.board_id}' not found (or not visible to this brand)`, "#audit.error:board=missing #next:whoami to confirm the PAT's brand; " +
170
+ "tool_call(@opvs-ai/agentboard, list_boards) to list boards.");
171
+ }
172
+ return err(`board_as_markdown failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:upstream #next:whoami to verify auth/brand; check the board_id.");
173
+ }
174
+ }
175
+ //# sourceMappingURL=board.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board.js","sourceRoot":"","sources":["../src/board.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /** Routing nudge for board/factory skills (GS.2). A POINTER to the canonical
2
+ * sources (the skill's SKILL.md + its references, read with `tool_read`, and
3
+ * the live claimTask/getRunnable `_guidance` envelopes), carrying the few
4
+ * platform invariants an MCP agent needs at decision time so it can't drive a
5
+ * factory board blind. Deliberately terse — the full recipe is
6
+ * references/run-a-factory-board.md; this never restates it.
7
+ *
8
+ * ASP-73: this used to send agents to `section='description'` for "the
9
+ * operating model + references index". tool_help's description section is the
10
+ * registry's one-line package description, so the pointer led back to this
11
+ * same `_guidance` — a loop. The operating model is what `tool_read` serves.
12
+ * ASP-75: the one-active-project claim gate is stated here too, because it
13
+ * refuses every claim before any of the other invariants matter. */
14
+ export declare const FACTORY_HELP_GUIDANCE: string;
@@ -0,0 +1,27 @@
1
+ /** Routing nudge for board/factory skills (GS.2). A POINTER to the canonical
2
+ * sources (the skill's SKILL.md + its references, read with `tool_read`, and
3
+ * the live claimTask/getRunnable `_guidance` envelopes), carrying the few
4
+ * platform invariants an MCP agent needs at decision time so it can't drive a
5
+ * factory board blind. Deliberately terse — the full recipe is
6
+ * references/run-a-factory-board.md; this never restates it.
7
+ *
8
+ * ASP-73: this used to send agents to `section='description'` for "the
9
+ * operating model + references index". tool_help's description section is the
10
+ * registry's one-line package description, so the pointer led back to this
11
+ * same `_guidance` — a loop. The operating model is what `tool_read` serves.
12
+ * ASP-75: the one-active-project claim gate is stated here too, because it
13
+ * refuses every claim before any of the other invariants matter. */
14
+ export const FACTORY_HELP_GUIDANCE = "#use:board/DAG/factory task pipelines. " +
15
+ "#read:tool_read(name) for the operating model (the skill's SKILL.md) — its " +
16
+ "`layers[]` lists every recipe; tool_read(name, path='references/run-a-factory-board.md') " +
17
+ "is the factory recipe. " +
18
+ "#factory:the board must be in exactly ONE active project (moveBoardToProject " +
19
+ "mode=move) or every claimTask returns task_id:null reason board_project_unresolved. " +
20
+ "New cards land in Backlog (staging, NOT claimable) → move to Ready " +
21
+ "(claimable by role 'builder') → a worker claims and it auto-moves to Executing. " +
22
+ "A factory board runs NOTHING until an opvs-sidecar worker is pointed at its " +
23
+ "board_id; set a `mode:` label (auto|assisted|manual) per card for build autonomy. " +
24
+ "#next:the live claimTask + getRunnable responses carry their own `_guidance` " +
25
+ "envelope (e.g. a claim that returns task_id:null+reason is a misconfig, " +
26
+ "not no-work) — read it.";
27
+ //# sourceMappingURL=factory-guidance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory-guidance.js","sourceRoot":"","sources":["../src/factory-guidance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;qEAYqE;AACrE,MAAM,CAAC,MAAM,qBAAqB,GAChC,yCAAyC;IACzC,6EAA6E;IAC7E,2FAA2F;IAC3F,yBAAyB;IACzB,+EAA+E;IAC/E,sFAAsF;IACtF,qEAAqE;IACrE,kFAAkF;IAClF,8EAA8E;IAC9E,oFAAoF;IACpF,+EAA+E;IAC/E,0EAA0E;IAC1E,yBAAyB,CAAC"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Fleet tools — the MACHINE and SLOT surface (OSB-119, BYO plan row 3.2).
3
+ *
4
+ * Until this module the MCP package had 11 tools and NONE of them reached the
5
+ * environment layer, so the owner's "dashboard or MCP" was dashboard-only.
6
+ *
7
+ * Lives outside `index.ts` because that file ends in a top-level
8
+ * `await server.connect(transport)` — importing it to unit-test a handler would
9
+ * start a server. See `http.ts` for the same reasoning.
10
+ */
11
+ import { type ToolResult } from "./http.js";
12
+ export declare const FLEET_TOOLS: ({
13
+ name: string;
14
+ description: string;
15
+ inputSchema: {
16
+ type: string;
17
+ properties: {
18
+ detail: {
19
+ type: string;
20
+ enum: string[];
21
+ description: string;
22
+ };
23
+ slot?: undefined;
24
+ name?: undefined;
25
+ runtime?: undefined;
26
+ region?: undefined;
27
+ description?: undefined;
28
+ };
29
+ required?: undefined;
30
+ };
31
+ } | {
32
+ name: string;
33
+ description: string;
34
+ inputSchema: {
35
+ type: string;
36
+ properties: {
37
+ slot: {
38
+ type: string;
39
+ description: string;
40
+ };
41
+ detail?: undefined;
42
+ name?: undefined;
43
+ runtime?: undefined;
44
+ region?: undefined;
45
+ description?: undefined;
46
+ };
47
+ required: string[];
48
+ };
49
+ } | {
50
+ name: string;
51
+ description: string;
52
+ inputSchema: {
53
+ type: string;
54
+ properties: {
55
+ detail?: undefined;
56
+ slot?: undefined;
57
+ name?: undefined;
58
+ runtime?: undefined;
59
+ region?: undefined;
60
+ description?: undefined;
61
+ };
62
+ required?: undefined;
63
+ };
64
+ } | {
65
+ name: string;
66
+ description: string;
67
+ inputSchema: {
68
+ type: string;
69
+ properties: {
70
+ name: {
71
+ type: string;
72
+ description: string;
73
+ };
74
+ runtime: {
75
+ type: string;
76
+ enum: string[];
77
+ description: string;
78
+ };
79
+ region: {
80
+ type: string;
81
+ description: string;
82
+ };
83
+ description: {
84
+ type: string;
85
+ description: string;
86
+ };
87
+ detail?: undefined;
88
+ slot?: undefined;
89
+ };
90
+ required: string[];
91
+ };
92
+ })[];
93
+ /** The slot fields a compact fleet row carries.
94
+ *
95
+ * AN ENUMERATION IS ITSELF A CLAIM, so this list is exported into the
96
+ * `fleet_list` description verbatim and `detail: "full"` exists to escape it.
97
+ * A trim that nobody can see is indistinguishable from a field the server
98
+ * stopped sending.
99
+ */
100
+ export declare const COMPACT_SLOT_FIELDS: readonly ["id", "short_id", "name", "status", "dormant", "runtime", "machine_id", "sidecar_port", "wg_address", "sidecar_version", "last_health_check", "last_sync_at", "pending_sync_count"];
101
+ export declare function compactSlot(row: Record<string, unknown>): Record<string, unknown>;
102
+ /** Group slots under the machine that reported hosting them.
103
+ *
104
+ * 🔴 A NULL `machine_id` is NOT a group. The column is written by the health
105
+ * poller from `/etc/machine-id` and is NULL whenever the sidecar is too old to
106
+ * report it — which is still the common case. Two boxes that both decline to
107
+ * say where they run are not thereby co-tenants, and collapsing them would
108
+ * invent a co-tenancy the platform has no evidence for
109
+ * (`schemas/environment.py`, `machine_id`). They go to `unplaced`, one entry
110
+ * each, in list order.
111
+ */
112
+ export declare function groupByMachine(rows: Array<Record<string, unknown>>): {
113
+ machines: Array<{
114
+ machine_id: string;
115
+ slot_count: number;
116
+ slots: string[];
117
+ }>;
118
+ unplaced: string[];
119
+ };
120
+ export declare function handleFleetList(args: {
121
+ detail?: string;
122
+ }): Promise<ToolResult>;
123
+ /** Resolve any slot ref (UUID | 8+ hex prefix | `env_…` short code) to the
124
+ * canonical environment row.
125
+ *
126
+ * 🔑 THIS EXISTS BECAUSE THE TWO ROUTES DISAGREE ABOUT REF SHAPES.
127
+ * `GET /environments/{ref}` funnels through `_resolve_env`, which accepts all
128
+ * three. `GET /workstations/{id}` gates on `_is_uuid` and returns 404 for
129
+ * anything else — so an agent that reads `short_id` out of `fleet_list` and
130
+ * hands it to the checklist route gets "not found" for a slot that plainly
131
+ * exists. Resolving here, once, and then using the row's own `id` for the
132
+ * second hop is what keeps `slot_get` honest about which refs it accepts.
133
+ */
134
+ export declare function resolveSlot(ref: string): Promise<{
135
+ ok: true;
136
+ row: Record<string, unknown>;
137
+ } | {
138
+ ok: false;
139
+ result: ToolResult;
140
+ }>;
141
+ type OnboardingStep = {
142
+ key?: unknown;
143
+ state?: unknown;
144
+ reason?: unknown;
145
+ hint?: unknown;
146
+ title?: unknown;
147
+ };
148
+ /** The one step an operator should act on: the first that is not `done`.
149
+ *
150
+ * Everything after it is `blocked_by:<key>` by construction (OSB-113), so
151
+ * picking the first non-done step is what turns four competing lines into one.
152
+ * Returns null when every step is done — a genuinely ready slot.
153
+ */
154
+ export declare function nextAction(steps: OnboardingStep[]): Record<string, unknown> | null;
155
+ export declare function handleSlotGet(args: {
156
+ slot: string;
157
+ }): Promise<ToolResult>;
158
+ export declare function handleSlotHealth(args: {
159
+ slot: string;
160
+ }): Promise<ToolResult>;
161
+ export declare function handleFleetLogins(): Promise<ToolResult>;
162
+ export declare const SLOT_RUNTIMES: string[];
163
+ export declare function handleSlotOrder(args: {
164
+ name: string;
165
+ runtime?: string;
166
+ region?: string;
167
+ description?: string;
168
+ }): Promise<ToolResult>;
169
+ export {};