loopctl-mcp-server 1.3.0 → 1.4.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 +7 -4
  2. package/index.js +144 -13
  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 35 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 37 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
 
@@ -62,10 +62,11 @@ Or if installed locally:
62
62
  | `LOOPCTL_API_KEY` | Global API key override (if set, always used) | -- |
63
63
  | `LOOPCTL_ORCH_KEY` | Orchestrator role API key (verify, reject, review, import) | -- |
64
64
  | `LOOPCTL_AGENT_KEY` | Agent role API key (contract, claim, start, request-review) | -- |
65
+ | `LOOPCTL_USER_KEY` | User role API key. Required ONLY for destructive admin tools like `knowledge_bulk_publish`. Leave unset if you don't use those tools. | -- |
65
66
 
66
67
  Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_KEY`.
67
68
 
68
- ## Tools (35)
69
+ ## Tools (37)
69
70
 
70
71
  ### Project Tools
71
72
 
@@ -140,10 +141,12 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
140
141
  | Tool | Description |
141
142
  |---|---|
142
143
  | `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`. |
144
+ | `knowledge_bulk_publish` | **Requires `LOOPCTL_USER_KEY`.** Atomically publish up to 100 drafts in a single call. Required: `article_ids` (array). |
145
+ | `knowledge_drafts` | List draft (unpublished) knowledge articles with pagination. Optional: `limit` (default 20, max 20), `offset` (default 0), `project_id`. Returns `meta.total_count`. |
146
+ | `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`. |
145
147
  | `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
148
  | `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`. |
149
+ | `knowledge_ingest_batch` | Submit up to 50 ingestion items in a single request. Each item has the same shape as `knowledge_ingest`. Returns per-item results. Required: `items`. Optional: batch-level `project_id` default. |
147
150
  | `knowledge_ingestion_jobs` | List recent content ingestion jobs (last 7 days, max 50). |
148
151
 
149
152
  ### Discovery Tools
package/index.js CHANGED
@@ -457,20 +457,34 @@ async function knowledgePublish({ article_id }) {
457
457
  return toContent(result);
458
458
  }
459
459
 
460
- async function knowledgeDrafts({ limit, offset }) {
460
+ async function knowledgeBulkPublish({ article_ids }) {
461
+ const result = await apiCall(
462
+ "POST",
463
+ "/api/v1/knowledge/bulk-publish",
464
+ { article_ids },
465
+ process.env.LOOPCTL_USER_KEY
466
+ );
467
+ return toContent(result);
468
+ }
469
+
470
+ async function knowledgeDrafts({ limit, offset, project_id }) {
461
471
  const params = new URLSearchParams();
462
- if (limit != null) params.set("limit", String(limit));
472
+ params.set(
473
+ "limit",
474
+ String(Math.min(limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE))
475
+ );
463
476
  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";
477
+ if (project_id) params.set("project_id", project_id);
478
+ const path = `/api/v1/knowledge/drafts?${params.toString()}`;
466
479
  const result = await apiCall("GET", path, null, process.env.LOOPCTL_ORCH_KEY);
467
480
  return toContent(result);
468
481
  }
469
482
 
470
- async function knowledgeLint({ project_id, stale_days, min_coverage }) {
483
+ async function knowledgeLint({ project_id, stale_days, min_coverage, max_per_category }) {
471
484
  const params = new URLSearchParams();
472
485
  if (stale_days != null) params.set("stale_days", String(stale_days));
473
486
  if (min_coverage != null) params.set("min_coverage", String(min_coverage));
487
+ if (max_per_category != null) params.set("max_per_category", String(max_per_category));
474
488
  const qs = params.toString();
475
489
  const basePath = project_id
476
490
  ? `/api/v1/projects/${project_id}/knowledge/lint`
@@ -489,6 +503,26 @@ async function knowledgeIngest({ url, content, source_type, project_id }) {
489
503
  return toContent(result);
490
504
  }
491
505
 
506
+ async function knowledgeIngestBatch({ items, project_id }) {
507
+ // If a batch-level project_id is supplied, apply it as a default to every
508
+ // item that doesn't already set its own.
509
+ const resolvedItems = Array.isArray(items)
510
+ ? items.map((item) =>
511
+ project_id && item && item.project_id == null
512
+ ? { ...item, project_id }
513
+ : item
514
+ )
515
+ : items;
516
+
517
+ const result = await apiCall(
518
+ "POST",
519
+ "/api/v1/knowledge/ingest/batch",
520
+ { items: resolvedItems },
521
+ process.env.LOOPCTL_ORCH_KEY
522
+ );
523
+ return toContent(result);
524
+ }
525
+
492
526
  async function knowledgeIngestionJobs() {
493
527
  const result = await apiCall("GET", "/api/v1/knowledge/ingestion-jobs", null, process.env.LOOPCTL_ORCH_KEY);
494
528
  return toContent(result);
@@ -1208,20 +1242,51 @@ const TOOLS = [
1208
1242
  required: ["article_id"],
1209
1243
  },
1210
1244
  },
1245
+ {
1246
+ name: "knowledge_bulk_publish",
1247
+ description:
1248
+ "Atomically publish up to 100 draft articles in a single call. " +
1249
+ "REQUIRES LOOPCTL_USER_KEY to be set in the MCP server env (user role — " +
1250
+ "orchestrator role is NOT sufficient for this destructive operation). " +
1251
+ "All articles must be drafts belonging to the tenant; if any fail validation, " +
1252
+ "the entire operation rolls back.",
1253
+ inputSchema: {
1254
+ type: "object",
1255
+ properties: {
1256
+ article_ids: {
1257
+ type: "array",
1258
+ items: { type: "string" },
1259
+ description: "List of draft article UUIDs to publish (max 100).",
1260
+ maxItems: 100,
1261
+ },
1262
+ },
1263
+ required: ["article_ids"],
1264
+ },
1265
+ },
1211
1266
  {
1212
1267
  name: "knowledge_drafts",
1213
1268
  description:
1214
- "List all draft (unpublished) knowledge articles. Requires orchestrator role. Use to review pending articles before publishing.",
1269
+ "List draft (unpublished) knowledge articles. Requires orchestrator role. " +
1270
+ "Returns paginated drafts with total_count in meta. Max 20 per page.",
1215
1271
  inputSchema: {
1216
1272
  type: "object",
1217
1273
  properties: {
1218
1274
  limit: {
1219
1275
  type: "integer",
1220
- description: "Optional: maximum number of drafts to return.",
1276
+ description: "Max drafts per page. Default 20, hard max 20.",
1277
+ default: 20,
1278
+ minimum: 1,
1279
+ maximum: 20,
1221
1280
  },
1222
1281
  offset: {
1223
1282
  type: "integer",
1224
- description: "Optional: pagination offset.",
1283
+ description: "Pagination offset. Default 0.",
1284
+ default: 0,
1285
+ minimum: 0,
1286
+ },
1287
+ project_id: {
1288
+ type: "string",
1289
+ description: "Optional: filter drafts to a specific project UUID.",
1225
1290
  },
1226
1291
  },
1227
1292
  required: [],
@@ -1231,7 +1296,9 @@ const TOOLS = [
1231
1296
  name: "knowledge_lint",
1232
1297
  description:
1233
1298
  "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.",
1299
+ "Requires orchestrator role. Optionally scoped to a project. " +
1300
+ "Each issue category is capped at max_per_category (default 50) with true totals " +
1301
+ "exposed in summary.total_per_category and per-category truncated flags.",
1235
1302
  inputSchema: {
1236
1303
  type: "object",
1237
1304
  properties: {
@@ -1244,10 +1311,18 @@ const TOOLS = [
1244
1311
  description: "Optional: flag articles not updated in this many days as stale.",
1245
1312
  },
1246
1313
  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,
1314
+ type: "integer",
1315
+ description:
1316
+ "Optional: minimum published articles per category below which a coverage gap is reported (default 3).",
1317
+ minimum: 1,
1318
+ },
1319
+ max_per_category: {
1320
+ type: "integer",
1321
+ description:
1322
+ "Max items per category to return. Default 50, max 500. True totals are still reported in summary.total_per_category.",
1323
+ default: 50,
1324
+ minimum: 1,
1325
+ maximum: 500,
1251
1326
  },
1252
1327
  },
1253
1328
  required: [],
@@ -1300,6 +1375,56 @@ const TOOLS = [
1300
1375
  required: ["source_type"],
1301
1376
  },
1302
1377
  },
1378
+ {
1379
+ name: "knowledge_ingest_batch",
1380
+ description:
1381
+ "Submit up to 50 ingestion items in a single request. Each item follows the same " +
1382
+ "shape as knowledge_ingest (url OR content, source_type required). Returns a " +
1383
+ "per-item result array — individual failures do not abort the batch. " +
1384
+ "Requires orchestrator role.",
1385
+ inputSchema: {
1386
+ type: "object",
1387
+ properties: {
1388
+ items: {
1389
+ type: "array",
1390
+ description: "Array of ingestion items (1-50). Each item must include source_type and exactly one of url or content.",
1391
+ minItems: 1,
1392
+ maxItems: 50,
1393
+ items: {
1394
+ type: "object",
1395
+ properties: {
1396
+ url: {
1397
+ type: "string",
1398
+ description: "URL to fetch content from (exactly one of url or content required).",
1399
+ },
1400
+ content: {
1401
+ type: "string",
1402
+ description: "Raw content to extract from (exactly one of url or content required).",
1403
+ },
1404
+ source_type: {
1405
+ type: "string",
1406
+ description: "Source type (e.g., newsletter, skill, web_article, ingestion). Required.",
1407
+ },
1408
+ project_id: {
1409
+ type: "string",
1410
+ description: "Optional: scope the item to a specific project UUID.",
1411
+ },
1412
+ metadata: {
1413
+ type: "object",
1414
+ description: "Optional metadata map.",
1415
+ },
1416
+ },
1417
+ required: ["source_type"],
1418
+ },
1419
+ },
1420
+ project_id: {
1421
+ type: "string",
1422
+ description: "Optional batch-level default project UUID applied to items that don't specify their own.",
1423
+ },
1424
+ },
1425
+ required: ["items"],
1426
+ },
1427
+ },
1303
1428
  {
1304
1429
  name: "knowledge_ingestion_jobs",
1305
1430
  description:
@@ -1442,6 +1567,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1442
1567
  case "knowledge_publish":
1443
1568
  return await knowledgePublish(args);
1444
1569
 
1570
+ case "knowledge_bulk_publish":
1571
+ return await knowledgeBulkPublish(args);
1572
+
1445
1573
  case "knowledge_drafts":
1446
1574
  return await knowledgeDrafts(args);
1447
1575
 
@@ -1455,6 +1583,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1455
1583
  case "knowledge_ingest":
1456
1584
  return await knowledgeIngest(args);
1457
1585
 
1586
+ case "knowledge_ingest_batch":
1587
+ return await knowledgeIngestBatch(args);
1588
+
1458
1589
  case "knowledge_ingestion_jobs":
1459
1590
  return await knowledgeIngestionJobs();
1460
1591
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",