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.
- package/README.md +8 -4
- package/index.js +91 -8
- 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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
1924
|
-
"
|
|
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-
|
|
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:
|
|
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
|
|