loopctl-mcp-server 2.54.0 → 2.55.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 +13 -2
- package/index.js +104 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,6 +16,14 @@ Or run directly with npx:
|
|
|
16
16
|
npx loopctl-mcp-server
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
### Requirements
|
|
20
|
+
|
|
21
|
+
- **Node.js >= 20.6.0.** The server makes all outbound HTTPS calls through Node's
|
|
22
|
+
global `fetch` (undici). Node 20.6.0 is the first release where that transport
|
|
23
|
+
honors `NODE_EXTRA_CA_CERTS` (the setting used for a custom/self-signed loopctl
|
|
24
|
+
CA -- see Troubleshooting below). On older runtimes that env var is silently
|
|
25
|
+
ignored, so the floor is enforced via `engines` in `package.json`.
|
|
26
|
+
|
|
19
27
|
## Configuration
|
|
20
28
|
|
|
21
29
|
Add to your `.mcp.json` (Claude Code) or equivalent MCP config:
|
|
@@ -121,7 +129,7 @@ REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (
|
|
|
121
129
|
autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
|
|
122
130
|
[`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
|
|
123
131
|
|
|
124
|
-
## Tools
|
|
132
|
+
## Tools
|
|
125
133
|
|
|
126
134
|
> Plus **per-tenant generated Context Retriever tools** (`cr_*`) appended
|
|
127
135
|
> dynamically at runtime — see [Dynamic per-tenant Context Retriever
|
|
@@ -324,6 +332,8 @@ nothing changes until a scope opts in.
|
|
|
324
332
|
| `declare_trusted_endpoint` | Declare a host you attest is YOUR OWN so a `local_only` scope can reach it. **This is an unverified tenant attestation, not network locality** — labelled as such everywhere. Three enforced constraints: PUBLIC addresses only (write time AND pin time), PURPOSE-SCOPED (`inference` / `webhook` / `ingest`), vendor hosts excluded. Carves nothing out of the SSRF denylist. Requires **user** key. |
|
|
325
333
|
| `revoke_trusted_endpoint` | Revoke a declaration. Invalidation is IMMEDIATE and cluster-wide — a revoked declaration does not keep working for the remainder of the pin TTL. Requires **user** key. |
|
|
326
334
|
| `egress_repin` | Recover from a `:pin_stale` error (your box got a new DHCP lease and the pinned address set changed — DISTINCT from `egress_blocked`). Re-resolves and re-pins the host. **Agent** key by design: requiring a human user-role write to recover would contradict loopctl's agent-native, no-UI design. |
|
|
335
|
+
| `custody_claim` | The recorded **egress custody claim** for one article or memory row: the append-only sequence of per-operation postures (create, each embed, each re-embed, each classification, each merge) with the endpoints resolved for THAT operation and their verdicts, plus the aggregate. Rides the existing hash-chained audit log + signed tree heads — each entry carries a `chain_position` and the leaf's `chain_entry_hash`, so `GET /api/v1/audit/sth/{tenant_id}/inclusion/{position}` proves inclusion of *that* leaf, and the leaf's payload names this row by a recomputable `posture_digest`. THREE states, only one an attestation: `no_claim_recorded`, `claim_pending`, `claim_recorded` (`complete` / `partial_history` / `incomplete`). Completeness is measured against a persisted per-row high-water mark, so losing the tail of a sequence is a gap, not a clean claim. `third_party_egress_on_covered_paths` is `false` only for NETWORK-local endpoints; a tenant-declared (unverified) endpoint yields `"tenant_declared_unverified"`. Attests ONLY to the endpoints loopctl called on the paths in `coverage` — never to what those endpoints did afterwards. **Agent** key. |
|
|
336
|
+
| `custody_failures` | Custody posture entries whose chain append was DROPPED after exhausting retries, plus `stale_pending` entries stranded by a flush that died outside its own final-attempt branch. Surfaced rather than silently absent: each `data` entry degrades its row's claim to `incomplete`, and a stranded entry would otherwise read as an in-flight claim forever. **Agent** key. |
|
|
327
337
|
|
|
328
338
|
### Discovery Tools
|
|
329
339
|
|
|
@@ -519,7 +529,8 @@ clean one-time bootstrap (no cross-key collision).
|
|
|
519
529
|
|
|
520
530
|
- Verify `LOOPCTL_SERVER` is set and reachable
|
|
521
531
|
- Check that the server URL includes the protocol (`https://`)
|
|
522
|
-
- If
|
|
532
|
+
- If your loopctl server uses a custom or self-signed CA, set `NODE_EXTRA_CA_CERTS=/path/to/ca.pem` in your environment so Node trusts that CA while keeping TLS certificate verification enabled. This requires **Node >= 20.6.0** (or the >= 18.19.0 backport): this server issues all requests through Node's global `fetch` (undici), which only began honoring `NODE_EXTRA_CA_CERTS` in those releases. On older runtimes the variable is silently ignored and you will still see `unable to verify the first certificate` / self-signed-cert errors -- upgrade Node rather than disabling verification.
|
|
533
|
+
- Do NOT set `NODE_TLS_REJECT_UNAUTHORIZED=0` -- it disables certificate verification for the entire Node process (all outbound TLS), exposing every connection to MITM, not just the loopctl one
|
|
523
534
|
|
|
524
535
|
### Authentication errors (401)
|
|
525
536
|
|
package/index.js
CHANGED
|
@@ -1902,6 +1902,33 @@ async function egressPosture() {
|
|
|
1902
1902
|
return toContent(result);
|
|
1903
1903
|
}
|
|
1904
1904
|
|
|
1905
|
+
// --- US-41.7: witnessed custody claim ---------------------------------------
|
|
1906
|
+
//
|
|
1907
|
+
// A READ, at agent role for the same reason egress_posture is: verifying a
|
|
1908
|
+
// harvest AFTER the fact must work with the key an agent already holds.
|
|
1909
|
+
async function custodyClaim({ subject_type, subject_id } = {}) {
|
|
1910
|
+
if (!subject_type || !subject_id) {
|
|
1911
|
+
throw new Error("subject_type and subject_id are required");
|
|
1912
|
+
}
|
|
1913
|
+
const result = await apiCall(
|
|
1914
|
+
"GET",
|
|
1915
|
+
`/api/v1/custody/claims/${encodeURIComponent(subject_type)}/${encodeURIComponent(subject_id)}`,
|
|
1916
|
+
null,
|
|
1917
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1918
|
+
);
|
|
1919
|
+
return toContent(result);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
async function custodyFailures() {
|
|
1923
|
+
const result = await apiCall(
|
|
1924
|
+
"GET",
|
|
1925
|
+
"/api/v1/custody/failures",
|
|
1926
|
+
null,
|
|
1927
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1928
|
+
);
|
|
1929
|
+
return toContent(result);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1905
1932
|
async function setLocalOnly({ project_id, acknowledge } = {}) {
|
|
1906
1933
|
const body = {};
|
|
1907
1934
|
if (project_id != null) body.project_id = project_id;
|
|
@@ -4962,7 +4989,8 @@ const TOOLS = [
|
|
|
4962
4989
|
name: "egress_posture",
|
|
4963
4990
|
description:
|
|
4964
4991
|
"VERIFY BEFORE YOU HARVEST. Reports this instance's egress posture for YOUR tenant: " +
|
|
4965
|
-
"the resolved embedding and chat endpoints,
|
|
4992
|
+
"the resolved embedding and chat endpoints, EVERY webhook destination " +
|
|
4993
|
+
"(webhook_destinations), a locality VERDICT for each " +
|
|
4966
4994
|
"(network-local / 'tenant-declared (unverified attestation), not network-local' / " +
|
|
4967
4995
|
"non-local), your declared trusted endpoints with their purposes, per-scope " +
|
|
4968
4996
|
"local_only status, and any named posture defects. Endpoints are shown; KEYS NEVER " +
|
|
@@ -4971,10 +4999,70 @@ const TOOLS = [
|
|
|
4971
4999
|
"key you already have. NOTE the deployment allowlist CONTENTS are operator " +
|
|
4972
5000
|
"infrastructure and are NOT disclosed at agent role: you get only a boolean per " +
|
|
4973
5001
|
"endpoint saying whether its verdict came from the allowlist (contents at user+). " +
|
|
5002
|
+
"Webhook destinations follow the same split: HOST plus verdict plus " +
|
|
5003
|
+
"blocked_by_local_only at agent role, the FULL destination URL (endpoint) only at " +
|
|
5004
|
+
"user+ — a webhook path is frequently the credential. " +
|
|
4974
5005
|
"SCOPE OF THE GUARANTEE: fail-closed enforcement covers every outbound HTTP call " +
|
|
4975
|
-
"made by loopctl application code on
|
|
4976
|
-
"
|
|
4977
|
-
"separate mcp-server codebase, are outside the static
|
|
5006
|
+
"made by loopctl application code on every CONTENT-CARRYING path — model-provider " +
|
|
5007
|
+
"calls, the ingestion fetch, and webhook delivery (US-41.5). HTTP performed inside " +
|
|
5008
|
+
"a dependency, plus this separate mcp-server codebase, are outside the static " +
|
|
5009
|
+
"chokepoint check; the remaining non-content outbound paths are triaged in " +
|
|
5010
|
+
"docs/egress-guard.md.",
|
|
5011
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
5012
|
+
},
|
|
5013
|
+
{
|
|
5014
|
+
name: "custody_claim",
|
|
5015
|
+
description:
|
|
5016
|
+
"The recorded EGRESS CUSTODY CLAIM for one article or memory row: the append-only " +
|
|
5017
|
+
"sequence of per-operation postures (create, each embedding, each re-embed, each " +
|
|
5018
|
+
"classification/merge) with the endpoint loopctl resolved for THAT operation and its " +
|
|
5019
|
+
"locality verdict, plus the aggregate over them. Each claim rides the existing " +
|
|
5020
|
+
"hash-chained audit log and its signed tree heads — every recorded entry carries the " +
|
|
5021
|
+
"chain_position you can fetch an inclusion proof for at " +
|
|
5022
|
+
"GET /api/v1/audit/sth/{tenant_id}/inclusion/{position} and check against the public " +
|
|
5023
|
+
"STH with the tenant's published audit key. THREE STATES, and only one of them is an " +
|
|
5024
|
+
"attestation: 'no_claim_recorded' (no operation sequence was ever assigned — the row " +
|
|
5025
|
+
"predates recording or its scope is not marked local_only; this asserts NOTHING in " +
|
|
5026
|
+
"either direction), 'claim_pending' (sequences assigned, batch append not yet " +
|
|
5027
|
+
"flushed), and 'claim_recorded', itself 'complete', 'partial_history' or " +
|
|
5028
|
+
"'incomplete'. An INCOMPLETE sequence (a gap, a lost tail, or a dropped append) is " +
|
|
5029
|
+
"never reported as no-third-party-egress: an unrecorded operation may have called " +
|
|
5030
|
+
"any endpoint. Completeness is measured against a PERSISTED per-row high-water mark, " +
|
|
5031
|
+
"not against the rows that happen to survive, so truncating the sequence cannot " +
|
|
5032
|
+
"restore a satisfied claim. 'partial_history' means operation 0 is not this row's " +
|
|
5033
|
+
"creation — recording began after the row already existed (typically the scope was " +
|
|
5034
|
+
"marked local_only later), so loopctl has no record of how it was produced. " +
|
|
5035
|
+
"third_party_egress_on_covered_paths is `false` ONLY when every recorded endpoint " +
|
|
5036
|
+
"was NETWORK-LOCAL; when the sequence leans on a TENANT-DECLARED endpoint (a public " +
|
|
5037
|
+
"host the tenant merely attested is its own, which loopctl never verified) the value " +
|
|
5038
|
+
"is the string 'tenant_declared_unverified', not false. " +
|
|
5039
|
+
"SCOPE, precisely: the claim attests ONLY to the endpoints loopctl called for the " +
|
|
5040
|
+
"recorded operations on this row, on the egress paths enumerated in the `coverage` " +
|
|
5041
|
+
"field. It makes NO statement about what those endpoints did with the data " +
|
|
5042
|
+
"afterwards, and none about a path listed as uncovered. READ tool, AGENT role.",
|
|
5043
|
+
inputSchema: {
|
|
5044
|
+
type: "object",
|
|
5045
|
+
properties: {
|
|
5046
|
+
subject_type: {
|
|
5047
|
+
type: "string",
|
|
5048
|
+
enum: ["article", "memory"],
|
|
5049
|
+
description: "The kind of row the claim is bound to.",
|
|
5050
|
+
},
|
|
5051
|
+
subject_id: { type: "string", description: "The row's UUID." },
|
|
5052
|
+
},
|
|
5053
|
+
required: ["subject_type", "subject_id"],
|
|
5054
|
+
},
|
|
5055
|
+
},
|
|
5056
|
+
{
|
|
5057
|
+
name: "custody_failures",
|
|
5058
|
+
description:
|
|
5059
|
+
"Custody posture entries whose audit-chain append was DROPPED after exhausting " +
|
|
5060
|
+
"retries, plus (under `stale_pending`) entries that have been in flight longer than " +
|
|
5061
|
+
"the stale window. A recording failure is surfaced here rather than silently absent, " +
|
|
5062
|
+
"because a missing claim must never read as a satisfied one: every entry under " +
|
|
5063
|
+
"`data` degrades its row's claim to 'incomplete', and a stranded `stale_pending` " +
|
|
5064
|
+
"entry is one a flush stamped and then died on — it would otherwise read as an " +
|
|
5065
|
+
"in-flight claim indefinitely. READ tool, AGENT role.",
|
|
4978
5066
|
inputSchema: { type: "object", properties: {}, required: [] },
|
|
4979
5067
|
},
|
|
4980
5068
|
{
|
|
@@ -4985,10 +5073,13 @@ const TOOLS = [
|
|
|
4985
5073
|
"everywhere; nothing changes until a scope opts in. Scope resolution is " +
|
|
4986
5074
|
"MOST-RESTRICTIVE-WINS (project OR tenant) and a project can NEVER relax a tenant " +
|
|
4987
5075
|
"marking. MANDATORY PRE-FLIGHT: the call is REFUSED with 409 would_block_endpoints, " +
|
|
4988
|
-
"naming every endpoint that would become egress_blocked
|
|
5076
|
+
"naming every endpoint that would become egress_blocked — including every WEBHOOK " +
|
|
5077
|
+
"SUBSCRIPTION whose destination would be refused (kind: 'webhook', US-41.5) — " +
|
|
5078
|
+
"unless you pass " +
|
|
4989
5079
|
"acknowledge: true — because on a tenant still using vendor default endpoints this " +
|
|
4990
5080
|
"instantly stops embedding, extraction, classification and merge, and only a " +
|
|
4991
|
-
"human user-role key can undo it.
|
|
5081
|
+
"human user-role key can undo it. Subscriptions are never silently disabled: they " +
|
|
5082
|
+
"are either the reason for the refusal, or reported back to you on acknowledgement. Requires an ORCHESTRATOR key: tightening is safe " +
|
|
4992
5083
|
"to automate. CLEARING is a different tool (clear_local_only) and is user-only.",
|
|
4993
5084
|
inputSchema: {
|
|
4994
5085
|
type: "object",
|
|
@@ -5975,6 +6066,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5975
6066
|
case "egress_posture":
|
|
5976
6067
|
return await egressPosture();
|
|
5977
6068
|
|
|
6069
|
+
// US-41.7 — witnessed custody claim
|
|
6070
|
+
case "custody_claim":
|
|
6071
|
+
return await custodyClaim(args);
|
|
6072
|
+
|
|
6073
|
+
case "custody_failures":
|
|
6074
|
+
return await custodyFailures();
|
|
6075
|
+
|
|
5978
6076
|
case "set_local_only":
|
|
5979
6077
|
return await setLocalOnly(args);
|
|
5980
6078
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopctl-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.55.0",
|
|
4
4
|
"description": "MCP server for loopctl \u2014 structural trust for AI development loops",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"exports": "./index.js",
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
33
|
+
"node": ">=20.6.0"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"index.js",
|