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.
Files changed (3) hide show
  1. package/README.md +49 -4
  2. package/index.js +95 -20
  3. 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 ONLY for destructive admin tools like `knowledge_bulk_publish`. Leave unset if you don't use those tools. | -- |
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 Anthropic LLM config: per-operation models, `has_api_key`, and a masked last-4 hint. Never returns the key. Requires **user** key. |
189
- | `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. |
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
- return toContent(result);
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
- return toContent(result);
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
- return toContent(result);
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
- return toContent(result);
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
- "Get the tenant's BYO Anthropic + embedding configuration: the per-operation " +
3413
- "model choices, whether each key is configured (has_api_key / has_embedding_key), " +
3414
- "and masked last-4 hints. NEVER returns a key itself. Requires user role " +
3415
- "(LOOPCTL_USER_KEY).",
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
- "Set/rotate the tenant's OWN Anthropic API key AND OpenAI embedding key (both " +
3422
- "stored encrypted, never returned) and the per-operation models " +
3423
- "(extraction/classification/merge/embedding). loopctl fronts no cost — the " +
3424
- "tenant's keys bill the tenant. Mandatory BYO for embeddings: without an " +
3425
- "embedding_api_key the tenant's articles are not vector-searchable. Any subset " +
3426
- "of fields may be sent; omitting a key leaves the existing key untouched. Model " +
3427
- "ids are free-form (any model the key permits). Requires user role " +
3428
- "(LOOPCTL_USER_KEY).",
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: "Anthropic API key (write-only; stored encrypted, never returned).",
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 (write-only; stored encrypted, never returned).",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.33.1",
3
+ "version": "2.34.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",