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.
- package/README.md +15 -5
- package/index.js +107 -0
- 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
|
|
|
@@ -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.
|
|
88
|
-
|
|
89
|
-
|
|
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 (
|
|
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(/&/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 {
|
|
@@ -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
|
|