loopctl-mcp-server 2.34.0 → 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 +15 -5
  2. package/index.js +107 -0
  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
 
@@ -84,9 +84,18 @@ per-operation model overrides (`extraction_model`, `classification_model`,
84
84
 
85
85
  ### The smooth path (once, at onboarding)
86
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.
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).
90
99
  2. **Set the env** in your `.mcp.json` (see [Configuration](#configuration)):
91
100
  `LOOPCTL_SERVER`, `LOOPCTL_USER_KEY` (needed for this step), plus `LOOPCTL_AGENT_KEY`
92
101
  / `LOOPCTL_ORCH_KEY`.
@@ -112,7 +121,7 @@ REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (
112
121
  autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
113
122
  [`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
114
123
 
115
- ## Tools (72)
124
+ ## Tools (73)
116
125
 
117
126
  ### Project Tools
118
127
 
@@ -258,6 +267,7 @@ Key distribution for the dispatch pattern (Epic 26): per-dispatch ephemeral keys
258
267
 
259
268
  | Tool | Description |
260
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`. |
261
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`. |
262
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`. |
263
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 {
@@ -1611,6 +1680,16 @@ async function createDispatch({
1611
1680
  return toContent(result);
1612
1681
  }
1613
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
+
1614
1693
  // US-26: Signed Tree Head retrieval
1615
1694
  async function getSth({ tenant_id }) {
1616
1695
  const result = await apiCall("GET", `/api/v1/audit/sth/${tenant_id}`);
@@ -3788,6 +3867,31 @@ const TOOLS = [
3788
3867
  },
3789
3868
 
3790
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
+ },
3791
3895
  {
3792
3896
  name: "get_sth",
3793
3897
  description: "Get the latest Signed Tree Head for a tenant's audit chain. Public — no auth required.",
@@ -4075,6 +4179,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4075
4179
  case "dispatch":
4076
4180
  return await createDispatch(args);
4077
4181
 
4182
+ case "signup":
4183
+ return await signup(args);
4184
+
4078
4185
  case "get_sth":
4079
4186
  return await getSth(args);
4080
4187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.34.0",
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",