You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(ams): migrate the AMS miner MCP server to @loopover/contract
All 11 loopover_miner_* tools now register from the shared contract instead of
declaring their own zod shapes -- the last of LoopOver's three MCP servers to
gain a source of truth, and the only one with zero output schemas before this
(every handler returned unschematized text). Every tool now emits
structuredContent alongside its text block.
Two real defects surfaced during the migration, not before it:
- The output schema for loopover_miner_get_audit_feed initially wrapped
collectEventLedgerAuditFeed's return value in a second { events: ... } layer,
double-nesting a payload that already has that shape ({ repoFullName?, events }).
The MCP SDK's own structured-output validation caught it immediately --
"expected array, received object" -- confirming the schema is doing its job
as a runtime contract, not just documentation.
- list_claims, list_plans, and governor_decisions previously returned BARE ARRAYS
as their text payload (JSON.stringify(ledger.listClaims(...)), etc.), but an MCP
structuredContent value must be a JSON object per spec. Wrapping structuredContent
in { claims }/{ plans }/{ decisions } while leaving the text block as the
original array (via a small structured/text split in the result helper) is what
keeps every existing text-parsing consumer working unchanged -- confirmed by
running the real handler before committing to the shape, not by inspecting the
aggregator's declared TS return type alone.
Also: a unified error envelope across all 11 tools. Before, only
loopover_miner_get_audit_feed caught a store failure and returned isError: true;
the other ten threw, so the same failure surfaced as a clean result from one tool
and a raw protocol error from the rest.
test/unit/miner-mcp-scaffold.test.ts's hardcoded 11-name array and
miner-mcp-tool-docs-parity.test.ts's regex-scrape of the COMPILED dist bundle
(a pattern my own earlier investigation flagged as fragile -- a reformatted
registration call would silently break the guard, not the code) are both
replaced by a direct read of listToolDefinitions({ locality: ["miner"] }): one
list every server and every test derives from, no build step, no regex.
Full unit suite: 23041 tests, 1208 files, all green. build:miner (135 files,
check-syntax), test:miner-pack, and test:miner-deployment-docs-audit all pass
unchanged.
Closes#9536
/** `{ repoFullName?, events }` -- `collectEventLedgerAuditFeed`'s real return shape. Each event is
175
+
* metadata only: `payload_json` and other raw ledger columns are never included by construction
176
+
* (an explicit named-column read, not a redaction step). */
177
+
exportconstMinerAuditFeedOutput=z.looseObject({
178
+
repoFullName: z.string().optional(),
179
+
events: z.array(
180
+
z.looseObject({
181
+
eventType: z.string(),
182
+
repoFullName: z.string().nullable(),
183
+
outcome: z.string().nullable(),
184
+
actor: z.string().nullable(),
185
+
detail: z.string().nullable(),
186
+
createdAt: z.string(),
187
+
}),
188
+
),
189
+
});
190
+
191
+
exportconstminerAuditFeedTool=defineTool({
192
+
name: "loopover_miner_get_audit_feed",
193
+
title: "Miner audit feed",
194
+
description:
195
+
"Read-only, metadata-only audit feed from the local append-only event ledger: eventType, repoFullName, outcome, actor, detail, and createdAt per row. Wraps collectEventLedgerAuditFeed() (no new query logic) -- the same read filters as `loopover-miner ledger list` (--repo, --since, --type). Never returns payload_json or other raw ledger columns; never writes to the ledger.",
196
+
category: "agent",
197
+
auth: "public",
198
+
locality: "miner",
199
+
availability: "selfhost",
200
+
input: MinerAuditFeedInput,
201
+
output: MinerAuditFeedOutput,
202
+
});
203
+
204
+
// ── run state ───────────────────────────────────────────────────────────────────────────────────
"Read-only per-repo miner run-state (idle/discovering/planning/preparing). Pass repoFullName for a single repo (a null state means none has been recorded for it yet), or omit it to list every repo's state. The read-only analog of ORB's loopover_get_automation_state; adds no state-set or mutation capability.",
228
+
category: "agent",
229
+
auth: "public",
230
+
locality: "miner",
231
+
availability: "selfhost",
232
+
input: MinerGetRunStateInput,
233
+
output: MinerGetRunStateOutput,
234
+
});
235
+
236
+
// ── plan store ──────────────────────────────────────────────────────────────────────────────────
/** `PlanStep`. `lastError` is nullish (both unset and explicit null appear -- the store's own type
245
+
* spells it `string | null | undefined`). */
246
+
exportconstminerPlanStepSchema=z.looseObject({
247
+
id: z.string(),
248
+
title: z.string(),
249
+
actionClass: z.string().optional(),
250
+
dependsOn: z.array(z.string()),
251
+
status: z.enum(MINER_PLAN_STEP_STATUSES),
252
+
attempts: z.number(),
253
+
maxAttempts: z.number(),
254
+
lastError: z.string().nullish(),
255
+
});
256
+
257
+
/** `PlanDag`. */
258
+
exportconstminerPlanDagSchema=z.looseObject({
259
+
steps: z.array(minerPlanStepSchema),
260
+
});
261
+
262
+
/** `PlanRecord`. */
263
+
exportconstminerPlanRecordSchema=z.looseObject({
264
+
planId: z.string(),
265
+
plan: minerPlanDagSchema,
266
+
status: z.enum(MINER_PLAN_STATUSES),
267
+
updatedAt: z.string(),
268
+
});
269
+
270
+
exportconstMinerListPlansInput=z.object({
271
+
status: z.enum(MINER_PLAN_STATUSES).optional(),
272
+
});
273
+
274
+
exportconstMinerListPlansOutput=z.looseObject({
275
+
plans: z.array(minerPlanRecordSchema),
276
+
});
277
+
278
+
exportconstminerListPlansTool=defineTool({
279
+
name: "loopover_miner_list_plans",
280
+
title: "List miner plans",
281
+
description:
282
+
"Read-only list of the miner's PERSISTED plan store (planId, plan DAG, status, updatedAt), optionally filtered by status. Wraps plan-store.js's existing listPlans query -- no new logic, no mutation. NOTE: this is the store-backed AMS plan store; it is distinct from ORB's stateless loopover_plan_status tool, which reads the caller's in-memory plan object rather than any persisted store.",
283
+
category: "agent",
284
+
auth: "public",
285
+
locality: "miner",
286
+
availability: "selfhost",
287
+
input: MinerListPlansInput,
288
+
output: MinerListPlansOutput,
289
+
});
290
+
291
+
exportconstMinerGetPlanInput=z.object({
292
+
planId: z.string().min(1),
293
+
});
294
+
295
+
/** `{ planId, found: false }` for an unknown id, or `{ found: true, plan }` -- deliberately not a
296
+
* discriminated union in the schema for the same reason as get_run_state above. */
297
+
exportconstMinerGetPlanOutput=z.looseObject({
298
+
planId: z.string().optional(),
299
+
found: z.boolean(),
300
+
plan: minerPlanRecordSchema.optional(),
301
+
});
302
+
303
+
exportconstminerGetPlanTool=defineTool({
304
+
name: "loopover_miner_get_plan",
305
+
title: "Get miner plan",
306
+
description:
307
+
"Read-only fetch of one persisted plan record by planId (the full plan DAG, status, updatedAt), or an explicit { planId, found: false } for an unknown id. Wraps plan-store.js's existing loadPlan lookup -- no mutation, no DAG/planning logic. Store-backed AMS plan store; distinct from ORB's stateless loopover_plan_status tool.",
"Read-only governor decision log: every accept/deny decision the local governor recorded, with its reason -- an explicit named-column SELECT, never SELECT *. Optional repoFullName filter (the only filter the ledger's readGovernorDecisions accepts). Excludes the raw payload column by construction; adds no decision-making, override, or write capability.",
343
+
category: "agent",
344
+
auth: "public",
345
+
locality: "miner",
346
+
availability: "selfhost",
347
+
input: MinerGovernorDecisionsInput,
348
+
output: MinerGovernorDecisionsOutput,
349
+
});
350
+
351
+
// ── status + doctor ─────────────────────────────────────────────────────────────────────────────
"Read-only miner status + doctor diagnostics. Returns { status, doctor }: status = package/engine versions (+ skew), node version, state-dir path, config-file path, and the resolved coding-agent driver (provider name, the model ENV-VAR NAME -- never its value -- and a CLI-present boolean); doctor = the same checks `loopover-miner doctor` runs (Docker/CLI presence, config validity, ...) as { name, ok, detail }. Reuses collectStatus/runDoctorChecks so it can never drift from the CLI. Only names / booleans / paths -- never any env-var value, token, key, or credential. Read-only; no writes or state changes.",
"Read-only miner-local prediction-accuracy report: per-project merge/close precision, joining this miner's own recorded gate predictions (prediction ledger) with the realized PR outcomes it later observed (pr_outcome events). Wraps calibration-cli.js's existing toPredictionRecords/toOutcomeRecords mappers and calibration.js's buildCalibrationReport composer -- no new join/scoring logic, no mutation. Strictly local and offline; distinct from ORB's hosted, maintainer-authenticated loopover_get_outcome_calibration tool, which reads a different (D1) data source. Takes no arguments.",
0 commit comments