loopctl-mcp-server 1.1.2 → 1.3.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 +23 -2
  2. package/index.js +398 -0
  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 35 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 (35)
69
69
 
70
70
  ### Project Tools
71
71
 
@@ -125,6 +125,27 @@ 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
+ | `knowledge_ingest` | Submit a URL or raw content for knowledge extraction. Enqueues an Oban job. Required: `source_type`. One of: `url` or `content`. Optional: `project_id`. |
147
+ | `knowledge_ingestion_jobs` | List recent content ingestion jobs (last 7 days, max 50). |
148
+
128
149
  ### Discovery Tools
129
150
 
130
151
  | Tool | Description |
package/index.js CHANGED
@@ -403,6 +403,115 @@ async function setTokenBudget({ scope_type, scope_id, budget_millicents, alert_t
403
403
  return toContent(result);
404
404
  }
405
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 knowledgeIngest({ url, content, source_type, project_id }) {
484
+ const body = { source_type };
485
+ if (url) body.url = url;
486
+ if (content) body.content = content;
487
+ if (project_id) body.project_id = project_id;
488
+ const result = await apiCall("POST", "/api/v1/knowledge/ingest", body, process.env.LOOPCTL_ORCH_KEY);
489
+ return toContent(result);
490
+ }
491
+
492
+ async function knowledgeIngestionJobs() {
493
+ const result = await apiCall("GET", "/api/v1/knowledge/ingestion-jobs", null, process.env.LOOPCTL_ORCH_KEY);
494
+ return toContent(result);
495
+ }
496
+
497
+ async function knowledgeExport({ project_id }) {
498
+ const basePath = project_id
499
+ ? `/api/v1/projects/${project_id}/knowledge/export`
500
+ : "/api/v1/knowledge/export";
501
+ const baseUrl = getBaseUrl();
502
+ const downloadCmd = `curl -H "Authorization: Bearer $LOOPCTL_ORCH_KEY" "${baseUrl}${basePath}" -o knowledge-export.zip`;
503
+ return {
504
+ content: [{
505
+ type: "text",
506
+ text: JSON.stringify({
507
+ message: "Knowledge export produces a ZIP file. Use the curl command below to download it directly.",
508
+ command: downloadCmd,
509
+ endpoint: `${baseUrl}${basePath}`,
510
+ }, null, 2),
511
+ }],
512
+ };
513
+ }
514
+
406
515
  // --- Discovery Tools ---
407
516
 
408
517
  async function listRoutes() {
@@ -950,6 +1059,259 @@ const TOOLS = [
950
1059
  },
951
1060
  },
952
1061
 
1062
+ // Knowledge Wiki Tools (agent key)
1063
+ {
1064
+ name: "knowledge_index",
1065
+ description:
1066
+ "Load the knowledge wiki catalog at session start. Returns lightweight article metadata " +
1067
+ "(titles, categories, tags) grouped by category. Use this to discover available knowledge before searching.",
1068
+ inputSchema: {
1069
+ type: "object",
1070
+ properties: {
1071
+ project_id: {
1072
+ type: "string",
1073
+ description: "Optional: scope the index to a specific project UUID.",
1074
+ },
1075
+ },
1076
+ required: [],
1077
+ },
1078
+ },
1079
+ {
1080
+ name: "knowledge_search",
1081
+ description:
1082
+ "Search the knowledge wiki by topic. Supports keyword, semantic, or combined search modes. " +
1083
+ "Returns snippets, not full bodies. Use after index to find specific articles.",
1084
+ inputSchema: {
1085
+ type: "object",
1086
+ properties: {
1087
+ q: {
1088
+ type: "string",
1089
+ description: "Search query string.",
1090
+ },
1091
+ project_id: {
1092
+ type: "string",
1093
+ description: "Optional: scope search to a specific project UUID.",
1094
+ },
1095
+ category: {
1096
+ type: "string",
1097
+ description: "Optional: filter results by category.",
1098
+ },
1099
+ tags: {
1100
+ type: "string",
1101
+ description: "Optional: comma-separated tags to filter by.",
1102
+ },
1103
+ mode: {
1104
+ type: "string",
1105
+ enum: ["keyword", "semantic", "combined"],
1106
+ description: "Optional: search mode (keyword, semantic, or combined).",
1107
+ },
1108
+ limit: {
1109
+ type: "integer",
1110
+ description: "Optional: maximum number of results to return.",
1111
+ },
1112
+ },
1113
+ required: ["q"],
1114
+ },
1115
+ },
1116
+ {
1117
+ name: "knowledge_get",
1118
+ description:
1119
+ "Get full article content by ID. Use after search to read an article in detail.",
1120
+ inputSchema: {
1121
+ type: "object",
1122
+ properties: {
1123
+ article_id: {
1124
+ type: "string",
1125
+ description: "The UUID of the article.",
1126
+ },
1127
+ },
1128
+ required: ["article_id"],
1129
+ },
1130
+ },
1131
+ {
1132
+ name: "knowledge_context",
1133
+ description:
1134
+ "Get relevance-and-recency-ranked full articles for a task query. Returns the best knowledge " +
1135
+ "for your current context with linked references. Use when starting a task that needs domain knowledge.",
1136
+ inputSchema: {
1137
+ type: "object",
1138
+ properties: {
1139
+ query: {
1140
+ type: "string",
1141
+ description: "The task or topic query to find relevant knowledge for.",
1142
+ },
1143
+ project_id: {
1144
+ type: "string",
1145
+ description: "Optional: scope context to a specific project UUID.",
1146
+ },
1147
+ limit: {
1148
+ type: "integer",
1149
+ description: "Optional: maximum number of articles to return.",
1150
+ },
1151
+ recency_weight: {
1152
+ type: "number",
1153
+ description: "Optional: weight for recency scoring (0.0-1.0).",
1154
+ minimum: 0,
1155
+ maximum: 1,
1156
+ },
1157
+ },
1158
+ required: ["query"],
1159
+ },
1160
+ },
1161
+ {
1162
+ name: "knowledge_create",
1163
+ description:
1164
+ "Create a new knowledge article. Use to file findings, document patterns, or record decisions " +
1165
+ "discovered during implementation.",
1166
+ inputSchema: {
1167
+ type: "object",
1168
+ properties: {
1169
+ title: {
1170
+ type: "string",
1171
+ description: "Article title.",
1172
+ },
1173
+ body: {
1174
+ type: "string",
1175
+ description: "Article body content (Markdown supported).",
1176
+ },
1177
+ category: {
1178
+ type: "string",
1179
+ description: "Optional: article category.",
1180
+ },
1181
+ tags: {
1182
+ type: "array",
1183
+ items: { type: "string" },
1184
+ description: "Optional: list of tags.",
1185
+ },
1186
+ project_id: {
1187
+ type: "string",
1188
+ description: "Optional: associate the article with a project UUID.",
1189
+ },
1190
+ },
1191
+ required: ["title", "body"],
1192
+ },
1193
+ },
1194
+
1195
+ // Knowledge Management Tools (orchestrator key)
1196
+ {
1197
+ name: "knowledge_publish",
1198
+ description:
1199
+ "Publish a draft knowledge article, making it visible to all agents. Requires orchestrator role.",
1200
+ inputSchema: {
1201
+ type: "object",
1202
+ properties: {
1203
+ article_id: {
1204
+ type: "string",
1205
+ description: "The UUID of the draft article to publish.",
1206
+ },
1207
+ },
1208
+ required: ["article_id"],
1209
+ },
1210
+ },
1211
+ {
1212
+ name: "knowledge_drafts",
1213
+ description:
1214
+ "List all draft (unpublished) knowledge articles. Requires orchestrator role. Use to review pending articles before publishing.",
1215
+ inputSchema: {
1216
+ type: "object",
1217
+ properties: {
1218
+ limit: {
1219
+ type: "integer",
1220
+ description: "Optional: maximum number of drafts to return.",
1221
+ },
1222
+ offset: {
1223
+ type: "integer",
1224
+ description: "Optional: pagination offset.",
1225
+ },
1226
+ },
1227
+ required: [],
1228
+ },
1229
+ },
1230
+ {
1231
+ name: "knowledge_lint",
1232
+ description:
1233
+ "Run a lint check on the knowledge wiki to identify stale, low-coverage, or broken articles. " +
1234
+ "Requires orchestrator role. Optionally scoped to a project.",
1235
+ inputSchema: {
1236
+ type: "object",
1237
+ properties: {
1238
+ project_id: {
1239
+ type: "string",
1240
+ description: "Optional: scope lint to a specific project UUID.",
1241
+ },
1242
+ stale_days: {
1243
+ type: "integer",
1244
+ description: "Optional: flag articles not updated in this many days as stale.",
1245
+ },
1246
+ min_coverage: {
1247
+ type: "number",
1248
+ description: "Optional: minimum required coverage score (0.0-1.0) to flag under-covered articles.",
1249
+ minimum: 0,
1250
+ maximum: 1,
1251
+ },
1252
+ },
1253
+ required: [],
1254
+ },
1255
+ },
1256
+ {
1257
+ name: "knowledge_export",
1258
+ description:
1259
+ "Export all knowledge articles as a ZIP archive. Because ZIP binary cannot be returned as MCP content, " +
1260
+ "this tool returns a curl command you can run directly to download the archive.",
1261
+ inputSchema: {
1262
+ type: "object",
1263
+ properties: {
1264
+ project_id: {
1265
+ type: "string",
1266
+ description: "Optional: scope export to a specific project UUID.",
1267
+ },
1268
+ },
1269
+ required: [],
1270
+ },
1271
+ },
1272
+
1273
+ // Knowledge Ingestion Tools
1274
+ {
1275
+ name: "knowledge_ingest",
1276
+ description:
1277
+ "Submit a URL or raw content for knowledge extraction. " +
1278
+ "Enqueues an Oban job that fetches the content (if URL), extracts knowledge articles via LLM, " +
1279
+ "and inserts them as draft articles. Requires orchestrator role.",
1280
+ inputSchema: {
1281
+ type: "object",
1282
+ properties: {
1283
+ url: {
1284
+ type: "string",
1285
+ description: "URL to fetch content from (exactly one of url or content required).",
1286
+ },
1287
+ content: {
1288
+ type: "string",
1289
+ description: "Raw content to extract knowledge from (exactly one of url or content required).",
1290
+ },
1291
+ source_type: {
1292
+ type: "string",
1293
+ description: "Source type (e.g., newsletter, skill, web_article, ingestion). Required.",
1294
+ },
1295
+ project_id: {
1296
+ type: "string",
1297
+ description: "Optional: scope extracted articles to a specific project UUID.",
1298
+ },
1299
+ },
1300
+ required: ["source_type"],
1301
+ },
1302
+ },
1303
+ {
1304
+ name: "knowledge_ingestion_jobs",
1305
+ description:
1306
+ "List recent content ingestion jobs for the current tenant. " +
1307
+ "Returns jobs from the last 7 days, max 50 results. Requires orchestrator role.",
1308
+ inputSchema: {
1309
+ type: "object",
1310
+ properties: {},
1311
+ required: [],
1312
+ },
1313
+ },
1314
+
953
1315
  // Discovery Tools
954
1316
  {
955
1317
  name: "list_routes",
@@ -1060,6 +1422,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1060
1422
  case "set_token_budget":
1061
1423
  return await setTokenBudget(args);
1062
1424
 
1425
+ // Knowledge Wiki Tools
1426
+ case "knowledge_index":
1427
+ return await knowledgeIndex(args);
1428
+
1429
+ case "knowledge_search":
1430
+ return await knowledgeSearch(args);
1431
+
1432
+ case "knowledge_get":
1433
+ return await knowledgeGet(args);
1434
+
1435
+ case "knowledge_context":
1436
+ return await knowledgeContext(args);
1437
+
1438
+ case "knowledge_create":
1439
+ return await knowledgeCreate(args);
1440
+
1441
+ // Knowledge Management Tools
1442
+ case "knowledge_publish":
1443
+ return await knowledgePublish(args);
1444
+
1445
+ case "knowledge_drafts":
1446
+ return await knowledgeDrafts(args);
1447
+
1448
+ case "knowledge_lint":
1449
+ return await knowledgeLint(args);
1450
+
1451
+ case "knowledge_export":
1452
+ return await knowledgeExport(args);
1453
+
1454
+ // Knowledge Ingestion Tools
1455
+ case "knowledge_ingest":
1456
+ return await knowledgeIngest(args);
1457
+
1458
+ case "knowledge_ingestion_jobs":
1459
+ return await knowledgeIngestionJobs();
1460
+
1063
1461
  // Discovery Tools
1064
1462
  case "list_routes":
1065
1463
  return await listRoutes();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",