loopctl-mcp-server 1.2.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.
- package/README.md +9 -4
- package/index.js +207 -13
- 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 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 (
|
|
69
|
+
## Tools (37)
|
|
69
70
|
|
|
70
71
|
### Project Tools
|
|
71
72
|
|
|
@@ -140,9 +141,13 @@ 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
|
-
| `
|
|
144
|
-
| `
|
|
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`. |
|
|
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. |
|
|
150
|
+
| `knowledge_ingestion_jobs` | List recent content ingestion jobs (last 7 days, max 50). |
|
|
146
151
|
|
|
147
152
|
### Discovery Tools
|
|
148
153
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
const path =
|
|
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`
|
|
@@ -480,6 +494,40 @@ async function knowledgeLint({ project_id, stale_days, min_coverage }) {
|
|
|
480
494
|
return toContent(result);
|
|
481
495
|
}
|
|
482
496
|
|
|
497
|
+
async function knowledgeIngest({ url, content, source_type, project_id }) {
|
|
498
|
+
const body = { source_type };
|
|
499
|
+
if (url) body.url = url;
|
|
500
|
+
if (content) body.content = content;
|
|
501
|
+
if (project_id) body.project_id = project_id;
|
|
502
|
+
const result = await apiCall("POST", "/api/v1/knowledge/ingest", body, process.env.LOOPCTL_ORCH_KEY);
|
|
503
|
+
return toContent(result);
|
|
504
|
+
}
|
|
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
|
+
|
|
526
|
+
async function knowledgeIngestionJobs() {
|
|
527
|
+
const result = await apiCall("GET", "/api/v1/knowledge/ingestion-jobs", null, process.env.LOOPCTL_ORCH_KEY);
|
|
528
|
+
return toContent(result);
|
|
529
|
+
}
|
|
530
|
+
|
|
483
531
|
async function knowledgeExport({ project_id }) {
|
|
484
532
|
const basePath = project_id
|
|
485
533
|
? `/api/v1/projects/${project_id}/knowledge/export`
|
|
@@ -1194,20 +1242,51 @@ const TOOLS = [
|
|
|
1194
1242
|
required: ["article_id"],
|
|
1195
1243
|
},
|
|
1196
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
|
+
},
|
|
1197
1266
|
{
|
|
1198
1267
|
name: "knowledge_drafts",
|
|
1199
1268
|
description:
|
|
1200
|
-
"List
|
|
1269
|
+
"List draft (unpublished) knowledge articles. Requires orchestrator role. " +
|
|
1270
|
+
"Returns paginated drafts with total_count in meta. Max 20 per page.",
|
|
1201
1271
|
inputSchema: {
|
|
1202
1272
|
type: "object",
|
|
1203
1273
|
properties: {
|
|
1204
1274
|
limit: {
|
|
1205
1275
|
type: "integer",
|
|
1206
|
-
description: "
|
|
1276
|
+
description: "Max drafts per page. Default 20, hard max 20.",
|
|
1277
|
+
default: 20,
|
|
1278
|
+
minimum: 1,
|
|
1279
|
+
maximum: 20,
|
|
1207
1280
|
},
|
|
1208
1281
|
offset: {
|
|
1209
1282
|
type: "integer",
|
|
1210
|
-
description: "
|
|
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.",
|
|
1211
1290
|
},
|
|
1212
1291
|
},
|
|
1213
1292
|
required: [],
|
|
@@ -1217,7 +1296,9 @@ const TOOLS = [
|
|
|
1217
1296
|
name: "knowledge_lint",
|
|
1218
1297
|
description:
|
|
1219
1298
|
"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."
|
|
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.",
|
|
1221
1302
|
inputSchema: {
|
|
1222
1303
|
type: "object",
|
|
1223
1304
|
properties: {
|
|
@@ -1230,10 +1311,18 @@ const TOOLS = [
|
|
|
1230
1311
|
description: "Optional: flag articles not updated in this many days as stale.",
|
|
1231
1312
|
},
|
|
1232
1313
|
min_coverage: {
|
|
1233
|
-
type: "
|
|
1234
|
-
description:
|
|
1235
|
-
|
|
1236
|
-
|
|
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,
|
|
1237
1326
|
},
|
|
1238
1327
|
},
|
|
1239
1328
|
required: [],
|
|
@@ -1256,6 +1345,98 @@ const TOOLS = [
|
|
|
1256
1345
|
},
|
|
1257
1346
|
},
|
|
1258
1347
|
|
|
1348
|
+
// Knowledge Ingestion Tools
|
|
1349
|
+
{
|
|
1350
|
+
name: "knowledge_ingest",
|
|
1351
|
+
description:
|
|
1352
|
+
"Submit a URL or raw content for knowledge extraction. " +
|
|
1353
|
+
"Enqueues an Oban job that fetches the content (if URL), extracts knowledge articles via LLM, " +
|
|
1354
|
+
"and inserts them as draft articles. Requires orchestrator role.",
|
|
1355
|
+
inputSchema: {
|
|
1356
|
+
type: "object",
|
|
1357
|
+
properties: {
|
|
1358
|
+
url: {
|
|
1359
|
+
type: "string",
|
|
1360
|
+
description: "URL to fetch content from (exactly one of url or content required).",
|
|
1361
|
+
},
|
|
1362
|
+
content: {
|
|
1363
|
+
type: "string",
|
|
1364
|
+
description: "Raw content to extract knowledge from (exactly one of url or content required).",
|
|
1365
|
+
},
|
|
1366
|
+
source_type: {
|
|
1367
|
+
type: "string",
|
|
1368
|
+
description: "Source type (e.g., newsletter, skill, web_article, ingestion). Required.",
|
|
1369
|
+
},
|
|
1370
|
+
project_id: {
|
|
1371
|
+
type: "string",
|
|
1372
|
+
description: "Optional: scope extracted articles to a specific project UUID.",
|
|
1373
|
+
},
|
|
1374
|
+
},
|
|
1375
|
+
required: ["source_type"],
|
|
1376
|
+
},
|
|
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
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
name: "knowledge_ingestion_jobs",
|
|
1430
|
+
description:
|
|
1431
|
+
"List recent content ingestion jobs for the current tenant. " +
|
|
1432
|
+
"Returns jobs from the last 7 days, max 50 results. Requires orchestrator role.",
|
|
1433
|
+
inputSchema: {
|
|
1434
|
+
type: "object",
|
|
1435
|
+
properties: {},
|
|
1436
|
+
required: [],
|
|
1437
|
+
},
|
|
1438
|
+
},
|
|
1439
|
+
|
|
1259
1440
|
// Discovery Tools
|
|
1260
1441
|
{
|
|
1261
1442
|
name: "list_routes",
|
|
@@ -1386,6 +1567,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1386
1567
|
case "knowledge_publish":
|
|
1387
1568
|
return await knowledgePublish(args);
|
|
1388
1569
|
|
|
1570
|
+
case "knowledge_bulk_publish":
|
|
1571
|
+
return await knowledgeBulkPublish(args);
|
|
1572
|
+
|
|
1389
1573
|
case "knowledge_drafts":
|
|
1390
1574
|
return await knowledgeDrafts(args);
|
|
1391
1575
|
|
|
@@ -1395,6 +1579,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1395
1579
|
case "knowledge_export":
|
|
1396
1580
|
return await knowledgeExport(args);
|
|
1397
1581
|
|
|
1582
|
+
// Knowledge Ingestion Tools
|
|
1583
|
+
case "knowledge_ingest":
|
|
1584
|
+
return await knowledgeIngest(args);
|
|
1585
|
+
|
|
1586
|
+
case "knowledge_ingest_batch":
|
|
1587
|
+
return await knowledgeIngestBatch(args);
|
|
1588
|
+
|
|
1589
|
+
case "knowledge_ingestion_jobs":
|
|
1590
|
+
return await knowledgeIngestionJobs();
|
|
1591
|
+
|
|
1398
1592
|
// Discovery Tools
|
|
1399
1593
|
case "list_routes":
|
|
1400
1594
|
return await listRoutes();
|