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.
- package/README.md +61 -6
- package/index.js +202 -20
- 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 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
|
|
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
|
-
##
|
|
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
|
|
189
|
-
| `set_llm_config` | Set/rotate the tenant's OWN Anthropic
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
250
|
+
.replace(/"/g, '"').replace(/'/g, "'").replace(/ /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
3413
|
-
"
|
|
3414
|
-
"and
|
|
3415
|
-
"
|
|
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
|
-
"
|
|
3422
|
-
"
|
|
3423
|
-
"
|
|
3424
|
-
"
|
|
3425
|
-
"
|
|
3426
|
-
"
|
|
3427
|
-
"
|
|
3428
|
-
"(
|
|
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:
|
|
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
|
|
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
|
|