loopctl-mcp-server 2.33.1 → 2.34.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 +49 -4
- package/index.js +95 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,11 +62,56 @@ 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
|
|
65
|
+
| `LOOPCTL_USER_KEY` | User role API key (minted at signup). Required for **first-time BYO LLM key provisioning** (`set_llm_config` / `llm_config` — see [First-time setup](#first-time-setup--provision-your-byo-llm-keys)) and for destructive admin tools like `knowledge_bulk_publish`. | -- |
|
|
66
66
|
| `LOOPCTL_STH_STATE_PATH` | Absolute path for the witness-protocol STH cache file (see [Witness protocol](#witness-protocol-sth)). Optional. | per-(server + key) file under the OS temp dir |
|
|
67
67
|
|
|
68
68
|
Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_KEY`.
|
|
69
69
|
|
|
70
|
+
## First-time setup — provision your BYO LLM keys
|
|
71
|
+
|
|
72
|
+
**loopctl is agent-native and strictly BYO (bring-your-own-keys).** loopctl fronts
|
|
73
|
+
**no** LLM cost — the knowledge wiki runs entirely on *your* provider keys, which you
|
|
74
|
+
provision once and which bill you directly. Two SEPARATE keys power two capabilities:
|
|
75
|
+
|
|
76
|
+
| Key | Provider | Powers | Missing ⇒ |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `api_key` | Anthropic | knowledge **ingest** — extraction, classification, merge synthesis | `knowledge_ingest` returns **422** (`code: no_api_key`) |
|
|
79
|
+
| `embedding_api_key` | OpenAI-compatible | article **embeddings + semantic search** | `knowledge_search` silently degrades to **keyword-only** (`meta.fallback_reason: no_embedding_key`) |
|
|
80
|
+
|
|
81
|
+
Both keys are stored **encrypted** and are **never returned** by any tool. You can set
|
|
82
|
+
per-operation model overrides (`extraction_model`, `classification_model`,
|
|
83
|
+
`merge_model`, `embedding_model`); each defaults server-side when omitted.
|
|
84
|
+
|
|
85
|
+
### The smooth path (once, at onboarding)
|
|
86
|
+
|
|
87
|
+
1. **Sign up** and obtain your keys. Tenant signup is anchored to a hardware
|
|
88
|
+
authenticator (WebAuthn) — the one human touch. Signup mints your **user-role**
|
|
89
|
+
API key. Also grab your **agent** and **orchestrator** keys for day-to-day work.
|
|
90
|
+
2. **Set the env** in your `.mcp.json` (see [Configuration](#configuration)):
|
|
91
|
+
`LOOPCTL_SERVER`, `LOOPCTL_USER_KEY` (needed for this step), plus `LOOPCTL_AGENT_KEY`
|
|
92
|
+
/ `LOOPCTL_ORCH_KEY`.
|
|
93
|
+
3. **Provision both keys in one call** (uses `LOOPCTL_USER_KEY`):
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
set_llm_config({ api_key: "sk-ant-...", embedding_api_key: "sk-..." })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Partial-merge: you can set or rotate one key at a time; omitting a key leaves the
|
|
100
|
+
existing one untouched. Optionally pass model overrides in the same call.
|
|
101
|
+
4. **You're live.** `knowledge_ingest` extracts articles and `knowledge_search`
|
|
102
|
+
(combined/semantic) ranks by meaning. Check status anytime with
|
|
103
|
+
`llm_config` (reports `has_api_key` / `has_embedding_key` + masked last-4 hints,
|
|
104
|
+
never the key).
|
|
105
|
+
|
|
106
|
+
### Self-healing: you can't get stuck
|
|
107
|
+
|
|
108
|
+
If you call `knowledge_ingest` or `knowledge_search` **before** provisioning, the
|
|
109
|
+
tool result **leads with an `ACTION REQUIRED` notice** and a machine-readable
|
|
110
|
+
`remediation` object — naming the `set_llm_config` tool, a copy-paste example, the
|
|
111
|
+
REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (or an
|
|
112
|
+
autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
|
|
113
|
+
[`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
|
|
114
|
+
|
|
70
115
|
## Tools (72)
|
|
71
116
|
|
|
72
117
|
### Project Tools
|
|
@@ -177,7 +222,7 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
|
|
|
177
222
|
| `knowledge_drafts` | List draft (unpublished) knowledge articles with pagination. Optional: `limit` (default 20, max 1000 — over-max → 400, no silent clamp), `offset` (default 0), `project_id`. Returns `meta.total_count`. |
|
|
178
223
|
| `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`. |
|
|
179
224
|
| `knowledge_export` | Export all knowledge articles as an OKF v0.1 bundle (gzipped tar archive, unbounded, bounded-memory streaming, fail-closed). Returns a curl command for direct download — **the download requires `LOOPCTL_USER_KEY`** (an orchestrator key would 403). Pass `format=json` for buffered in-memory JSON (convenience tool for file writers; capped at `export_max_buffered_export_articles` — returns 413 if over-cap). Optional: `project_id`, `format` (`tar.gz` default or `json`). |
|
|
180
|
-
| `knowledge_ingest` | Submit a URL or raw content for knowledge extraction. Enqueues an Oban job. Extracted articles are **drafts by default** (lower-trust LLM output); pass `publish: true` to publish on extraction. Required: `source_type`. One of: `url` or `content`. Optional: `project_id`, `publish`. |
|
|
225
|
+
| `knowledge_ingest` | Submit a URL or raw content for knowledge extraction. Enqueues an Oban job. Extracted articles are **drafts by default** (lower-trust LLM output); pass `publish: true` to publish on extraction. **BYO:** runs on the tenant's own Anthropic key — a keyless tenant gets a 422 whose result leads with an `ACTION REQUIRED` notice pointing at `set_llm_config` (see [First-time setup](#first-time-setup--provision-your-byo-llm-keys)). Required: `source_type`. One of: `url` or `content`. Optional: `project_id`, `publish`. |
|
|
181
226
|
| `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. |
|
|
182
227
|
| `knowledge_ingestion_jobs` | List recent content ingestion jobs (last 7 days, max 50). |
|
|
183
228
|
|
|
@@ -185,8 +230,8 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
|
|
|
185
230
|
|
|
186
231
|
| Tool | Description |
|
|
187
232
|
|---|---|
|
|
188
|
-
| `llm_config` | Get the tenant's BYO
|
|
189
|
-
| `set_llm_config` | Set/rotate the tenant's OWN Anthropic
|
|
233
|
+
| `llm_config` | **Check your onboarding status.** Get the tenant's BYO LLM config: per-operation models and whether each key is set — `has_api_key` (Anthropic) / `has_embedding_key` (OpenAI embedding) — plus masked last-4 hints. Never returns a key. Requires **user** key. |
|
|
234
|
+
| `set_llm_config` | **First-time setup (do once).** Set/rotate the tenant's OWN **Anthropic `api_key`** (powers ingest) AND **OpenAI `embedding_api_key`** (powers semantic search) — both stored encrypted, never returned — plus the per-operation models (`extraction_model`/`classification_model`/`merge_model`/`embedding_model`). Any subset; partial-merge (omitting a key leaves it untouched). See [First-time setup](#first-time-setup--provision-your-byo-llm-keys). Requires **user** key. |
|
|
190
235
|
| `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. |
|
|
191
236
|
|
|
192
237
|
### Knowledge Analytics Tools (orchestrator key)
|
package/index.js
CHANGED
|
@@ -233,6 +233,51 @@ function toContentCompact(result) {
|
|
|
233
233
|
return toContent(result);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Surface a server-emitted BYO-LLM `remediation` PROMINENTLY as a leading text
|
|
238
|
+
* block. A stranger agent that calls knowledge_ingest / knowledge_search /
|
|
239
|
+
* knowledge_context BEFORE provisioning its keys then reads the exact next step
|
|
240
|
+
* in the tool result — call set_llm_config — instead of hunting through raw JSON.
|
|
241
|
+
*
|
|
242
|
+
* The remediation lives in one of two places depending on the failure shape:
|
|
243
|
+
* - ingest no-key 422 → result.body.error.remediation (code "no_api_key")
|
|
244
|
+
* - search/context degrade → result.meta.remediation (fallback_reason
|
|
245
|
+
* "no_embedding_key"); the request still returns 200 keyword-only results.
|
|
246
|
+
*
|
|
247
|
+
* Returns a notice string, or null when there is no LLM remediation to surface.
|
|
248
|
+
*/
|
|
249
|
+
function llmRemediationNotice(result) {
|
|
250
|
+
const rem =
|
|
251
|
+
(result &&
|
|
252
|
+
result.error === true &&
|
|
253
|
+
result.body &&
|
|
254
|
+
result.body.error &&
|
|
255
|
+
result.body.error.remediation) ||
|
|
256
|
+
(result && result.meta && result.meta.remediation) ||
|
|
257
|
+
null;
|
|
258
|
+
|
|
259
|
+
if (!rem || rem.action !== "configure_llm") return null;
|
|
260
|
+
|
|
261
|
+
const missing = Array.isArray(rem.missing) ? rem.missing.join(", ") : rem.missing;
|
|
262
|
+
return (
|
|
263
|
+
`ACTION REQUIRED — BYO LLM key not configured (missing: ${missing}). ` +
|
|
264
|
+
`${rem.message || ""} Provision it ONCE with the ${rem.mcp_tool} MCP tool, e.g. ` +
|
|
265
|
+
`${rem.example} (REST: ${rem.api}). Requires your user-role key (LOOPCTL_USER_KEY). ` +
|
|
266
|
+
`Docs: ${rem.docs}`
|
|
267
|
+
).replace(/\s+/g, " ").trim();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* toContent, but with any BYO-LLM remediation notice prepended so it is the first
|
|
272
|
+
* thing the agent reads. Preserves isError from the underlying result.
|
|
273
|
+
*/
|
|
274
|
+
function withRemediationNotice(result) {
|
|
275
|
+
const base = toContent(result);
|
|
276
|
+
const notice = llmRemediationNotice(result);
|
|
277
|
+
if (!notice) return base;
|
|
278
|
+
return { ...base, content: [{ type: "text", text: notice }, ...base.content] };
|
|
279
|
+
}
|
|
280
|
+
|
|
236
281
|
// ---------------------------------------------------------------------------
|
|
237
282
|
// Tool implementations
|
|
238
283
|
// ---------------------------------------------------------------------------
|
|
@@ -831,7 +876,10 @@ async function knowledgeSearch({ q, project_id, story_id, category, tags, match,
|
|
|
831
876
|
if (offset != null) params.set("offset", String(offset));
|
|
832
877
|
|
|
833
878
|
const result = await apiCall("GET", `/api/v1/knowledge/search?${params}`, null, process.env.LOOPCTL_AGENT_KEY);
|
|
834
|
-
|
|
879
|
+
// If search silently degraded to keyword-only for a missing embedding key, the
|
|
880
|
+
// server attaches meta.remediation — surface it PROMINENTLY so the agent knows to
|
|
881
|
+
// call set_llm_config to enable semantic ranking.
|
|
882
|
+
return withRemediationNotice(result);
|
|
835
883
|
}
|
|
836
884
|
|
|
837
885
|
async function knowledgeList({
|
|
@@ -902,7 +950,8 @@ async function knowledgeContext({
|
|
|
902
950
|
if (conversation_id) params.set("conversation_id", conversation_id);
|
|
903
951
|
|
|
904
952
|
const result = await apiCall("GET", `/api/v1/knowledge/context?${params}`, null, process.env.LOOPCTL_AGENT_KEY);
|
|
905
|
-
|
|
953
|
+
// Same as knowledge_search: surface a missing-embedding-key remediation prominently.
|
|
954
|
+
return withRemediationNotice(result);
|
|
906
955
|
}
|
|
907
956
|
|
|
908
957
|
async function knowledgeCreate({
|
|
@@ -1158,7 +1207,9 @@ async function knowledgeIngest({ url, content, source_type, project_id, publish
|
|
|
1158
1207
|
if (project_id) body.project_id = project_id;
|
|
1159
1208
|
if (publish) body.publish = true;
|
|
1160
1209
|
const result = await apiCall("POST", "/api/v1/knowledge/ingest", body, process.env.LOOPCTL_ORCH_KEY);
|
|
1161
|
-
|
|
1210
|
+
// A keyless tenant gets a 422 (code no_api_key) carrying a remediation — surface it
|
|
1211
|
+
// prominently so a first-time agent knows to call set_llm_config before ingesting.
|
|
1212
|
+
return withRemediationNotice(result);
|
|
1162
1213
|
}
|
|
1163
1214
|
|
|
1164
1215
|
async function knowledgeIngestBatch({ items, project_id, publish }) {
|
|
@@ -1181,7 +1232,9 @@ async function knowledgeIngestBatch({ items, project_id, publish }) {
|
|
|
1181
1232
|
{ items: resolvedItems },
|
|
1182
1233
|
process.env.LOOPCTL_ORCH_KEY
|
|
1183
1234
|
);
|
|
1184
|
-
|
|
1235
|
+
// Batch ingest gates on the Anthropic key up front too — a keyless tenant gets a
|
|
1236
|
+
// 422 with the same remediation; surface it prominently.
|
|
1237
|
+
return withRemediationNotice(result);
|
|
1185
1238
|
}
|
|
1186
1239
|
|
|
1187
1240
|
async function knowledgeIngestionJobs(args = {}) {
|
|
@@ -2618,7 +2671,9 @@ const TOOLS = [
|
|
|
2618
2671
|
"If semantic ranking is unavailable the search transparently degrades to keyword-only " +
|
|
2619
2672
|
"(meta.fallback: true, meta.search_mode: 'keyword_only') and now reports meta.fallback_reason " +
|
|
2620
2673
|
"— a stable tag naming WHY (e.g. no_embedding_key, embedding_circuit_open, " +
|
|
2621
|
-
"embedding_provider_error_<status>, embedding_timeout)."
|
|
2674
|
+
"embedding_provider_error_<status>, embedding_timeout). When the reason is a MISSING " +
|
|
2675
|
+
"embedding key (no_embedding_key), the result leads with an ACTION REQUIRED notice + " +
|
|
2676
|
+
"meta.remediation telling you to provision it with set_llm_config (BYO — do it once).",
|
|
2622
2677
|
inputSchema: {
|
|
2623
2678
|
type: "object",
|
|
2624
2679
|
properties: {
|
|
@@ -3296,7 +3351,10 @@ const TOOLS = [
|
|
|
3296
3351
|
"Enqueues an Oban job that fetches the content (if URL), extracts knowledge articles via LLM, " +
|
|
3297
3352
|
"and inserts them. Extracted articles are DRAFTS by default (lower-trust LLM output, staged " +
|
|
3298
3353
|
"for review) — unlike knowledge_create which publishes by default. Pass publish:true to " +
|
|
3299
|
-
"publish them on extraction. Requires orchestrator role."
|
|
3354
|
+
"publish them on extraction. Requires orchestrator role. BYO: extraction runs on the " +
|
|
3355
|
+
"tenant's OWN Anthropic key — a keyless tenant gets a 422 (code no_api_key) and the " +
|
|
3356
|
+
"result leads with an ACTION REQUIRED notice telling you to provision it once via " +
|
|
3357
|
+
"set_llm_config.",
|
|
3300
3358
|
inputSchema: {
|
|
3301
3359
|
type: "object",
|
|
3302
3360
|
properties: {
|
|
@@ -3409,29 +3467,44 @@ const TOOLS = [
|
|
|
3409
3467
|
{
|
|
3410
3468
|
name: "llm_config",
|
|
3411
3469
|
description:
|
|
3412
|
-
"
|
|
3413
|
-
"
|
|
3414
|
-
"and
|
|
3415
|
-
"
|
|
3470
|
+
"CHECK your BYO LLM onboarding status. Returns this tenant's per-operation model " +
|
|
3471
|
+
"choices and whether each key is configured — `has_api_key` (Anthropic, powers " +
|
|
3472
|
+
"ingest) and `has_embedding_key` (OpenAI embedding, powers semantic search) — plus " +
|
|
3473
|
+
"masked last-4 hints. NEVER returns a key itself. Call this BEFORE ingest/search to " +
|
|
3474
|
+
"confirm setup, or after set_llm_config to verify it took. Uses your user-role key " +
|
|
3475
|
+
"(LOOPCTL_USER_KEY, obtained at signup); managing tenant secrets is user-only, so an " +
|
|
3476
|
+
"agent/orchestrator key is rejected.",
|
|
3416
3477
|
inputSchema: { type: "object", properties: {}, required: [] },
|
|
3417
3478
|
},
|
|
3418
3479
|
{
|
|
3419
3480
|
name: "set_llm_config",
|
|
3420
3481
|
description:
|
|
3421
|
-
"
|
|
3422
|
-
"
|
|
3423
|
-
"
|
|
3424
|
-
"
|
|
3425
|
-
"
|
|
3426
|
-
"
|
|
3427
|
-
"
|
|
3428
|
-
"(
|
|
3482
|
+
"FIRST-TIME SETUP — do this ONCE, right after signup, to bring the wiki online. " +
|
|
3483
|
+
"Provision your OWN Anthropic + OpenAI embedding keys so knowledge ingest AND " +
|
|
3484
|
+
"semantic search work. loopctl is strictly BYO: it fronts NO LLM cost — your keys " +
|
|
3485
|
+
"bill you directly and are stored ENCRYPTED, never returned. WHY it's required: with " +
|
|
3486
|
+
"no `api_key`, knowledge_ingest returns 422 (code no_api_key); with no " +
|
|
3487
|
+
"`embedding_api_key`, knowledge_search silently degrades to keyword-only " +
|
|
3488
|
+
"(meta.fallback_reason: no_embedding_key) — both responses carry a `remediation` " +
|
|
3489
|
+
"pointing back to THIS tool. WHICH key powers WHAT: `api_key` (Anthropic) → ingest " +
|
|
3490
|
+
"extraction / classification / merge synthesis; `embedding_api_key` " +
|
|
3491
|
+
"(OpenAI-compatible) → article embeddings + semantic ranking. All params are optional " +
|
|
3492
|
+
"and partial-merge — omitting a key leaves the existing one untouched, so you can set " +
|
|
3493
|
+
"or rotate one at a time. The per-operation model overrides " +
|
|
3494
|
+
"(extraction_model / classification_model / merge_model / embedding_model) are " +
|
|
3495
|
+
"free-form (any model the key permits) and default server-side when omitted. Typical " +
|
|
3496
|
+
'onboarding call: set_llm_config({api_key: "sk-ant-...", embedding_api_key: "sk-..."}). ' +
|
|
3497
|
+
"Verify anytime with llm_config (has_api_key / has_embedding_key). REQUIRES your " +
|
|
3498
|
+
"user-role key LOOPCTL_USER_KEY (minted by the human-anchored signup ceremony); if it " +
|
|
3499
|
+
"is unset the tool fails fast telling you to set it.",
|
|
3429
3500
|
inputSchema: {
|
|
3430
3501
|
type: "object",
|
|
3431
3502
|
properties: {
|
|
3432
3503
|
api_key: {
|
|
3433
3504
|
type: "string",
|
|
3434
|
-
description:
|
|
3505
|
+
description:
|
|
3506
|
+
"Anthropic API key — powers knowledge ingest (extraction/classification/merge). " +
|
|
3507
|
+
"Write-only; stored encrypted, never returned.",
|
|
3435
3508
|
},
|
|
3436
3509
|
extraction_model: {
|
|
3437
3510
|
type: "string",
|
|
@@ -3448,7 +3521,9 @@ const TOOLS = [
|
|
|
3448
3521
|
embedding_api_key: {
|
|
3449
3522
|
type: "string",
|
|
3450
3523
|
description:
|
|
3451
|
-
"OpenAI-compatible embedding API key
|
|
3524
|
+
"OpenAI-compatible embedding API key — powers article embeddings + semantic " +
|
|
3525
|
+
"search. Write-only; stored encrypted, never returned. Without it, articles are " +
|
|
3526
|
+
"not vector-searchable and search degrades to keyword-only.",
|
|
3452
3527
|
},
|
|
3453
3528
|
embedding_model: {
|
|
3454
3529
|
type: "string",
|