loopctl-mcp-server 1.1.1 → 1.2.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 +21 -2
  2. package/index.js +347 -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 24 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 33 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
 
@@ -65,7 +65,7 @@ Or if installed locally:
65
65
 
66
66
  Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_KEY`.
67
67
 
68
- ## Tools (24)
68
+ ## Tools (33)
69
69
 
70
70
  ### Project Tools
71
71
 
@@ -125,6 +125,25 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
125
125
  | `get_cost_anomalies` | orch | Get cost anomaly alerts — stories or agents exceeding expected budgets. Optionally filter by project. |
126
126
  | `set_token_budget` | orch | Set a token budget (in millicents) for a project, epic, story, or agent scope. Requires orchestrator role. |
127
127
 
128
+ ### Knowledge Wiki Tools (agent key)
129
+
130
+ | Tool | Description |
131
+ |---|---|
132
+ | `knowledge_index` | Load the knowledge wiki catalog at session start. Returns lightweight article metadata grouped by category. |
133
+ | `knowledge_search` | Search the knowledge wiki by topic. Supports keyword, semantic, or combined search modes. Returns snippets. |
134
+ | `knowledge_get` | Get full article content by ID. Use after search to read an article in detail. |
135
+ | `knowledge_context` | Get relevance-and-recency-ranked full articles for a task query. Best knowledge for your current context. |
136
+ | `knowledge_create` | Create a new knowledge article. File findings, document patterns, or record decisions. |
137
+
138
+ ### Knowledge Management Tools (orchestrator key)
139
+
140
+ | Tool | Description |
141
+ |---|---|
142
+ | `knowledge_publish` | Publish a draft article, making it visible to all agents. Required: `article_id`. |
143
+ | `knowledge_drafts` | List all draft (unpublished) knowledge articles. Optional: `limit`, `offset`. |
144
+ | `knowledge_lint` | Run a lint check on the knowledge wiki to identify stale or low-coverage articles. Optional: `project_id`, `stale_days`, `min_coverage`. |
145
+ | `knowledge_export` | Export all knowledge articles as a ZIP archive. Returns a curl command for direct download (ZIP binary cannot be returned as MCP content). Optional: `project_id`. |
146
+
128
147
  ### Discovery Tools
129
148
 
130
149
  | Tool | Description |
package/index.js CHANGED
@@ -116,10 +116,12 @@ function toContent(result) {
116
116
  }
117
117
 
118
118
  /**
119
- * Compact variant for list endpoints — strips bulky fields (acceptance_criteria,
120
- * description) from each story to stay within token limits. Callers can use
121
- * get_story for full details on individual stories.
119
+ * Compact variant for list endpoints — strips acceptance_criteria and
120
+ * description (use get_story for full details). Keeps all other fields.
121
+ * Enforces a max page size to prevent MCP response token overflow.
122
122
  */
123
+ const MAX_PAGE_SIZE = 20;
124
+
123
125
  function toContentCompact(result) {
124
126
  if (result && result.error === true) return toContent(result);
125
127
 
@@ -191,7 +193,7 @@ async function listStories({ project_id, agent_status, verified_status, epic_id,
191
193
  if (agent_status) params.set("agent_status", agent_status);
192
194
  if (verified_status) params.set("verified_status", verified_status);
193
195
  if (epic_id) params.set("epic_id", epic_id);
194
- params.set("limit", String(limit ?? 20));
196
+ params.set("limit", String(Math.min(limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE)));
195
197
  if (offset != null) params.set("offset", String(offset));
196
198
  if (include_token_totals) params.set("include_token_totals", "true");
197
199
 
@@ -201,7 +203,7 @@ async function listStories({ project_id, agent_status, verified_status, epic_id,
201
203
 
202
204
  async function listReadyStories({ project_id, limit }) {
203
205
  const params = new URLSearchParams({ project_id });
204
- params.set("limit", String(limit ?? 20));
206
+ params.set("limit", String(Math.min(limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE)));
205
207
 
206
208
  const result = await apiCall("GET", `/api/v1/stories/ready?${params}`);
207
209
  return toContentCompact(result);
@@ -401,6 +403,101 @@ async function setTokenBudget({ scope_type, scope_id, budget_millicents, alert_t
401
403
  return toContent(result);
402
404
  }
403
405
 
406
+ // --- Knowledge Wiki Tools (agent key) ---
407
+
408
+ async function knowledgeIndex({ project_id }) {
409
+ const path = project_id
410
+ ? `/api/v1/projects/${project_id}/knowledge/index`
411
+ : "/api/v1/knowledge/index";
412
+ const result = await apiCall("GET", path, null, process.env.LOOPCTL_AGENT_KEY);
413
+ return toContent(result);
414
+ }
415
+
416
+ async function knowledgeSearch({ q, project_id, category, tags, mode, limit }) {
417
+ const params = new URLSearchParams({ q });
418
+ if (project_id) params.set("project_id", project_id);
419
+ if (category) params.set("category", category);
420
+ if (tags) params.set("tags", tags);
421
+ if (mode) params.set("mode", mode);
422
+ if (limit != null) params.set("limit", String(limit));
423
+
424
+ const result = await apiCall("GET", `/api/v1/knowledge/search?${params}`, null, process.env.LOOPCTL_AGENT_KEY);
425
+ return toContent(result);
426
+ }
427
+
428
+ async function knowledgeGet({ article_id }) {
429
+ const result = await apiCall("GET", `/api/v1/articles/${article_id}`, null, process.env.LOOPCTL_AGENT_KEY);
430
+ return toContent(result);
431
+ }
432
+
433
+ async function knowledgeContext({ query, project_id, limit, recency_weight }) {
434
+ const params = new URLSearchParams({ query });
435
+ if (project_id) params.set("project_id", project_id);
436
+ if (limit != null) params.set("limit", String(limit));
437
+ if (recency_weight != null) params.set("recency_weight", String(recency_weight));
438
+
439
+ const result = await apiCall("GET", `/api/v1/knowledge/context?${params}`, null, process.env.LOOPCTL_AGENT_KEY);
440
+ return toContent(result);
441
+ }
442
+
443
+ async function knowledgeCreate({ title, body, category, tags, project_id }) {
444
+ const payload = { title, body };
445
+ if (category) payload.category = category;
446
+ if (tags) payload.tags = tags;
447
+ if (project_id) payload.project_id = project_id;
448
+
449
+ const result = await apiCall("POST", "/api/v1/articles", payload, process.env.LOOPCTL_AGENT_KEY);
450
+ return toContent(result);
451
+ }
452
+
453
+ // --- Knowledge Management Tools (orch key) ---
454
+
455
+ async function knowledgePublish({ article_id }) {
456
+ const result = await apiCall("POST", `/api/v1/articles/${article_id}/publish`, null, process.env.LOOPCTL_ORCH_KEY);
457
+ return toContent(result);
458
+ }
459
+
460
+ async function knowledgeDrafts({ limit, offset }) {
461
+ const params = new URLSearchParams();
462
+ if (limit != null) params.set("limit", String(limit));
463
+ if (offset != null) params.set("offset", String(offset));
464
+ const qs = params.toString();
465
+ const path = qs ? `/api/v1/knowledge/drafts?${qs}` : "/api/v1/knowledge/drafts";
466
+ const result = await apiCall("GET", path, null, process.env.LOOPCTL_ORCH_KEY);
467
+ return toContent(result);
468
+ }
469
+
470
+ async function knowledgeLint({ project_id, stale_days, min_coverage }) {
471
+ const params = new URLSearchParams();
472
+ if (stale_days != null) params.set("stale_days", String(stale_days));
473
+ if (min_coverage != null) params.set("min_coverage", String(min_coverage));
474
+ const qs = params.toString();
475
+ const basePath = project_id
476
+ ? `/api/v1/projects/${project_id}/knowledge/lint`
477
+ : "/api/v1/knowledge/lint";
478
+ const path = qs ? `${basePath}?${qs}` : basePath;
479
+ const result = await apiCall("GET", path, null, process.env.LOOPCTL_ORCH_KEY);
480
+ return toContent(result);
481
+ }
482
+
483
+ async function knowledgeExport({ project_id }) {
484
+ const basePath = project_id
485
+ ? `/api/v1/projects/${project_id}/knowledge/export`
486
+ : "/api/v1/knowledge/export";
487
+ const baseUrl = getBaseUrl();
488
+ const downloadCmd = `curl -H "Authorization: Bearer $LOOPCTL_ORCH_KEY" "${baseUrl}${basePath}" -o knowledge-export.zip`;
489
+ return {
490
+ content: [{
491
+ type: "text",
492
+ text: JSON.stringify({
493
+ message: "Knowledge export produces a ZIP file. Use the curl command below to download it directly.",
494
+ command: downloadCmd,
495
+ endpoint: `${baseUrl}${basePath}`,
496
+ }, null, 2),
497
+ }],
498
+ };
499
+ }
500
+
404
501
  // --- Discovery Tools ---
405
502
 
406
503
  async function listRoutes() {
@@ -490,7 +587,8 @@ const TOOLS = [
490
587
  description:
491
588
  "List stories for a project, optionally filtered by agent_status, verified_status, or epic_id. " +
492
589
  "Returns compact results (no acceptance_criteria/description) — use get_story for full details. " +
493
- "Defaults to 20 stories per page; use limit/offset to paginate.",
590
+ "Max 20 per page. Use offset to paginate (response includes total_count). " +
591
+ "Filter by epic_id or agent_status to reduce result size.",
494
592
  inputSchema: {
495
593
  type: "object",
496
594
  properties: {
@@ -531,7 +629,8 @@ const TOOLS = [
531
629
  name: "list_ready_stories",
532
630
  description:
533
631
  "List stories that are ready to be worked on (contracted, dependencies met). " +
534
- "Returns compact results — use get_story for full details. Defaults to 20 per page.",
632
+ "Returns compact results — use get_story for full details. " +
633
+ "Max 20 per page. Response includes total_count for pagination.",
535
634
  inputSchema: {
536
635
  type: "object",
537
636
  properties: {
@@ -946,6 +1045,217 @@ const TOOLS = [
946
1045
  },
947
1046
  },
948
1047
 
1048
+ // Knowledge Wiki Tools (agent key)
1049
+ {
1050
+ name: "knowledge_index",
1051
+ description:
1052
+ "Load the knowledge wiki catalog at session start. Returns lightweight article metadata " +
1053
+ "(titles, categories, tags) grouped by category. Use this to discover available knowledge before searching.",
1054
+ inputSchema: {
1055
+ type: "object",
1056
+ properties: {
1057
+ project_id: {
1058
+ type: "string",
1059
+ description: "Optional: scope the index to a specific project UUID.",
1060
+ },
1061
+ },
1062
+ required: [],
1063
+ },
1064
+ },
1065
+ {
1066
+ name: "knowledge_search",
1067
+ description:
1068
+ "Search the knowledge wiki by topic. Supports keyword, semantic, or combined search modes. " +
1069
+ "Returns snippets, not full bodies. Use after index to find specific articles.",
1070
+ inputSchema: {
1071
+ type: "object",
1072
+ properties: {
1073
+ q: {
1074
+ type: "string",
1075
+ description: "Search query string.",
1076
+ },
1077
+ project_id: {
1078
+ type: "string",
1079
+ description: "Optional: scope search to a specific project UUID.",
1080
+ },
1081
+ category: {
1082
+ type: "string",
1083
+ description: "Optional: filter results by category.",
1084
+ },
1085
+ tags: {
1086
+ type: "string",
1087
+ description: "Optional: comma-separated tags to filter by.",
1088
+ },
1089
+ mode: {
1090
+ type: "string",
1091
+ enum: ["keyword", "semantic", "combined"],
1092
+ description: "Optional: search mode (keyword, semantic, or combined).",
1093
+ },
1094
+ limit: {
1095
+ type: "integer",
1096
+ description: "Optional: maximum number of results to return.",
1097
+ },
1098
+ },
1099
+ required: ["q"],
1100
+ },
1101
+ },
1102
+ {
1103
+ name: "knowledge_get",
1104
+ description:
1105
+ "Get full article content by ID. Use after search to read an article in detail.",
1106
+ inputSchema: {
1107
+ type: "object",
1108
+ properties: {
1109
+ article_id: {
1110
+ type: "string",
1111
+ description: "The UUID of the article.",
1112
+ },
1113
+ },
1114
+ required: ["article_id"],
1115
+ },
1116
+ },
1117
+ {
1118
+ name: "knowledge_context",
1119
+ description:
1120
+ "Get relevance-and-recency-ranked full articles for a task query. Returns the best knowledge " +
1121
+ "for your current context with linked references. Use when starting a task that needs domain knowledge.",
1122
+ inputSchema: {
1123
+ type: "object",
1124
+ properties: {
1125
+ query: {
1126
+ type: "string",
1127
+ description: "The task or topic query to find relevant knowledge for.",
1128
+ },
1129
+ project_id: {
1130
+ type: "string",
1131
+ description: "Optional: scope context to a specific project UUID.",
1132
+ },
1133
+ limit: {
1134
+ type: "integer",
1135
+ description: "Optional: maximum number of articles to return.",
1136
+ },
1137
+ recency_weight: {
1138
+ type: "number",
1139
+ description: "Optional: weight for recency scoring (0.0-1.0).",
1140
+ minimum: 0,
1141
+ maximum: 1,
1142
+ },
1143
+ },
1144
+ required: ["query"],
1145
+ },
1146
+ },
1147
+ {
1148
+ name: "knowledge_create",
1149
+ description:
1150
+ "Create a new knowledge article. Use to file findings, document patterns, or record decisions " +
1151
+ "discovered during implementation.",
1152
+ inputSchema: {
1153
+ type: "object",
1154
+ properties: {
1155
+ title: {
1156
+ type: "string",
1157
+ description: "Article title.",
1158
+ },
1159
+ body: {
1160
+ type: "string",
1161
+ description: "Article body content (Markdown supported).",
1162
+ },
1163
+ category: {
1164
+ type: "string",
1165
+ description: "Optional: article category.",
1166
+ },
1167
+ tags: {
1168
+ type: "array",
1169
+ items: { type: "string" },
1170
+ description: "Optional: list of tags.",
1171
+ },
1172
+ project_id: {
1173
+ type: "string",
1174
+ description: "Optional: associate the article with a project UUID.",
1175
+ },
1176
+ },
1177
+ required: ["title", "body"],
1178
+ },
1179
+ },
1180
+
1181
+ // Knowledge Management Tools (orchestrator key)
1182
+ {
1183
+ name: "knowledge_publish",
1184
+ description:
1185
+ "Publish a draft knowledge article, making it visible to all agents. Requires orchestrator role.",
1186
+ inputSchema: {
1187
+ type: "object",
1188
+ properties: {
1189
+ article_id: {
1190
+ type: "string",
1191
+ description: "The UUID of the draft article to publish.",
1192
+ },
1193
+ },
1194
+ required: ["article_id"],
1195
+ },
1196
+ },
1197
+ {
1198
+ name: "knowledge_drafts",
1199
+ description:
1200
+ "List all draft (unpublished) knowledge articles. Requires orchestrator role. Use to review pending articles before publishing.",
1201
+ inputSchema: {
1202
+ type: "object",
1203
+ properties: {
1204
+ limit: {
1205
+ type: "integer",
1206
+ description: "Optional: maximum number of drafts to return.",
1207
+ },
1208
+ offset: {
1209
+ type: "integer",
1210
+ description: "Optional: pagination offset.",
1211
+ },
1212
+ },
1213
+ required: [],
1214
+ },
1215
+ },
1216
+ {
1217
+ name: "knowledge_lint",
1218
+ description:
1219
+ "Run a lint check on the knowledge wiki to identify stale, low-coverage, or broken articles. " +
1220
+ "Requires orchestrator role. Optionally scoped to a project.",
1221
+ inputSchema: {
1222
+ type: "object",
1223
+ properties: {
1224
+ project_id: {
1225
+ type: "string",
1226
+ description: "Optional: scope lint to a specific project UUID.",
1227
+ },
1228
+ stale_days: {
1229
+ type: "integer",
1230
+ description: "Optional: flag articles not updated in this many days as stale.",
1231
+ },
1232
+ min_coverage: {
1233
+ type: "number",
1234
+ description: "Optional: minimum required coverage score (0.0-1.0) to flag under-covered articles.",
1235
+ minimum: 0,
1236
+ maximum: 1,
1237
+ },
1238
+ },
1239
+ required: [],
1240
+ },
1241
+ },
1242
+ {
1243
+ name: "knowledge_export",
1244
+ description:
1245
+ "Export all knowledge articles as a ZIP archive. Because ZIP binary cannot be returned as MCP content, " +
1246
+ "this tool returns a curl command you can run directly to download the archive.",
1247
+ inputSchema: {
1248
+ type: "object",
1249
+ properties: {
1250
+ project_id: {
1251
+ type: "string",
1252
+ description: "Optional: scope export to a specific project UUID.",
1253
+ },
1254
+ },
1255
+ required: [],
1256
+ },
1257
+ },
1258
+
949
1259
  // Discovery Tools
950
1260
  {
951
1261
  name: "list_routes",
@@ -965,7 +1275,7 @@ const TOOLS = [
965
1275
  const server = new Server(
966
1276
  {
967
1277
  name: "loopctl",
968
- version: "1.1.1",
1278
+ version: "1.1.2",
969
1279
  },
970
1280
  {
971
1281
  capabilities: { tools: {} },
@@ -1056,6 +1366,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1056
1366
  case "set_token_budget":
1057
1367
  return await setTokenBudget(args);
1058
1368
 
1369
+ // Knowledge Wiki Tools
1370
+ case "knowledge_index":
1371
+ return await knowledgeIndex(args);
1372
+
1373
+ case "knowledge_search":
1374
+ return await knowledgeSearch(args);
1375
+
1376
+ case "knowledge_get":
1377
+ return await knowledgeGet(args);
1378
+
1379
+ case "knowledge_context":
1380
+ return await knowledgeContext(args);
1381
+
1382
+ case "knowledge_create":
1383
+ return await knowledgeCreate(args);
1384
+
1385
+ // Knowledge Management Tools
1386
+ case "knowledge_publish":
1387
+ return await knowledgePublish(args);
1388
+
1389
+ case "knowledge_drafts":
1390
+ return await knowledgeDrafts(args);
1391
+
1392
+ case "knowledge_lint":
1393
+ return await knowledgeLint(args);
1394
+
1395
+ case "knowledge_export":
1396
+ return await knowledgeExport(args);
1397
+
1059
1398
  // Discovery Tools
1060
1399
  case "list_routes":
1061
1400
  return await listRoutes();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",