loopctl-mcp-server 2.30.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 +41 -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 {
@@ -1837,7 +1869,7 @@ const TOOLS = [
1837
1869
  properties: {
1838
1870
  input_tokens: { type: "integer", description: "Total input tokens consumed." },
1839
1871
  output_tokens: { type: "integer", description: "Total output tokens consumed." },
1840
- 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)." },
1841
1873
  cost_millicents: { type: "integer", description: "Total cost in millicents (1/1000 of a cent)." },
1842
1874
  },
1843
1875
  },
@@ -1934,8 +1966,9 @@ const TOOLS = [
1934
1966
  {
1935
1967
  name: "bulk_mark_complete",
1936
1968
  description:
1937
- "Bulk mark multiple stories as complete in a single API call. " +
1938
- "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.",
1939
1972
  inputSchema: {
1940
1973
  type: "object",
1941
1974
  properties: {
@@ -2013,7 +2046,7 @@ const TOOLS = [
2013
2046
  },
2014
2047
  model_name: {
2015
2048
  type: "string",
2016
- 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).",
2017
2050
  },
2018
2051
  cost_millicents: {
2019
2052
  type: "integer",
@@ -3558,7 +3591,7 @@ const TOOLS = [
3558
3591
  const server = new Server(
3559
3592
  {
3560
3593
  name: "loopctl",
3561
- version: "1.2.0",
3594
+ version: SERVER_VERSION,
3562
3595
  },
3563
3596
  {
3564
3597
  capabilities: { tools: {} },
@@ -3578,7 +3611,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3578
3611
  return await getTenant();
3579
3612
 
3580
3613
  case "list_projects":
3581
- return await listProjects();
3614
+ return await listProjects(args);
3582
3615
 
3583
3616
  case "create_project":
3584
3617
  return await createProject(args);
@@ -3752,7 +3785,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3752
3785
  return await knowledgeIngestBatch(args);
3753
3786
 
3754
3787
  case "knowledge_ingestion_jobs":
3755
- return await knowledgeIngestionJobs();
3788
+ return await knowledgeIngestionJobs(args);
3756
3789
 
3757
3790
  // Knowledge Analytics Tools
3758
3791
  case "knowledge_curation_log":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.30.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",