loopctl-mcp-server 2.33.1 → 2.35.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 +61 -6
  2. package/index.js +202 -20
  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 72 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 73 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,12 +62,66 @@ 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
- ## Tools (72)
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. loopctl has two trust tiers (US-26.7.1):
88
+ - **Human-anchored (unlocks work-breakdown / chain-of-custody too):** visit
89
+ `https://loopctl.com/signup` and enroll a hardware authenticator (WebAuthn) —
90
+ the one human touch. This mints your **user-role** API key; also grab your
91
+ **agent** and **orchestrator** keys for day-to-day work.
92
+ - **Agent-rooted (KB-tier only, fully automated):** call the `signup` MCP tool
93
+ (or `POST /api/v1/signup` directly) with `name`/`slug`/`email` — no human, no
94
+ hardware key. This mints a one-time `role: user` API key with the FULL
95
+ knowledge-wiki surface (ingest/search/curate, BYO LLM config, agent
96
+ registration), but it **cannot** perform work-breakdown / chain-of-custody
97
+ operations. The `raw_key` in the response is shown ONCE — save it
98
+ immediately (it cannot be retrieved again).
99
+ 2. **Set the env** in your `.mcp.json` (see [Configuration](#configuration)):
100
+ `LOOPCTL_SERVER`, `LOOPCTL_USER_KEY` (needed for this step), plus `LOOPCTL_AGENT_KEY`
101
+ / `LOOPCTL_ORCH_KEY`.
102
+ 3. **Provision both keys in one call** (uses `LOOPCTL_USER_KEY`):
103
+
104
+ ```
105
+ set_llm_config({ api_key: "sk-ant-...", embedding_api_key: "sk-..." })
106
+ ```
107
+
108
+ Partial-merge: you can set or rotate one key at a time; omitting a key leaves the
109
+ existing one untouched. Optionally pass model overrides in the same call.
110
+ 4. **You're live.** `knowledge_ingest` extracts articles and `knowledge_search`
111
+ (combined/semantic) ranks by meaning. Check status anytime with
112
+ `llm_config` (reports `has_api_key` / `has_embedding_key` + masked last-4 hints,
113
+ never the key).
114
+
115
+ ### Self-healing: you can't get stuck
116
+
117
+ If you call `knowledge_ingest` or `knowledge_search` **before** provisioning, the
118
+ tool result **leads with an `ACTION REQUIRED` notice** and a machine-readable
119
+ `remediation` object — naming the `set_llm_config` tool, a copy-paste example, the
120
+ REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (or an
121
+ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
122
+ [`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
123
+
124
+ ## Tools (73)
71
125
 
72
126
  ### Project Tools
73
127
 
@@ -177,7 +231,7 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
177
231
  | `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
232
  | `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
233
  | `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`. |
234
+ | `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
235
  | `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
236
  | `knowledge_ingestion_jobs` | List recent content ingestion jobs (last 7 days, max 50). |
183
237
 
@@ -185,8 +239,8 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
185
239
 
186
240
  | Tool | Description |
187
241
  |---|---|
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. |
242
+ | `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. |
243
+ | `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
244
  | `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
245
 
192
246
  ### Knowledge Analytics Tools (orchestrator key)
@@ -213,6 +267,7 @@ Key distribution for the dispatch pattern (Epic 26): per-dispatch ephemeral keys
213
267
 
214
268
  | Tool | Description |
215
269
  |---|---|
270
+ | `signup` | **US-26.7.1.** Create a NEW **agent-rooted (KB-tier)** tenant and mint its one-time root API key — entirely through this call, no human operator, no hardware authenticator, no existing API key required. The tenant gets the FULL knowledge-wiki surface but **cannot** perform work-breakdown / chain-of-custody operations (those require a separate human-anchored tenant via the WebAuthn ceremony at `https://loopctl.com/signup`). Rate-limited per client IP (<= 5/hour). The `raw_key` is shown ONCE — save it immediately (e.g. as `LOOPCTL_USER_KEY`). Required: `name`, `slug`, `email`. |
216
271
  | `dispatch` | Mint an ephemeral, scoped api_key for a sub-agent dispatch, carrying its lineage path. The `raw_key` is returned ONCE — pass it to the sub-agent's launch args, never store it in env vars; it expires after `expires_in_seconds` (default 3600, max 14400). Required: `role` (`agent`/`orchestrator`), `agent_id`. Optional: `parent_dispatch_id`, `story_id`. |
217
272
  | `recover_cap` | Re-mint a capability token for a story you're assigned to, after a session crash lost your cap. Required: `story_id`. Optional: `cap_type` (`start_cap`/`report_cap`, default `start_cap`), `lineage`. |
218
273
  | `get_sth` | Get the latest Signed Tree Head for a tenant's tamper-evident audit chain. Public — no auth required. Required: `tenant_id`. |
package/index.js CHANGED
@@ -189,6 +189,75 @@ async function apiCall(method, path, body, keyOverride, { exactKey = false } = {
189
189
  return responseBody;
190
190
  }
191
191
 
192
+ /**
193
+ * Unauthenticated request helper for genuinely public endpoints (currently
194
+ * only POST /api/v1/signup). No Authorization header is sent — the public
195
+ * signup POST carries no witness header either (US-26.7.1), so this
196
+ * bypasses `witnessClientFor` entirely and calls `fetch` directly. Response
197
+ * parsing mirrors `apiCall`'s so callers can pass the result straight to
198
+ * `toContent`/`toContentCompact`.
199
+ */
200
+ async function publicApiCall(method, path, body) {
201
+ const url = `${getBaseUrl()}${path}`;
202
+ const headers = { "Content-Type": "application/json", Accept: "application/json" };
203
+ const serializedBody =
204
+ body !== undefined && body !== null ? JSON.stringify(body) : undefined;
205
+
206
+ let response;
207
+ try {
208
+ response = await fetch(url, {
209
+ method,
210
+ headers,
211
+ body: serializedBody,
212
+ signal: AbortSignal.timeout(30_000),
213
+ });
214
+ } catch (err) {
215
+ if (err.name === "TimeoutError") {
216
+ return { error: true, status: 0, body: "Request timed out after 30s" };
217
+ }
218
+ const cause = err.cause?.message ? ` (${err.cause.message})` : "";
219
+ return { error: true, status: 0, body: `Network error: ${err.message}${cause}` };
220
+ }
221
+
222
+ if (response.status === 204) {
223
+ return { ok: true };
224
+ }
225
+
226
+ let responseBody;
227
+ const contentType = response.headers.get("content-type") || "";
228
+ if (contentType.includes("application/json")) {
229
+ const raw = await response.text();
230
+ const outcome = parseJsonResponseBody(raw, response.status);
231
+ if (outcome.error) {
232
+ return outcome;
233
+ }
234
+ responseBody = outcome.parsed;
235
+ } else {
236
+ const text = await response.text();
237
+ try {
238
+ responseBody = JSON.parse(text);
239
+ } catch {
240
+ responseBody = text;
241
+ }
242
+ }
243
+
244
+ if (!response.ok) {
245
+ let errorBody = responseBody;
246
+ if (typeof errorBody === "string" && errorBody.length > 500) {
247
+ errorBody = errorBody
248
+ .replace(/<[^>]+>/g, " ")
249
+ .replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">")
250
+ .replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ")
251
+ .replace(/\s+/g, " ")
252
+ .trim()
253
+ .slice(0, 500) + "... (truncated)";
254
+ }
255
+ return { error: true, status: response.status, body: errorBody };
256
+ }
257
+
258
+ return responseBody;
259
+ }
260
+
192
261
  function toContent(result) {
193
262
  const isErr = result && result.error === true;
194
263
  return {
@@ -233,6 +302,51 @@ function toContentCompact(result) {
233
302
  return toContent(result);
234
303
  }
235
304
 
305
+ /**
306
+ * Surface a server-emitted BYO-LLM `remediation` PROMINENTLY as a leading text
307
+ * block. A stranger agent that calls knowledge_ingest / knowledge_search /
308
+ * knowledge_context BEFORE provisioning its keys then reads the exact next step
309
+ * in the tool result — call set_llm_config — instead of hunting through raw JSON.
310
+ *
311
+ * The remediation lives in one of two places depending on the failure shape:
312
+ * - ingest no-key 422 → result.body.error.remediation (code "no_api_key")
313
+ * - search/context degrade → result.meta.remediation (fallback_reason
314
+ * "no_embedding_key"); the request still returns 200 keyword-only results.
315
+ *
316
+ * Returns a notice string, or null when there is no LLM remediation to surface.
317
+ */
318
+ function llmRemediationNotice(result) {
319
+ const rem =
320
+ (result &&
321
+ result.error === true &&
322
+ result.body &&
323
+ result.body.error &&
324
+ result.body.error.remediation) ||
325
+ (result && result.meta && result.meta.remediation) ||
326
+ null;
327
+
328
+ if (!rem || rem.action !== "configure_llm") return null;
329
+
330
+ const missing = Array.isArray(rem.missing) ? rem.missing.join(", ") : rem.missing;
331
+ return (
332
+ `ACTION REQUIRED — BYO LLM key not configured (missing: ${missing}). ` +
333
+ `${rem.message || ""} Provision it ONCE with the ${rem.mcp_tool} MCP tool, e.g. ` +
334
+ `${rem.example} (REST: ${rem.api}). Requires your user-role key (LOOPCTL_USER_KEY). ` +
335
+ `Docs: ${rem.docs}`
336
+ ).replace(/\s+/g, " ").trim();
337
+ }
338
+
339
+ /**
340
+ * toContent, but with any BYO-LLM remediation notice prepended so it is the first
341
+ * thing the agent reads. Preserves isError from the underlying result.
342
+ */
343
+ function withRemediationNotice(result) {
344
+ const base = toContent(result);
345
+ const notice = llmRemediationNotice(result);
346
+ if (!notice) return base;
347
+ return { ...base, content: [{ type: "text", text: notice }, ...base.content] };
348
+ }
349
+
236
350
  // ---------------------------------------------------------------------------
237
351
  // Tool implementations
238
352
  // ---------------------------------------------------------------------------
@@ -831,7 +945,10 @@ async function knowledgeSearch({ q, project_id, story_id, category, tags, match,
831
945
  if (offset != null) params.set("offset", String(offset));
832
946
 
833
947
  const result = await apiCall("GET", `/api/v1/knowledge/search?${params}`, null, process.env.LOOPCTL_AGENT_KEY);
834
- return toContent(result);
948
+ // If search silently degraded to keyword-only for a missing embedding key, the
949
+ // server attaches meta.remediation — surface it PROMINENTLY so the agent knows to
950
+ // call set_llm_config to enable semantic ranking.
951
+ return withRemediationNotice(result);
835
952
  }
836
953
 
837
954
  async function knowledgeList({
@@ -902,7 +1019,8 @@ async function knowledgeContext({
902
1019
  if (conversation_id) params.set("conversation_id", conversation_id);
903
1020
 
904
1021
  const result = await apiCall("GET", `/api/v1/knowledge/context?${params}`, null, process.env.LOOPCTL_AGENT_KEY);
905
- return toContent(result);
1022
+ // Same as knowledge_search: surface a missing-embedding-key remediation prominently.
1023
+ return withRemediationNotice(result);
906
1024
  }
907
1025
 
908
1026
  async function knowledgeCreate({
@@ -1158,7 +1276,9 @@ async function knowledgeIngest({ url, content, source_type, project_id, publish
1158
1276
  if (project_id) body.project_id = project_id;
1159
1277
  if (publish) body.publish = true;
1160
1278
  const result = await apiCall("POST", "/api/v1/knowledge/ingest", body, process.env.LOOPCTL_ORCH_KEY);
1161
- return toContent(result);
1279
+ // A keyless tenant gets a 422 (code no_api_key) carrying a remediation — surface it
1280
+ // prominently so a first-time agent knows to call set_llm_config before ingesting.
1281
+ return withRemediationNotice(result);
1162
1282
  }
1163
1283
 
1164
1284
  async function knowledgeIngestBatch({ items, project_id, publish }) {
@@ -1181,7 +1301,9 @@ async function knowledgeIngestBatch({ items, project_id, publish }) {
1181
1301
  { items: resolvedItems },
1182
1302
  process.env.LOOPCTL_ORCH_KEY
1183
1303
  );
1184
- return toContent(result);
1304
+ // Batch ingest gates on the Anthropic key up front too — a keyless tenant gets a
1305
+ // 422 with the same remediation; surface it prominently.
1306
+ return withRemediationNotice(result);
1185
1307
  }
1186
1308
 
1187
1309
  async function knowledgeIngestionJobs(args = {}) {
@@ -1558,6 +1680,16 @@ async function createDispatch({
1558
1680
  return toContent(result);
1559
1681
  }
1560
1682
 
1683
+ // US-26.7.1: public, agent-rooted (KB-tier) self-signup. No API key required —
1684
+ // this creates the tenant AND the key. The resulting tenant is KB-tier only
1685
+ // (knowledge ingest/search/curate on the caller's own BYO LLM keys); the
1686
+ // work-breakdown / chain-of-custody surface requires a separate,
1687
+ // human-anchored WebAuthn signup ceremony at https://loopctl.com/signup.
1688
+ async function signup({ name, slug, email }) {
1689
+ const result = await publicApiCall("POST", "/api/v1/signup", { name, slug, email });
1690
+ return toContent(result);
1691
+ }
1692
+
1561
1693
  // US-26: Signed Tree Head retrieval
1562
1694
  async function getSth({ tenant_id }) {
1563
1695
  const result = await apiCall("GET", `/api/v1/audit/sth/${tenant_id}`);
@@ -2618,7 +2750,9 @@ const TOOLS = [
2618
2750
  "If semantic ranking is unavailable the search transparently degrades to keyword-only " +
2619
2751
  "(meta.fallback: true, meta.search_mode: 'keyword_only') and now reports meta.fallback_reason " +
2620
2752
  "— a stable tag naming WHY (e.g. no_embedding_key, embedding_circuit_open, " +
2621
- "embedding_provider_error_<status>, embedding_timeout).",
2753
+ "embedding_provider_error_<status>, embedding_timeout). When the reason is a MISSING " +
2754
+ "embedding key (no_embedding_key), the result leads with an ACTION REQUIRED notice + " +
2755
+ "meta.remediation telling you to provision it with set_llm_config (BYO — do it once).",
2622
2756
  inputSchema: {
2623
2757
  type: "object",
2624
2758
  properties: {
@@ -3296,7 +3430,10 @@ const TOOLS = [
3296
3430
  "Enqueues an Oban job that fetches the content (if URL), extracts knowledge articles via LLM, " +
3297
3431
  "and inserts them. Extracted articles are DRAFTS by default (lower-trust LLM output, staged " +
3298
3432
  "for review) — unlike knowledge_create which publishes by default. Pass publish:true to " +
3299
- "publish them on extraction. Requires orchestrator role.",
3433
+ "publish them on extraction. Requires orchestrator role. BYO: extraction runs on the " +
3434
+ "tenant's OWN Anthropic key — a keyless tenant gets a 422 (code no_api_key) and the " +
3435
+ "result leads with an ACTION REQUIRED notice telling you to provision it once via " +
3436
+ "set_llm_config.",
3300
3437
  inputSchema: {
3301
3438
  type: "object",
3302
3439
  properties: {
@@ -3409,29 +3546,44 @@ const TOOLS = [
3409
3546
  {
3410
3547
  name: "llm_config",
3411
3548
  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).",
3549
+ "CHECK your BYO LLM onboarding status. Returns this tenant's per-operation model " +
3550
+ "choices and whether each key is configured — `has_api_key` (Anthropic, powers " +
3551
+ "ingest) and `has_embedding_key` (OpenAI embedding, powers semantic search) plus " +
3552
+ "masked last-4 hints. NEVER returns a key itself. Call this BEFORE ingest/search to " +
3553
+ "confirm setup, or after set_llm_config to verify it took. Uses your user-role key " +
3554
+ "(LOOPCTL_USER_KEY, obtained at signup); managing tenant secrets is user-only, so an " +
3555
+ "agent/orchestrator key is rejected.",
3416
3556
  inputSchema: { type: "object", properties: {}, required: [] },
3417
3557
  },
3418
3558
  {
3419
3559
  name: "set_llm_config",
3420
3560
  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).",
3561
+ "FIRST-TIME SETUP do this ONCE, right after signup, to bring the wiki online. " +
3562
+ "Provision your OWN Anthropic + OpenAI embedding keys so knowledge ingest AND " +
3563
+ "semantic search work. loopctl is strictly BYO: it fronts NO LLM cost — your keys " +
3564
+ "bill you directly and are stored ENCRYPTED, never returned. WHY it's required: with " +
3565
+ "no `api_key`, knowledge_ingest returns 422 (code no_api_key); with no " +
3566
+ "`embedding_api_key`, knowledge_search silently degrades to keyword-only " +
3567
+ "(meta.fallback_reason: no_embedding_key) both responses carry a `remediation` " +
3568
+ "pointing back to THIS tool. WHICH key powers WHAT: `api_key` (Anthropic) → ingest " +
3569
+ "extraction / classification / merge synthesis; `embedding_api_key` " +
3570
+ "(OpenAI-compatible) → article embeddings + semantic ranking. All params are optional " +
3571
+ "and partial-merge — omitting a key leaves the existing one untouched, so you can set " +
3572
+ "or rotate one at a time. The per-operation model overrides " +
3573
+ "(extraction_model / classification_model / merge_model / embedding_model) are " +
3574
+ "free-form (any model the key permits) and default server-side when omitted. Typical " +
3575
+ 'onboarding call: set_llm_config({api_key: "sk-ant-...", embedding_api_key: "sk-..."}). ' +
3576
+ "Verify anytime with llm_config (has_api_key / has_embedding_key). REQUIRES your " +
3577
+ "user-role key LOOPCTL_USER_KEY (minted by the human-anchored signup ceremony); if it " +
3578
+ "is unset the tool fails fast telling you to set it.",
3429
3579
  inputSchema: {
3430
3580
  type: "object",
3431
3581
  properties: {
3432
3582
  api_key: {
3433
3583
  type: "string",
3434
- description: "Anthropic API key (write-only; stored encrypted, never returned).",
3584
+ description:
3585
+ "Anthropic API key — powers knowledge ingest (extraction/classification/merge). " +
3586
+ "Write-only; stored encrypted, never returned.",
3435
3587
  },
3436
3588
  extraction_model: {
3437
3589
  type: "string",
@@ -3448,7 +3600,9 @@ const TOOLS = [
3448
3600
  embedding_api_key: {
3449
3601
  type: "string",
3450
3602
  description:
3451
- "OpenAI-compatible embedding API key (write-only; stored encrypted, never returned).",
3603
+ "OpenAI-compatible embedding API key powers article embeddings + semantic " +
3604
+ "search. Write-only; stored encrypted, never returned. Without it, articles are " +
3605
+ "not vector-searchable and search degrades to keyword-only.",
3452
3606
  },
3453
3607
  embedding_model: {
3454
3608
  type: "string",
@@ -3713,6 +3867,31 @@ const TOOLS = [
3713
3867
  },
3714
3868
 
3715
3869
  // Chain of Custody v2 tools
3870
+ {
3871
+ name: "signup",
3872
+ description:
3873
+ "Create a NEW agent-rooted (KB-tier) tenant and mint its one-time root API key — " +
3874
+ "entirely through this call, no human operator and no hardware authenticator required. " +
3875
+ "Public — no existing API key needed to call this tool. The returned tenant can " +
3876
+ "immediately use the FULL knowledge-wiki surface (ingest/search/context/curate, BYO " +
3877
+ "LLM key config, agent registration) but CANNOT perform work-breakdown / " +
3878
+ "chain-of-custody operations (create projects/epics/stories, claim/verify/report, " +
3879
+ "dispatch, etc.) — those require a separate, human-anchored tenant created via the " +
3880
+ "WebAuthn ceremony at https://loopctl.com/signup. Rate-limited per client IP " +
3881
+ "(<= 5 signups/hour). The raw_key in the response is shown ONCE — save it immediately " +
3882
+ "(e.g. as LOOPCTL_USER_KEY) since it cannot be retrieved again. Next steps after " +
3883
+ "signup: configure your BYO LLM keys with set_llm_config, then register an agent " +
3884
+ "identity, then ingest/search the wiki.",
3885
+ inputSchema: {
3886
+ type: "object",
3887
+ properties: {
3888
+ name: { type: "string", description: "Tenant display name.", maxLength: 120 },
3889
+ slug: { type: "string", description: "URL-safe unique tenant slug.", maxLength: 64 },
3890
+ email: { type: "string", description: "Contact email for the tenant." },
3891
+ },
3892
+ required: ["name", "slug", "email"],
3893
+ },
3894
+ },
3716
3895
  {
3717
3896
  name: "get_sth",
3718
3897
  description: "Get the latest Signed Tree Head for a tenant's audit chain. Public — no auth required.",
@@ -4000,6 +4179,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4000
4179
  case "dispatch":
4001
4180
  return await createDispatch(args);
4002
4181
 
4182
+ case "signup":
4183
+ return await signup(args);
4184
+
4003
4185
  case "get_sth":
4004
4186
  return await getSth(args);
4005
4187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.33.1",
3
+ "version": "2.35.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",