loopctl-mcp-server 2.29.0 → 2.30.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.
Files changed (3) hide show
  1. package/README.md +8 -4
  2. package/index.js +91 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP (Model Context Protocol) server for [loopctl](https://loopctl.com) -- structural trust for AI development loops.
4
4
 
5
- Wraps the loopctl REST API into 65 typed MCP tools so AI coding agents (Claude Code, etc.) can interact with loopctl without writing curl commands.
5
+ Wraps the loopctl REST API into 69 typed MCP tools so AI coding agents (Claude Code, etc.) can interact with loopctl without writing curl commands.
6
6
 
7
7
  ## Installation
8
8
 
@@ -66,7 +66,7 @@ Or if installed locally:
66
66
 
67
67
  Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_KEY`.
68
68
 
69
- ## Tools (65)
69
+ ## Tools (69)
70
70
 
71
71
  ### Project Tools
72
72
 
@@ -117,7 +117,7 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
117
117
 
118
118
  | Tool | Description |
119
119
  |---|---|
120
- | `bulk_mark_complete` | Bulk mark multiple stories as complete in a single API call. |
120
+ | `bulk_mark_complete` | **Backfill-only.** Bulk-marks pre-existing, never-dispatched stories (pending, unassigned, no dispatch lineage) as complete in a single call. Dispatched stories are refused — they must go through the normal report → review → verify flow. |
121
121
  | `verify_all_in_epic` | Bulk verify all reported_done, unverified stories in an epic. |
122
122
 
123
123
  ### Token Efficiency Tools
@@ -156,6 +156,8 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
156
156
  | `knowledge_distant_pairs` | Distant-but-bridgeable article pairs in the optimal-novelty embedding band (cosine distance, default 0.3–0.7) — the creative sweet spot. Sampled from **agent's visible published articles**; agent callers see only their own and `shared` articles. `bridge_path: true` requires a ≤2-hop link path. Returns `{a, b, distance}` pairs, paginated. Optional: `min_distance`, `max_distance`, `bridge_path`, `limit` (default 20, max 100), `offset`. |
157
157
  | `knowledge_novelty` | Score ideas by novelty: embeds each idea's text, returns `novelty_score` = cosine distance to the nearest **visible** prior proposal (0 = identical, higher = more novel, up to 2.0; `null` when the idea text is blank, no visible priors exist, or embedding fails — see `meta.prior_count`). Agent callers see only their own and `shared` articles as priors. Priors default to articles tagged `proposal`. Provide ideas as `texts` (strings) OR `ideas` (strings or objects), ≤50. Optional: `prior_tag`. |
158
158
  | `knowledge_random_walk` | Random walk through the link graph from `start_id` (no cycles, up to `length` nodes), traversing only **agent's visible published articles**, surfacing unexpected connections. Agent callers see only their own and `shared` articles. Returns `{id, title, category}` in walk order. Required: `start_id`. Optional: `length` (default 4, max 25). |
159
+ | `knowledge_conflicts` | List potential-conflict article pairs — published articles flagged "too similar to comfortably coexist" by the auto-linker / nightly lint sweep, highest-overlap first. The KB only FLAGS the pair; it does NOT decide redundancy-vs-contradiction — that's your call with live context. Each entry has both articles (id/title/status/category) + similarity. Then merge (supersede one, `knowledge_create` the merged article, or PATCH) or reconcile if they genuinely disagree. Paginated with `total_count` in meta. Agent role. Optional: `limit` (default 50, max 1000, clamped), `offset`. |
160
+ | `knowledge_resolve_conflict` | Record YOUR verdict on a potential-conflict pair (from `knowledge_conflicts`). Dispositions: `dismiss` (false positive, drops from queue), `supersede` (one wins — pass `authoritative_article_id`; nightly executor links + retires loser, only at `confidence:"high"`, reversible/audited), `merge` (at high confidence an LLM synthesizes both into ONE new DRAFT, sources preserved, never auto-published). Non-destructive at agent role — you record intent, the privileged nightly job executes. Last-write-wins per pair. Required: `source_article_id`, `target_article_id`, `disposition`. Optional: `authoritative_article_id`, `classification`, `evidence`, `confidence`. |
159
161
  | `knowledge_create` | Create a new knowledge article. File findings, document patterns, or record decisions. **Published immediately by default** (visible per `metadata.visibility` — default `owner` for agent authors, only visible to that agent; `shared` for visibility to all agents) — the response `note` says which outcome occurred. Pass `draft: true` to stage it for later review instead (publish afterwards with `knowledge_publish`). Pass `metadata: {visibility: "shared"}` to make the article visible to other agents; higher roles can set visibility and agent_id explicitly. Pass `idempotency_key` for idempotent capture (re-creating with the same key is a no-op returning the existing article — no partial duplicates). Optional: `category`, `tags`, `project_id`, `draft`, `idempotency_key`, `source_type`, `source_id`, `metadata`. |
160
162
  | `knowledge_okf_export` | **Requires `LOOPCTL_USER_KEY`.** Export the wiki as a portable OKF (Open Knowledge Format) v0.1 bundle of markdown files. Writes to `out_dir`, or returns `{files, meta}` inline. |
161
163
  | `knowledge_okf_import` | **Requires `LOOPCTL_USER_KEY`.** Import an OKF v0.1 bundle from a local directory. Creates or (with `merge`) updates articles; tolerates and preserves unknown frontmatter. |
@@ -173,7 +175,7 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
173
175
  | `knowledge_bulk_delete` | **Requires `LOOPCTL_USER_KEY`.** Bulk archive (default, reversible) or IRREVERSIBLE hard-delete by selector. Provide exactly one selector: `article_ids` (list), `source_type`+`source_id` (every active article from a source), or `tag`+`confirm:true` (every active article with the tag — high blast radius). Default = set-based soft archive (idempotent; `meta.count`=archived, `meta.counts`/`meta.results` give the breakdown; ≤5000). **Dry-run** (`dry_run:true`) mutates nothing, returns `meta.would_affect` (with `hard:true` also a single-use `meta.token`, or `meta.confirm_hash` for oversized selectors). **Hard delete** (irreversible): dry-run with `hard:true` for a token, then call again with `hard:true`+`token` to FK-correctly delete the frozen id-set (links first, access events cascade). |
174
176
  | `knowledge_drafts` | List draft (unpublished) knowledge articles with pagination. Optional: `limit` (default 20, max 1000 — over-max → 400, no silent clamp), `offset` (default 0), `project_id`. Returns `meta.total_count`. |
175
177
  | `knowledge_lint` | Run a lint check on the knowledge wiki to identify stale or low-coverage articles. Optional: `project_id`, `stale_days`, `min_coverage`, `max_per_category` (default 50, max 500). True totals returned in `summary.total_per_category`. |
176
- | `knowledge_export` | Export all knowledge articles as an OKF v0.1 bundle (gzipped tar archive, unbounded, bounded-memory streaming, fail-closed). Returns a curl command for direct download. Pass `format=json` for buffered in-memory JSON (convenience tool for file writers; capped at `export_max_buffered_export_articles` — returns 413 if over-cap). Optional: `project_id`, `format` (tar.gz default, json, or obsidian). |
178
+ | `knowledge_export` | Export all knowledge articles as an OKF v0.1 bundle (gzipped tar archive, unbounded, bounded-memory streaming, fail-closed). Returns a curl command for direct download — **the download requires `LOOPCTL_USER_KEY`** (an orchestrator key would 403). Pass `format=json` for buffered in-memory JSON (convenience tool for file writers; capped at `export_max_buffered_export_articles` — returns 413 if over-cap). Optional: `project_id`, `format` (`tar.gz` default or `json`). |
177
179
  | `knowledge_ingest` | Submit a URL or raw content for knowledge extraction. Enqueues an Oban job. Extracted articles are **drafts by default** (lower-trust LLM output); pass `publish: true` to publish on extraction. Required: `source_type`. One of: `url` or `content`. Optional: `project_id`, `publish`. |
178
180
  | `knowledge_ingest_batch` | Submit up to 50 ingestion items in a single request. Each item has the same shape as `knowledge_ingest` (incl. `publish`). Returns per-item results. Required: `items`. Optional: batch-level `project_id` / `publish` defaults. |
179
181
  | `knowledge_ingestion_jobs` | List recent content ingestion jobs (last 7 days, max 50). |
@@ -186,6 +188,8 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
186
188
  | `knowledge_article_stats` | Per-article usage stats: total accesses, unique agents, by-type breakdown, recent events. Required: `article_id`. |
187
189
  | `knowledge_agent_usage` | Per-agent knowledge usage: total reads, unique articles, top read articles. Required: exactly one of `api_key_id` (credential) or `agent_id` (logical identity). Optional: `limit`, `since_days`. See Wiki Attribution section. |
188
190
  | `knowledge_unused_articles` | Published articles with zero accesses in the window. Optional: `days_unused` (default 30), `limit` (default 50, max 200). |
191
+ | `knowledge_curation_log` | Concise human-readable log of KB CURATION adjustments — novelty-gate decisions (`gate_duplicate`/`gate_draft`) and conflict resolutions (`supersede`/`merge`/`dismiss`) — for analyzing the agents'-KB rollout, distinct from the verbose audit log. Each entry: `{at, kind, summary, refs, actor, confidence}`. **RECORDED ONLY when `settings.kb_curation_log` is on** (PATCH `/api/v1/admin/tenants/:id` with `settings:{kb_curation_log:true}`); off by default = no rows. Most recent first. Requires orchestrator role. Optional: `kind`, `since` (ISO8601), `limit` (default 50, max 500), `offset`. |
192
+ | `knowledge_retrieval_metrics` | Daily retrieval-PRECISION time series: for each day, the share of search results the agent then opened (search → get/context within a window). A proxy for whether retrieval is improving as the corpus is de-duplicated, better navigated (MOCs), and conflict-resolved. Most recent day first. Requires orchestrator role. Optional: `limit` (default 30, max 365), `offset`. |
189
193
 
190
194
  ### Discovery Tools
191
195
 
package/index.js CHANGED
@@ -10,6 +10,17 @@ import {
10
10
  ListToolsRequestSchema,
11
11
  CallToolRequestSchema,
12
12
  } from "@modelcontextprotocol/sdk/types.js";
13
+ import { readFileSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { dirname, join } from "node:path";
16
+
17
+ // Single source of truth for the server version: the package.json this file
18
+ // ships with (npm always includes package.json in the published tarball).
19
+ // Keeping it derived prevents the handshake version from drifting from the
20
+ // published package version.
21
+ const SERVER_VERSION = JSON.parse(
22
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), "package.json"), "utf8")
23
+ ).version;
13
24
 
14
25
  // ---------------------------------------------------------------------------
15
26
  // HTTP helper — witness protocol state
@@ -96,7 +107,28 @@ async function apiCall(method, path, body, keyOverride) {
96
107
  let responseBody;
97
108
  const contentType = response.headers.get("content-type") || "";
98
109
  if (contentType.includes("application/json")) {
99
- responseBody = await response.json();
110
+ // A JSON content-type is no guarantee of a well-formed body: a transient Fly
111
+ // edge 502/503 or a truncated/empty response can arrive with the JSON header.
112
+ // Read the raw text and parse defensively so a malformed body becomes a
113
+ // structured MCP error instead of an unhandled throw from response.json().
114
+ const raw = await response.text();
115
+ if (raw.trim() === "") {
116
+ return {
117
+ error: true,
118
+ status: response.status,
119
+ body: `invalid/empty JSON response from server (HTTP ${response.status}): empty body`,
120
+ };
121
+ }
122
+ try {
123
+ responseBody = JSON.parse(raw);
124
+ } catch {
125
+ const snippet = raw.length > 200 ? `${raw.slice(0, 200)}... (truncated)` : raw;
126
+ return {
127
+ error: true,
128
+ status: response.status,
129
+ body: `invalid/empty JSON response from server (HTTP ${response.status}): ${snippet}`,
130
+ };
131
+ }
100
132
  } else {
101
133
  const text = await response.text();
102
134
  try {
@@ -1163,6 +1195,20 @@ async function knowledgeRetrievalMetrics({ limit, offset } = {}) {
1163
1195
  return toContent(result);
1164
1196
  }
1165
1197
 
1198
+ async function knowledgeCurationLog({ kind, since, limit, offset } = {}) {
1199
+ const params = new URLSearchParams();
1200
+ if (kind) params.set("kind", kind);
1201
+ if (since) params.set("since", since);
1202
+ if (limit != null) params.set("limit", String(limit));
1203
+ if (offset != null) params.set("offset", String(offset));
1204
+ const qs = params.toString();
1205
+ const path = qs
1206
+ ? `/api/v1/knowledge/curation-log?${qs}`
1207
+ : "/api/v1/knowledge/curation-log";
1208
+ const result = await apiCall("GET", path, null, process.env.LOOPCTL_ORCH_KEY);
1209
+ return toContent(result);
1210
+ }
1211
+
1166
1212
  async function knowledgeArticleStats({ article_id }) {
1167
1213
  const result = await apiCall(
1168
1214
  "GET",
@@ -1823,7 +1869,7 @@ const TOOLS = [
1823
1869
  properties: {
1824
1870
  input_tokens: { type: "integer", description: "Total input tokens consumed." },
1825
1871
  output_tokens: { type: "integer", description: "Total output tokens consumed." },
1826
- model_name: { type: "string", description: "Model name (e.g. claude-sonnet-4-5)." },
1872
+ model_name: { type: "string", description: "Model name (e.g. claude-sonnet-5)." },
1827
1873
  cost_millicents: { type: "integer", description: "Total cost in millicents (1/1000 of a cent)." },
1828
1874
  },
1829
1875
  },
@@ -1920,8 +1966,9 @@ const TOOLS = [
1920
1966
  {
1921
1967
  name: "bulk_mark_complete",
1922
1968
  description:
1923
- "Bulk mark multiple stories as complete in a single API call. " +
1924
- "Each story entry needs a story_id, summary, and review_type. Uses the ORCH key.",
1969
+ "Bulk mark multiple stories as complete (backfill-only for never-dispatched work). " +
1970
+ "ADMIN USE ONLY: backfill pre-existing completed work into pending stories that never entered the dispatch lifecycle. " +
1971
+ "For dispatched stories, use the normal report/review/verify flow. Each story entry needs a story_id, summary, and review_type. Uses the ORCH key.",
1925
1972
  inputSchema: {
1926
1973
  type: "object",
1927
1974
  properties: {
@@ -1999,7 +2046,7 @@ const TOOLS = [
1999
2046
  },
2000
2047
  model_name: {
2001
2048
  type: "string",
2002
- description: "Name of the model used (e.g. claude-sonnet-4-5, gpt-4o).",
2049
+ description: "Name of the model used (e.g. claude-sonnet-5, gpt-4o).",
2003
2050
  },
2004
2051
  cost_millicents: {
2005
2052
  type: "integer",
@@ -3266,6 +3313,39 @@ const TOOLS = [
3266
3313
  },
3267
3314
 
3268
3315
  // Knowledge Analytics Tools (orchestrator key)
3316
+ {
3317
+ name: "knowledge_curation_log",
3318
+ description:
3319
+ "The concise, human-readable log of KB CURATION adjustments — novelty-gate decisions " +
3320
+ "(gate_duplicate/gate_draft) and conflict resolutions (supersede/merge/dismiss) — for " +
3321
+ "analyzing the agents'-KB rollout, distinct from the verbose audit log. Each entry is a " +
3322
+ "one-liner: {at, kind, summary, refs, actor, confidence}. RECORDED ONLY while the tenant " +
3323
+ "has the toggle on: settings.kb_curation_log (flip via the admin tenant API, " +
3324
+ "PATCH /api/v1/admin/tenants/:id with settings:{kb_curation_log:true}). Off by default = " +
3325
+ "no rows. Filter by kind and since (ISO8601). Most recent first. Requires orchestrator role.",
3326
+ inputSchema: {
3327
+ type: "object",
3328
+ properties: {
3329
+ kind: {
3330
+ type: "string",
3331
+ description:
3332
+ "Optional: filter by kind (gate_duplicate | gate_draft | supersede | merge | dismiss).",
3333
+ },
3334
+ since: {
3335
+ type: "string",
3336
+ description: "Optional: ISO8601 date or datetime lower bound (inclusive).",
3337
+ },
3338
+ limit: {
3339
+ type: "integer",
3340
+ description: "Events per page (default 50, max 500). Clamped, never rejected.",
3341
+ minimum: 1,
3342
+ maximum: 500,
3343
+ },
3344
+ offset: { type: "integer", description: "Events to skip. Default 0.", minimum: 0 },
3345
+ },
3346
+ required: [],
3347
+ },
3348
+ },
3269
3349
  {
3270
3350
  name: "knowledge_retrieval_metrics",
3271
3351
  description:
@@ -3511,7 +3591,7 @@ const TOOLS = [
3511
3591
  const server = new Server(
3512
3592
  {
3513
3593
  name: "loopctl",
3514
- version: "1.2.0",
3594
+ version: SERVER_VERSION,
3515
3595
  },
3516
3596
  {
3517
3597
  capabilities: { tools: {} },
@@ -3531,7 +3611,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3531
3611
  return await getTenant();
3532
3612
 
3533
3613
  case "list_projects":
3534
- return await listProjects();
3614
+ return await listProjects(args);
3535
3615
 
3536
3616
  case "create_project":
3537
3617
  return await createProject(args);
@@ -3705,9 +3785,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3705
3785
  return await knowledgeIngestBatch(args);
3706
3786
 
3707
3787
  case "knowledge_ingestion_jobs":
3708
- return await knowledgeIngestionJobs();
3788
+ return await knowledgeIngestionJobs(args);
3709
3789
 
3710
3790
  // Knowledge Analytics Tools
3791
+ case "knowledge_curation_log":
3792
+ return await knowledgeCurationLog(args);
3793
+
3711
3794
  case "knowledge_retrieval_metrics":
3712
3795
  return await knowledgeRetrievalMetrics(args);
3713
3796
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.29.0",
3
+ "version": "2.30.1",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",