loopctl-mcp-server 2.46.0 → 2.47.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 (3) hide show
  1. package/README.md +24 -1
  2. package/index.js +102 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -121,7 +121,7 @@ REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (
121
121
  autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
122
122
  [`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
123
123
 
124
- ## Tools (89)
124
+ ## Tools (95)
125
125
 
126
126
  > Plus **per-tenant generated Context Retriever tools** (`cr_*`) appended
127
127
  > dynamically at runtime — see [Dynamic per-tenant Context Retriever
@@ -139,6 +139,25 @@ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycl
139
139
  | `get_progress` | Get progress summary for a project, including story counts by status. Pass `include_cost=true` for cost data. |
140
140
  | `import_stories` | Import stories into a project from a structured payload (Epic 12 import format). Pass `merge: true` to add stories to epics that already exist (otherwise duplicates return 409). For large payloads, use `payload_path` to read JSON from disk instead of passing it inline. |
141
141
 
142
+ ### KB Scope Tools (agent key)
143
+
144
+ Knowledge-only project scopes (`kind: kb`) partition knowledge articles by repo. Unlike `create_project` (work project, orchestrator+ / human-anchored), these are available to an agent-rooted (KB-tier) tenant on an agent key and carry NO chain-of-custody / work-breakdown surface.
145
+
146
+ | Tool | Description |
147
+ |---|---|
148
+ | `create_kb_scope` | Create a knowledge-only project scope (`kind: kb`) for the current tenant. A kb scope cannot host epics/stories/dispatch/ui-tests — it exists only to partition knowledge articles by repo. Resolve it via `resolve_project` and pass the returned id as `project_id` on article/knowledge writes. Counts toward the tenant's `max_projects` budget. |
149
+ | `archive_kb_scope` | Archive (reversible soft-delete) a kb scope you own. Frees the scope's slot in the tenant's `max_projects` budget; its articles remain readable/writable. Rejects a `kind: work` project (422). Idempotent on an already-archived scope. |
150
+ | `restore_kb_scope` | Restore (re-activate) an archived kb scope you own — the reverse of `archive_kb_scope`. Consumes an active `max_projects` slot, so it is rejected (422) when the tenant is at its cap. Rejects a `kind: work` project (422). |
151
+
152
+ ### Repo Coordination Tools (agent key)
153
+
154
+ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for agents to share working state. A channel IS a `project_id` (a work project or a kb scope); posts are RLS-scoped to the caller's tenant, so this is an agent-role coordination surface, not a chain-of-custody gate.
155
+
156
+ | Tool | Description |
157
+ |---|---|
158
+ | `channel_post` | Post a message to a repo coordination channel. Provide a `key` to upsert your per-session working-state slot (200) instead of appending a new post (201); omit it to append. `host` and `session_id` are proxy-supplied — do NOT pass them. Optional structured `refs` map (`file`, `pr`, `branch`, `commit`). Required: `project_id`, `body`. |
159
+ | `channel_recent` | Read recent posts from a repo coordination channel — RLS returns only your own tenant's channel (oracle-safe read). Use `since` (a full ISO8601 instant) to page forward and `limit` to cap results (default 25, max 100). Required: `project_id`. |
160
+
142
161
  ### Story Tools
143
162
 
144
163
  | Tool | Description |
@@ -323,6 +342,10 @@ Key distribution for the dispatch pattern (Epic 26): per-dispatch ephemeral keys
323
342
  | `dispatch` | Mint an ephemeral, scoped api_key for a sub-agent dispatch, carrying its lineage path. The `raw_key` is returned ONCE — pass it to the sub-agent's launch args, never store it in env vars; it expires after `expires_in_seconds` (default 3600, max 14400). Required: `role` (`agent`/`orchestrator`), `agent_id`. Optional: `parent_dispatch_id`, `story_id`. |
324
343
  | `recover_cap` | Re-mint a capability token for a story you're assigned to, after a session crash lost your cap. Required: `story_id`. Optional: `cap_type` (`start_cap`/`report_cap`, default `start_cap`), `lineage`. |
325
344
  | `get_sth` | Get the latest Signed Tree Head for a tenant's tamper-evident audit chain. Public — no auth required. Required: `tenant_id`. |
345
+ | `request_authenticator_challenge` | **US-26.7.2.** Step 1 of the opt-in WebAuthn trust-tier upgrade ceremony: issues a registration challenge for enrolling a hardware authenticator against an EXISTING agent-rooted (KB-tier) tenant, promoting it to `human_anchored` on success. Requires an interactive WebAuthn client. |
346
+ | `enroll_authenticator` | Step 2 of the WebAuthn trust-tier upgrade ceremony: completes enrollment with the attestation produced by `navigator.credentials.create()` against the challenge from `request_authenticator_challenge`. On a tenant's first enrollment the tenant is promoted to `human_anchored`. |
347
+ | `request_authenticator_revoke_challenge` | Issues a fresh-assertion challenge to authorize revoking one of a tenant's enrolled WebAuthn authenticators. Requires an interactive WebAuthn client + human touch to produce the assertion `revoke_authenticator` needs. |
348
+ | `revoke_authenticator` | Revokes an enrolled authenticator using the assertion from `request_authenticator_revoke_challenge` (`navigator.credentials.get()` against an existing authenticator — human touch required). Refuses (409 `last_authenticator`) when it would leave a human-anchored tenant with no authenticators. |
326
349
 
327
350
  ## Wiki Attribution
328
351
 
package/index.js CHANGED
@@ -437,6 +437,46 @@ async function restoreKbScope({ project_id }) {
437
437
  return toContent(result);
438
438
  }
439
439
 
440
+ async function channelPost({ project_id, body, key, refs }) {
441
+ // Repo Coordination Bus (Epic 39): post a coordination message to a channel
442
+ // (a channel IS a project_id — a work project or a kb scope). Agent-role, RLS
443
+ // tenant-scoped — posting to your own tenant's channel is coordination, NOT
444
+ // self-approval (owner decision #331), so it carries no chain-of-custody authority.
445
+ const payload = { project_id, body };
446
+ if (key) payload.key = key;
447
+ if (refs) payload.refs = refs;
448
+ // host + session_id are proxy-filled (NOT caller args). host from os.hostname();
449
+ // session_id from CLAUDE_SESSION_ID (the SAME id SessionStart sees) so US-39.6
450
+ // self-dedup can skip a session's own echoed posts. Omit session_id entirely when
451
+ // unset — it is client-supplied + informational, never a security dependency.
452
+ payload.host = os.hostname();
453
+ if (process.env.CLAUDE_SESSION_ID) payload.session_id = process.env.CLAUDE_SESSION_ID;
454
+ const result = await apiCall(
455
+ "POST",
456
+ "/api/v1/channel/posts",
457
+ payload,
458
+ process.env.LOOPCTL_AGENT_KEY,
459
+ );
460
+ return toContent(result);
461
+ }
462
+
463
+ async function channelRecent({ project_id, since, limit }) {
464
+ // Read recent coordination posts for a channel (project_id) on the AGENT key.
465
+ // Oracle-safe read: returns the tenant's own channel only (RLS). `since` is a full
466
+ // ISO8601 instant (date-only is ignored server-side); limit defaults to 25, max 100.
467
+ const params = new URLSearchParams();
468
+ if (project_id) params.set("project_id", project_id);
469
+ if (since) params.set("since", since);
470
+ if (limit) params.set("limit", limit);
471
+ const result = await apiCall(
472
+ "GET",
473
+ `/api/v1/channel/posts?${params}`,
474
+ null,
475
+ process.env.LOOPCTL_AGENT_KEY,
476
+ );
477
+ return toContent(result);
478
+ }
479
+
440
480
  async function deleteProject({ project_id }) {
441
481
  const result = await apiCall(
442
482
  "DELETE",
@@ -2207,6 +2247,62 @@ const TOOLS = [
2207
2247
  required: ["project_id"],
2208
2248
  },
2209
2249
  },
2250
+ {
2251
+ name: "channel_post",
2252
+ description:
2253
+ "Post a message to a repo coordination channel (Epic 39 Repo Coordination Bus) on the agent key. A channel IS a project_id (a work project or a kb scope); posts are tenant-isolated by RLS. This is an agent-role COORDINATION surface, not chain-of-custody — posting to your own tenant's channel is not self-approval. host is auto-filled from the proxy's os.hostname() and session_id is auto-filled from the Claude Code session id (both proxy-supplied, informational only — do NOT pass them). Provide a key to upsert your per-session working-state slot (200) instead of appending a new post (201); omit it to append.",
2254
+ inputSchema: {
2255
+ type: "object",
2256
+ properties: {
2257
+ project_id: {
2258
+ type: "string",
2259
+ description: "UUID of the channel (a work project or kb scope) to post to.",
2260
+ },
2261
+ body: { type: "string", description: "The coordination message body." },
2262
+ key: {
2263
+ type: "string",
2264
+ description:
2265
+ "Optional per-session working-state slot key. When given, upserts the caller's slot for that key instead of appending a new post. Requires an active Claude Code session: the upsert is keyed on the auto-filled session_id (from CLAUDE_SESSION_ID), so a keyed post made outside a Claude Code session — where that env var is absent — is rejected with a 422 (session_id can't be blank). Omit key to append a plain post, which needs no session.",
2266
+ },
2267
+ refs: {
2268
+ type: "object",
2269
+ description:
2270
+ "Optional structured references map: { file, pr, branch, commit }.",
2271
+ properties: {
2272
+ file: { type: "string" },
2273
+ pr: { type: "string" },
2274
+ branch: { type: "string" },
2275
+ commit: { type: "string" },
2276
+ },
2277
+ },
2278
+ },
2279
+ required: ["project_id", "body"],
2280
+ },
2281
+ },
2282
+ {
2283
+ name: "channel_recent",
2284
+ description:
2285
+ "Read recent posts from a repo coordination channel (Epic 39 Repo Coordination Bus) on the agent key. A channel IS a project_id; RLS returns only your own tenant's channel, so this is an oracle-safe read. Use since (a full ISO8601 instant) to page forward from a known point and limit to cap results (default 25, max 100).",
2286
+ inputSchema: {
2287
+ type: "object",
2288
+ properties: {
2289
+ project_id: {
2290
+ type: "string",
2291
+ description: "UUID of the channel (a work project or kb scope) to read.",
2292
+ },
2293
+ since: {
2294
+ type: "string",
2295
+ description:
2296
+ "Optional ISO8601 instant; return posts newer than this (date-only is ignored server-side).",
2297
+ },
2298
+ limit: {
2299
+ type: "integer",
2300
+ description: "Optional max posts to return (default 25, max 100).",
2301
+ },
2302
+ },
2303
+ required: ["project_id"],
2304
+ },
2305
+ },
2210
2306
  {
2211
2307
  name: "delete_project",
2212
2308
  description:
@@ -5017,6 +5113,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5017
5113
  case "restore_kb_scope":
5018
5114
  return await restoreKbScope(args);
5019
5115
 
5116
+ case "channel_post":
5117
+ return await channelPost(args);
5118
+
5119
+ case "channel_recent":
5120
+ return await channelRecent(args);
5121
+
5020
5122
  case "delete_project":
5021
5123
  return await deleteProject(args);
5022
5124
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.46.0",
3
+ "version": "2.47.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",