loopctl-mcp-server 2.30.1 → 2.31.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 +10 -2
- package/index.js +166 -38
- package/lib/http-helpers.js +106 -0
- package/package.json +4 -2
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 72 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 (72)
|
|
70
70
|
|
|
71
71
|
### Project Tools
|
|
72
72
|
|
|
@@ -180,6 +180,14 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
|
|
|
180
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. |
|
|
181
181
|
| `knowledge_ingestion_jobs` | List recent content ingestion jobs (last 7 days, max 50). |
|
|
182
182
|
|
|
183
|
+
### Per-tenant BYO LLM config + usage (Epic 28, #179)
|
|
184
|
+
|
|
185
|
+
| Tool | Description |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `llm_config` | Get the tenant's BYO Anthropic LLM config: per-operation models, `has_api_key`, and a masked last-4 hint. Never returns the key. Requires **user** key. |
|
|
188
|
+
| `set_llm_config` | Set/rotate the tenant's OWN Anthropic API key (stored encrypted, never returned) + the three per-operation models (`extraction_model`/`classification_model`/`merge_model`). Any subset; omitting `api_key` leaves it untouched. Requires **user** key. |
|
|
189
|
+
| `knowledge_llm_usage` | Per-tenant LLM token-usage summary, grouped by operation + model + source_type + day over an optional `from`/`to` range (defaults to a 90-day lookback; effective window echoed in `meta.from`/`meta.to`), with `limit`/`offset` pagination. Record-only (no budget enforcement). Requires orchestrator key. |
|
|
190
|
+
|
|
183
191
|
### Knowledge Analytics Tools (orchestrator key)
|
|
184
192
|
|
|
185
193
|
| Tool | Description |
|
package/index.js
CHANGED
|
@@ -13,6 +13,12 @@ import {
|
|
|
13
13
|
import { readFileSync } from "node:fs";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
projectsPath,
|
|
18
|
+
ingestionJobsPath,
|
|
19
|
+
llmUsagePath,
|
|
20
|
+
parseJsonResponseBody,
|
|
21
|
+
} from "./lib/http-helpers.js";
|
|
16
22
|
|
|
17
23
|
// Single source of truth for the server version: the package.json this file
|
|
18
24
|
// ships with (npm always includes package.json in the published tarball).
|
|
@@ -52,12 +58,22 @@ function resolveKey(keyOverride) {
|
|
|
52
58
|
);
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
async function apiCall(method, path, body, keyOverride) {
|
|
61
|
+
async function apiCall(method, path, body, keyOverride, { exactKey = false } = {}) {
|
|
56
62
|
const url = `${getBaseUrl()}${path}`;
|
|
57
|
-
|
|
63
|
+
// Secret-managing tools pass exactKey:true so the request uses the EXACT
|
|
64
|
+
// role-pinned key (LOOPCTL_USER_KEY) and does NOT fall back to the global
|
|
65
|
+
// LOOPCTL_API_KEY override — a secret op must never silently run under a
|
|
66
|
+
// non-user global key (review #12).
|
|
67
|
+
const key = exactKey ? keyOverride : resolveKey(keyOverride);
|
|
58
68
|
|
|
59
69
|
if (!key) {
|
|
60
|
-
return {
|
|
70
|
+
return {
|
|
71
|
+
error: true,
|
|
72
|
+
status: 0,
|
|
73
|
+
body: exactKey
|
|
74
|
+
? "No user-role API key configured. Set LOOPCTL_USER_KEY to a user-role key to manage LLM configuration."
|
|
75
|
+
: "No API key configured. Set LOOPCTL_API_KEY, LOOPCTL_ORCH_KEY, or LOOPCTL_AGENT_KEY.",
|
|
76
|
+
};
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
const headers = {
|
|
@@ -109,26 +125,15 @@ async function apiCall(method, path, body, keyOverride) {
|
|
|
109
125
|
if (contentType.includes("application/json")) {
|
|
110
126
|
// A JSON content-type is no guarantee of a well-formed body: a transient Fly
|
|
111
127
|
// edge 502/503 or a truncated/empty response can arrive with the JSON header.
|
|
112
|
-
// Read the raw text and parse defensively
|
|
113
|
-
//
|
|
128
|
+
// Read the raw text and parse defensively (shared with the test suite via
|
|
129
|
+
// lib/http-helpers.js) so a malformed body becomes a structured MCP error
|
|
130
|
+
// instead of an unhandled throw from response.json().
|
|
114
131
|
const raw = await response.text();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
};
|
|
132
|
+
const outcome = parseJsonResponseBody(raw, response.status);
|
|
133
|
+
if (outcome.error) {
|
|
134
|
+
return outcome;
|
|
131
135
|
}
|
|
136
|
+
responseBody = outcome.parsed;
|
|
132
137
|
} else {
|
|
133
138
|
const text = await response.text();
|
|
134
139
|
try {
|
|
@@ -210,12 +215,10 @@ async function getTenant() {
|
|
|
210
215
|
return toContent(result);
|
|
211
216
|
}
|
|
212
217
|
|
|
213
|
-
async function listProjects(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const query = params.toString() ? `?${params}` : "";
|
|
218
|
-
const result = await apiCall("GET", `/api/v1/projects${query}`);
|
|
218
|
+
async function listProjects(args = {}) {
|
|
219
|
+
// Query-string building lives in lib/http-helpers.js so the test suite exercises
|
|
220
|
+
// the same page/page_size logic the server ships (#247, mcp-01).
|
|
221
|
+
const result = await apiCall("GET", projectsPath(args));
|
|
219
222
|
return toContent(result);
|
|
220
223
|
}
|
|
221
224
|
|
|
@@ -1152,15 +1155,56 @@ async function knowledgeIngestBatch({ items, project_id, publish }) {
|
|
|
1152
1155
|
return toContent(result);
|
|
1153
1156
|
}
|
|
1154
1157
|
|
|
1155
|
-
async function knowledgeIngestionJobs(
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (offset != null) params.set("offset", String(offset));
|
|
1159
|
-
if (since_days != null) params.set("since_days", String(since_days));
|
|
1160
|
-
const query = params.toString() ? `?${params}` : "";
|
|
1158
|
+
async function knowledgeIngestionJobs(args = {}) {
|
|
1159
|
+
// Query-string building lives in lib/http-helpers.js so the test suite exercises
|
|
1160
|
+
// the same limit/offset/since_days logic the server ships (#248, mcp-02).
|
|
1161
1161
|
const result = await apiCall(
|
|
1162
1162
|
"GET",
|
|
1163
|
-
|
|
1163
|
+
ingestionJobsPath(args),
|
|
1164
|
+
null,
|
|
1165
|
+
process.env.LOOPCTL_ORCH_KEY,
|
|
1166
|
+
);
|
|
1167
|
+
return toContent(result);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// --- Per-tenant BYO LLM config + usage (Epic 28 residual, #179) ---
|
|
1171
|
+
|
|
1172
|
+
async function llmConfig() {
|
|
1173
|
+
// Reading/writing the tenant LLM config touches a stored secret → EXACT user
|
|
1174
|
+
// key only (bypass the global LOOPCTL_API_KEY override; fail fast if unset).
|
|
1175
|
+
const result = await apiCall(
|
|
1176
|
+
"GET",
|
|
1177
|
+
"/api/v1/tenants/me/llm-config",
|
|
1178
|
+
null,
|
|
1179
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1180
|
+
{ exactKey: true },
|
|
1181
|
+
);
|
|
1182
|
+
return toContent(result);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async function setLlmConfig({ api_key, extraction_model, classification_model, merge_model }) {
|
|
1186
|
+
const body = {};
|
|
1187
|
+
if (api_key != null) body.api_key = api_key;
|
|
1188
|
+
if (extraction_model !== undefined) body.extraction_model = extraction_model;
|
|
1189
|
+
if (classification_model !== undefined) body.classification_model = classification_model;
|
|
1190
|
+
if (merge_model !== undefined) body.merge_model = merge_model;
|
|
1191
|
+
// PATCH (partial-merge) + EXACT user key (review #12, #13).
|
|
1192
|
+
const result = await apiCall(
|
|
1193
|
+
"PATCH",
|
|
1194
|
+
"/api/v1/tenants/me/llm-config",
|
|
1195
|
+
body,
|
|
1196
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1197
|
+
{ exactKey: true },
|
|
1198
|
+
);
|
|
1199
|
+
return toContent(result);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
async function knowledgeLlmUsage(args = {}) {
|
|
1203
|
+
// Query-string building lives in lib/http-helpers.js so the test suite exercises
|
|
1204
|
+
// the same from/to/limit/offset logic the server ships.
|
|
1205
|
+
const result = await apiCall(
|
|
1206
|
+
"GET",
|
|
1207
|
+
llmUsagePath(args),
|
|
1164
1208
|
null,
|
|
1165
1209
|
process.env.LOOPCTL_ORCH_KEY,
|
|
1166
1210
|
);
|
|
@@ -2443,10 +2487,14 @@ const TOOLS = [
|
|
|
2443
2487
|
description:
|
|
2444
2488
|
"Find distant-but-bridgeable article pairs in the optimal-novelty embedding band " +
|
|
2445
2489
|
"(cosine distance min..max, default 0.3–0.7) — the creative sweet spot (neither banal " +
|
|
2446
|
-
"nor nonsense). Returns { data: [{a, b, distance}], meta:{count} }.
|
|
2447
|
-
"
|
|
2448
|
-
"
|
|
2449
|
-
"
|
|
2490
|
+
"nor nonsense). Returns { data: [{a, b, distance}], meta:{count, has_more, total_count} }. " +
|
|
2491
|
+
"Paginate via meta.has_more (a limit+1 look-ahead) — NOT total_count, which is DEPRECATED " +
|
|
2492
|
+
"and always null here: unlike sibling offset/limit tools, an exact total is an " +
|
|
2493
|
+
"O(candidates²) cost (the pair set is a column-to-column self-join), so it was removed for " +
|
|
2494
|
+
"latency (loopctl #202/#203). With bridge_path:true, only pairs also connected in the link " +
|
|
2495
|
+
"graph (≤2 hops) are returned — that branch samples a smaller candidate slice, so it may " +
|
|
2496
|
+
"return fewer pairs. Samples up to 1000 embedded published articles (500 for bridge_path). " +
|
|
2497
|
+
"For computational-creativity ideation (remote-associates generator).",
|
|
2450
2498
|
inputSchema: {
|
|
2451
2499
|
type: "object",
|
|
2452
2500
|
properties: {
|
|
@@ -3312,6 +3360,76 @@ const TOOLS = [
|
|
|
3312
3360
|
},
|
|
3313
3361
|
},
|
|
3314
3362
|
|
|
3363
|
+
// Per-tenant BYO LLM config + usage (Epic 28 residual, #179)
|
|
3364
|
+
{
|
|
3365
|
+
name: "llm_config",
|
|
3366
|
+
description:
|
|
3367
|
+
"Get the tenant's BYO Anthropic LLM configuration: the per-operation model " +
|
|
3368
|
+
"choices, whether a key is configured (has_api_key), and a masked last-4 hint. " +
|
|
3369
|
+
"NEVER returns the key itself. Requires user role (LOOPCTL_USER_KEY).",
|
|
3370
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
3371
|
+
},
|
|
3372
|
+
{
|
|
3373
|
+
name: "set_llm_config",
|
|
3374
|
+
description:
|
|
3375
|
+
"Set/rotate the tenant's OWN Anthropic API key (stored encrypted, never returned) " +
|
|
3376
|
+
"and the three per-operation models (extraction/classification/merge). loopctl " +
|
|
3377
|
+
"fronts no LLM cost — the tenant's key bills the tenant. Any subset of fields may " +
|
|
3378
|
+
"be sent; omitting api_key leaves the existing key untouched. Model ids are " +
|
|
3379
|
+
"free-form (any model the key permits). Requires user role (LOOPCTL_USER_KEY).",
|
|
3380
|
+
inputSchema: {
|
|
3381
|
+
type: "object",
|
|
3382
|
+
properties: {
|
|
3383
|
+
api_key: {
|
|
3384
|
+
type: "string",
|
|
3385
|
+
description: "Anthropic API key (write-only; stored encrypted, never returned).",
|
|
3386
|
+
},
|
|
3387
|
+
extraction_model: {
|
|
3388
|
+
type: "string",
|
|
3389
|
+
description: "Model id for knowledge extraction (null → server default).",
|
|
3390
|
+
},
|
|
3391
|
+
classification_model: {
|
|
3392
|
+
type: "string",
|
|
3393
|
+
description: "Model id for category classification (null → server default).",
|
|
3394
|
+
},
|
|
3395
|
+
merge_model: {
|
|
3396
|
+
type: "string",
|
|
3397
|
+
description: "Model id for article merge synthesis (null → server default).",
|
|
3398
|
+
},
|
|
3399
|
+
},
|
|
3400
|
+
required: [],
|
|
3401
|
+
},
|
|
3402
|
+
},
|
|
3403
|
+
{
|
|
3404
|
+
name: "knowledge_llm_usage",
|
|
3405
|
+
description:
|
|
3406
|
+
"Per-tenant LLM token-usage summary, grouped by operation + model + source_type + " +
|
|
3407
|
+
"day over an optional date range, newest day first, with offset/limit pagination " +
|
|
3408
|
+
"over meta.total_count. When `from` is omitted it defaults to a 90-day lookback; the " +
|
|
3409
|
+
"EFFECTIVE window is echoed in meta.from/meta.to so you can detect that older usage " +
|
|
3410
|
+
"was excluded (pass an explicit `from` to widen it). Record-only — there is no budget " +
|
|
3411
|
+
"enforcement. Requires orchestrator role.",
|
|
3412
|
+
inputSchema: {
|
|
3413
|
+
type: "object",
|
|
3414
|
+
properties: {
|
|
3415
|
+
from: {
|
|
3416
|
+
type: "string",
|
|
3417
|
+
description: "Optional ISO 8601 lower bound (inclusive) on occurred_at.",
|
|
3418
|
+
},
|
|
3419
|
+
to: {
|
|
3420
|
+
type: "string",
|
|
3421
|
+
description: "Optional ISO 8601 upper bound (inclusive) on occurred_at.",
|
|
3422
|
+
},
|
|
3423
|
+
limit: {
|
|
3424
|
+
type: "integer",
|
|
3425
|
+
description: "Rows per page (default 50, clamped to 200).",
|
|
3426
|
+
},
|
|
3427
|
+
offset: { type: "integer", description: "Rows to skip (default 0)." },
|
|
3428
|
+
},
|
|
3429
|
+
required: [],
|
|
3430
|
+
},
|
|
3431
|
+
},
|
|
3432
|
+
|
|
3315
3433
|
// Knowledge Analytics Tools (orchestrator key)
|
|
3316
3434
|
{
|
|
3317
3435
|
name: "knowledge_curation_log",
|
|
@@ -3787,6 +3905,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3787
3905
|
case "knowledge_ingestion_jobs":
|
|
3788
3906
|
return await knowledgeIngestionJobs(args);
|
|
3789
3907
|
|
|
3908
|
+
// Per-tenant BYO LLM config + usage (Epic 28, #179)
|
|
3909
|
+
case "llm_config":
|
|
3910
|
+
return await llmConfig();
|
|
3911
|
+
|
|
3912
|
+
case "set_llm_config":
|
|
3913
|
+
return await setLlmConfig(args);
|
|
3914
|
+
|
|
3915
|
+
case "knowledge_llm_usage":
|
|
3916
|
+
return await knowledgeLlmUsage(args);
|
|
3917
|
+
|
|
3790
3918
|
// Knowledge Analytics Tools
|
|
3791
3919
|
case "knowledge_curation_log":
|
|
3792
3920
|
return await knowledgeCurationLog(args);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Pure, importable HTTP helpers shared by index.js and the test suite.
|
|
2
|
+
//
|
|
3
|
+
// index.js is a stdio entry point with top-level await (importing it would boot
|
|
4
|
+
// the MCP server), so it cannot be imported directly by tests. Extracting the
|
|
5
|
+
// logic that actually had bugs — query-string building for the paginated tools
|
|
6
|
+
// and the defensive JSON parse in apiCall — into this side-effect-free module
|
|
7
|
+
// gives BOTH index.js and the tests a single source of truth. The tests exercise
|
|
8
|
+
// the SAME code the server ships, so a regression in this logic fails CI instead
|
|
9
|
+
// of silently passing against a hand-copied mirror.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a `?a=b&c=d` query string from an array of [key, value] pairs. Skips
|
|
13
|
+
* null/undefined values (so unset params are omitted) and stringifies the rest.
|
|
14
|
+
* Returns "" when nothing is set.
|
|
15
|
+
*
|
|
16
|
+
* @param {Array<[string, unknown]>} pairs
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function buildQuery(pairs) {
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
for (const [key, value] of pairs) {
|
|
22
|
+
if (value != null) params.set(key, String(value));
|
|
23
|
+
}
|
|
24
|
+
const qs = params.toString();
|
|
25
|
+
return qs ? `?${qs}` : "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Path for `list_projects`, honoring page/page_size (#247, mcp-01).
|
|
30
|
+
*
|
|
31
|
+
* @param {{ page?: number, page_size?: number }} [args]
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function projectsPath({ page, page_size } = {}) {
|
|
35
|
+
return `/api/v1/projects${buildQuery([
|
|
36
|
+
["page", page],
|
|
37
|
+
["page_size", page_size],
|
|
38
|
+
])}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Path for `knowledge_ingestion_jobs`, honoring limit/offset/since_days
|
|
43
|
+
* (#248, mcp-02).
|
|
44
|
+
*
|
|
45
|
+
* @param {{ limit?: number, offset?: number, since_days?: number }} [args]
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function ingestionJobsPath({ limit, offset, since_days } = {}) {
|
|
49
|
+
return `/api/v1/knowledge/ingestion-jobs${buildQuery([
|
|
50
|
+
["limit", limit],
|
|
51
|
+
["offset", offset],
|
|
52
|
+
["since_days", since_days],
|
|
53
|
+
])}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Path for `knowledge_llm_usage`, honoring from/to/limit/offset (Epic 28, #179).
|
|
58
|
+
*
|
|
59
|
+
* @param {{ from?: string, to?: string, limit?: number, offset?: number }} [args]
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function llmUsagePath({ from, to, limit, offset } = {}) {
|
|
63
|
+
return `/api/v1/knowledge/llm-usage${buildQuery([
|
|
64
|
+
["from", from],
|
|
65
|
+
["to", to],
|
|
66
|
+
["limit", limit],
|
|
67
|
+
["offset", offset],
|
|
68
|
+
])}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Defensively parse the raw text body of a JSON-content-type HTTP response
|
|
73
|
+
* (#249, mcp-03).
|
|
74
|
+
*
|
|
75
|
+
* A JSON content-type header is no guarantee of a well-formed body: a transient
|
|
76
|
+
* Fly edge 502/503 or a truncated response can arrive with the JSON header but an
|
|
77
|
+
* empty or non-JSON body, which makes a bare `response.json()` throw an unhandled
|
|
78
|
+
* exception. This turns that into a STRUCTURED error the MCP tool layer already
|
|
79
|
+
* understands (`{ error: true, status, body }`), including the HTTP status and a
|
|
80
|
+
* raw-body snippet for debugging.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} rawText - the response body as text
|
|
83
|
+
* @param {number} status - the HTTP status code
|
|
84
|
+
* @returns {{ parsed: unknown } | { error: true, status: number, body: string }}
|
|
85
|
+
* `{ parsed }` on success, or a structured error object on empty/invalid JSON.
|
|
86
|
+
*/
|
|
87
|
+
export function parseJsonResponseBody(rawText, status) {
|
|
88
|
+
if (rawText.trim() === "") {
|
|
89
|
+
return {
|
|
90
|
+
error: true,
|
|
91
|
+
status,
|
|
92
|
+
body: `invalid/empty JSON response from server (HTTP ${status}): empty body`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return { parsed: JSON.parse(rawText) };
|
|
97
|
+
} catch {
|
|
98
|
+
const snippet =
|
|
99
|
+
rawText.length > 200 ? `${rawText.slice(0, 200)}... (truncated)` : rawText;
|
|
100
|
+
return {
|
|
101
|
+
error: true,
|
|
102
|
+
status,
|
|
103
|
+
body: `invalid/empty JSON response from server (HTTP ${status}): ${snippet}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopctl-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.31.1",
|
|
4
4
|
"description": "MCP server for loopctl — structural trust for AI development loops",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node index.js",
|
|
12
|
-
"test": "node --test"
|
|
12
|
+
"test": "node --test test/*.test.js",
|
|
13
|
+
"test:smoke": "node smoke_test.mjs"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
15
16
|
"mcp",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
},
|
|
34
35
|
"files": [
|
|
35
36
|
"index.js",
|
|
37
|
+
"lib",
|
|
36
38
|
"README.md",
|
|
37
39
|
"LICENSE"
|
|
38
40
|
],
|