Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions apps/loopover-ui/src/lib/mcp-tool-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ export const MCP_TOOL_REFERENCE: readonly McpToolReferenceEntry[] = [
"availability": "both",
"description": "Create a queued copilot-only LoopOver agent run. The agent plans and explains; it does not edit code or open PRs."
},
{
"name": "loopover_ams_tenant_health",
"category": "tenant",
"locality": "remote",
"availability": "cloud",
"description": "Operator only. One hosted AMS tenant's health: lifecycle state, its cron-wake cadence, when it last woke, that cycle's outcome, and container health. Read-only, and scoped server-side to the authenticated tenant — a name outside that scope is refused rather than answered."
},
{
"name": "loopover_ams_tenant_wake",
"category": "tenant",
"locality": "remote",
"availability": "cloud",
"description": "Operator only. Trigger an immediate cycle for one hosted AMS tenant, instead of waiting for its next scheduled wake. Bounded by the SAME per-tenant schedule guards the cron path obeys — a wake too soon after the last one is reported as throttled rather than forced through."
},
{
"name": "loopover_apply_labels",
"category": "agent",
Expand Down Expand Up @@ -696,6 +710,27 @@ export const MCP_TOOL_REFERENCE: readonly McpToolReferenceEntry[] = [
"availability": "both",
"description": "Mark a contributor's own delivered notifications as read (clears the badge). Self-scoped; pass `ids` to clear specific notifications or omit to clear all."
},
{
"name": "loopover_miner_claim_release",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "Release this miner's claim on one issue, so the claim ledger no longer reserves it. Dispatches through the governor-gated chokepoint and is recorded in the event ledger with source=mcp."
},
{
"name": "loopover_miner_deny_hooks_decide",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "Approve or reject one synthesized deny-hook awaiting review. Approving puts it into force for future runs; rejecting discards it. Dispatches through the governor-gated chokepoint and is recorded in the event ledger with source=mcp."
},
{
"name": "loopover_miner_doctor",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "Read-only diagnostic checks for this AMS miner: state directory, engine version match, store reachability, credentials, and configuration. Split out of loopover_miner_status (#9523) so status stays cheap and doctor can grow checks. Every check runs and reports its own pass/warn/fail — nothing is mutated and nothing stops at the first failure."
},
{
"name": "loopover_miner_get_audit_feed",
"category": "agent",
Expand Down Expand Up @@ -724,6 +759,13 @@ export const MCP_TOOL_REFERENCE: readonly McpToolReferenceEntry[] = [
"availability": "selfhost",
"description": "Read-only manage-phase status: the per-managed-PR rows `loopover-miner manage status` reports (branch, CI state, gate verdict, outcome, last-polled-at, queue status/priority) plus the run-level portfolio view (one row per tracked repo: run state, updated-at, PR count). Joins the portfolio queue, the append-only event ledger, and run-state by reusing the existing collectManageStatus/collectRunPortfolio aggregators -- no new join logic. Read-only: never calls GitHub, never mutates local stores. Takes no arguments."
},
{
"name": "loopover_miner_get_metrics_snapshot",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "The same Prometheus metric families the `metrics` CLI exports, as structured JSON — so an agent can read them without parsing the text exposition format. Read-only."
},
{
"name": "loopover_miner_get_plan",
"category": "agent",
Expand All @@ -745,6 +787,20 @@ export const MCP_TOOL_REFERENCE: readonly McpToolReferenceEntry[] = [
"availability": "selfhost",
"description": "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."
},
{
"name": "loopover_miner_governor_pause",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "Pause this miner's governor: no new work is admitted until it resumes. Administrative control, not a content write — the same action the dashboard's pause button dispatches, through the same governor-gated chokepoint, firing the same notification side-channel. Recorded in the event ledger with source=mcp."
},
{
"name": "loopover_miner_governor_resume",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "Resume this miner's governor after a pause, re-admitting work. The same action the dashboard's resume button dispatches, through the same governor-gated chokepoint. Recorded in the event ledger with source=mcp."
},
{
"name": "loopover_miner_list_claims",
"category": "agent",
Expand All @@ -766,6 +822,34 @@ export const MCP_TOOL_REFERENCE: readonly McpToolReferenceEntry[] = [
"availability": "selfhost",
"description": "Health check for the loopover-miner MCP server. Returns a static status object confirming the server is reachable. Reads no AMS state and takes no arguments."
},
{
"name": "loopover_miner_purge_repo",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "Right-to-be-forgotten: delete every trace of one repo from this miner's local stores, returning the same per-store report as the CLI. IRREVERSIBLE — the rows are gone, not archived. Requires confirm=true, elicits confirmation where the client supports it, and dispatches through the governor-gated chokepoint."
},
{
"name": "loopover_miner_queue_release",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "Release one claimed portfolio-queue item back to unclaimed, so another cycle can pick it up. Mirrors the dashboard's release action and dispatches through the same governor-gated chokepoint. Recorded in the event ledger with source=mcp."
},
{
"name": "loopover_miner_queue_requeue",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "Return one portfolio-queue item to the pending pool for another attempt. Mirrors the dashboard's requeue action and dispatches through the same governor-gated chokepoint. Recorded in the event ledger with source=mcp."
},
{
"name": "loopover_miner_run_migrations",
"category": "ops",
"locality": "miner",
"availability": "selfhost",
"description": "Apply pending schema migrations to this miner's EXISTING local stores — it never creates a store that is not already there. Reports each store as migrated, up-to-date, or failed. There is no dry-run mode: applying a migration is opening the store, and the CLI has none either. Dispatches through the governor-gated chokepoint."
},
{
"name": "loopover_miner_status",
"category": "utility",
Expand Down
72 changes: 72 additions & 0 deletions packages/loopover-contract/src/tools/ams-tenant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// The hosted AMS tenant surface (#9523, #9199).
//
// CATALOG AMENDMENT, recorded here rather than improvised: #9523 listed
// `loopover_ams_tenant_create` / `_list` / `_destroy`, and they are NOT here. #9522 landed
// `loopover_tenant_create` / `_list` / `_destroy` taking a `product` of "ams" or "orb", because the control
// plane's own `/v1/tenants` routes are product-parameterized and key their registry by `${product}:${name}`.
// A second, AMS-only spelling of those three would be two names for one capability -- the same rot that kept
// `loopover_fleet_get_analytics` out of #9522 -- and would leave an agent guessing which to call. Use the
// product-parameterized tools with `product: "ams"`.
//
// What IS genuinely AMS-specific, and therefore here, is the pair that has no ORB counterpart: a tenant's
// wake schedule and cycle outcomes, and the ability to trigger a cycle now.
import { z } from "zod";
import { defineTool } from "../tool-definition.js";

const TenantName = z.string().min(1).max(200);

export const AmsTenantHealthInput = z.object({
name: TenantName.describe("The tenant's name, as reported by loopover_tenant_list with product=ams."),
});

export const AmsTenantHealthOutput = z.looseObject({
configured: z.boolean().describe("False when this deployment administers no hosted tenants."),
name: z.string().optional(),
state: z.string().optional().describe("The control plane's own lifecycle vocabulary, passed through verbatim."),
schedule: z.string().nullable().optional().describe("The cron-wake cadence, or null when the tenant wakes only on demand."),
lastWakeAt: z.string().nullable().optional(),
lastCycleOutcome: z.string().nullable().optional(),
containerHealthy: z.boolean().nullable().optional(),
error: z.string().optional(),
});

export const amsTenantHealthTool = defineTool({
name: "loopover_ams_tenant_health",
title: "Read a hosted AMS tenant's health",
description:
"Operator only. One hosted AMS tenant's health: lifecycle state, its cron-wake cadence, when it last woke, that cycle's outcome, and container health. Read-only, and scoped server-side to the authenticated tenant — a name outside that scope is refused rather than answered.",
category: "tenant",
auth: "internal",
locality: "remote",
availability: "cloud",
input: AmsTenantHealthInput,
output: AmsTenantHealthOutput,
});

export const AmsTenantWakeInput = z.object({
name: TenantName,
});

export const AmsTenantWakeOutput = z.looseObject({
configured: z.boolean(),
name: z.string().optional(),
woken: z.boolean().optional(),
throttled: z.boolean().optional().describe("True when the tenant's own schedule guard refused a wake this soon after the last one."),
error: z.string().optional(),
});

export const amsTenantWakeTool = defineTool({
name: "loopover_ams_tenant_wake",
title: "Wake a hosted AMS tenant now",
description:
"Operator only. Trigger an immediate cycle for one hosted AMS tenant, instead of waiting for its next scheduled wake. Bounded by the SAME per-tenant schedule guards the cron path obeys — a wake too soon after the last one is reported as throttled rather than forced through.",
category: "tenant",
auth: "internal",
locality: "remote",
availability: "cloud",
annotations: { readOnlyHint: false, destructiveHint: false },
input: AmsTenantWakeInput,
output: AmsTenantWakeOutput,
});

export const AMS_TENANT_TOOLS = [amsTenantHealthTool, amsTenantWakeTool] as const;
6 changes: 6 additions & 0 deletions packages/loopover-contract/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ import { OPS_TOOLS } from "./ops.js";
import { FLEET_TOOLS } from "./fleet.js";
import { TENANT_TOOLS } from "./tenant.js";
import { INSTANCE_OPS_TOOLS } from "./instance-ops.js";
import { MINER_OPS_TOOLS } from "./miner-ops.js";
import { AMS_TENANT_TOOLS } from "./ams-tenant.js";
import { adminRotateSecretTool } from "./admin-config.js";

export const TOOL_CONTRACTS: readonly ToolContract[] = [
Expand Down Expand Up @@ -282,6 +284,8 @@ export const TOOL_CONTRACTS: readonly ToolContract[] = [
...OPS_TOOLS,
...FLEET_TOOLS,
...TENANT_TOOLS,
...MINER_OPS_TOOLS,
...AMS_TENANT_TOOLS,
];

const CONTRACTS_BY_NAME: ReadonlyMap<string, ToolContract> = new Map(
Expand Down Expand Up @@ -318,3 +322,5 @@ export * from "./ops.js";
export * from "./fleet.js";
export * from "./tenant.js";
export * from "./instance-ops.js";
export * from "./miner-ops.js";
export * from "./ams-tenant.js";
Loading
Loading