loopctl-mcp-server 2.52.0 → 2.54.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 +16 -2
  2. package/index.js +309 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -121,7 +121,7 @@ REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (
121
121
  autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
122
122
  [`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
123
123
 
124
- ## Tools (96)
124
+ ## Tools (109)
125
125
 
126
126
  > Plus **per-tenant generated Context Retriever tools** (`cr_*`) appended
127
127
  > dynamically at runtime — see [Dynamic per-tenant Context Retriever
@@ -297,7 +297,7 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
297
297
  | Tool | Description |
298
298
  |---|---|
299
299
  | `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. |
300
- | `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. |
300
+ | `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). **Private tier (US-41.3):** also sets `chat_provider` (`anthropic` | `openai_compatible`), `chat_base_url`, `extraction_model` (required on that provider) and `chat_api_key` (optional — omit it for a keyless local server) so ingest extraction / classification / merge / memory promotion run against YOUR OWN OpenAI-compatible endpoint and document text never leaves your boundary. Every model the config resolves to is PROBED with a trivial completion before it is saved (a 422 persists nothing); the probe re-runs on any chat-surface change (provider, base_url, key rotation, model), a private-range host is refused by the SSRF guard, and moving to a NEW host needs a matching `chat_api_key` or `acknowledge_key_transmission: true`. See [First-time setup](#first-time-setup--provision-your-byo-llm-keys). Requires **user** key. |
301
301
  | `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. |
302
302
 
303
303
  ### Knowledge Analytics Tools (orchestrator key)
@@ -311,6 +311,20 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
311
311
  | `knowledge_curation_log` | Concise human-readable log of KB CURATION adjustments — novelty-gate decisions (`gate_duplicate`/`gate_draft`) and conflict resolutions (`supersede`/`merge`/`dismiss`) — for analyzing the agents'-KB rollout, distinct from the verbose audit log. Each entry: `{at, kind, summary, refs, actor, confidence}`. **RECORDED ONLY when `settings.kb_curation_log` is on** (PATCH `/api/v1/admin/tenants/:id` with `settings:{kb_curation_log:true}`); off by default = no rows. Most recent first. Requires orchestrator role. Optional: `kind`, `since` (ISO8601), `limit` (default 50, max 500), `offset`. |
312
312
  | `knowledge_retrieval_metrics` | Daily retrieval-PRECISION time series: for each day, the share of search results the agent then opened (search → get/context within a window). A proxy for whether retrieval is improving as the corpus is de-duplicated, better navigated (MOCs), and conflict-resolved. Most recent day first. Requires orchestrator role. Optional: `limit` (default 30, max 365), `offset`. |
313
313
 
314
+ ### Egress / Privacy Tools (US-41.4)
315
+
316
+ The fail-closed no-egress guard. `local_only` is **OFF by default everywhere** —
317
+ nothing changes until a scope opts in.
318
+
319
+ | Tool | Description |
320
+ |---|---|
321
+ | `egress_posture` | **VERIFY BEFORE YOU HARVEST.** This instance's egress posture for your tenant: the resolved embedding + chat endpoints with a locality VERDICT for each (`network-local` / `tenant-declared (unverified attestation), not network-local` / `non-local`), your declared trusted endpoints and their purposes, per-scope `local_only` / `encrypt_body`, and any named posture defects. Endpoints are shown; **keys never are**. Deployment-allowlist CONTENTS appear only at **user**+ — at agent role each endpoint carries only a boolean saying whether its verdict came from the allowlist. Agent key. |
322
+ | `set_local_only` | **TIGHTEN**: mark a scope `local_only`, so loopctl HARD-REFUSES any model-provider call whose resolved endpoint is not classified local. Scope resolution is MOST-RESTRICTIVE-WINS (project OR tenant); a project can never relax a tenant marking. MANDATORY PRE-FLIGHT: refused with **409** `would_block_endpoints` naming every endpoint that would become `egress_blocked`, unless you pass `acknowledge: true`. Requires **orchestrator** key — tightening is safe to automate. |
323
+ | `clear_local_only` | **WIDEN**: remove a scope's `local_only` marking. Requires your **user** key (`LOOPCTL_USER_KEY`) — deliberately not available to an agent or orchestrator, because clearing is the self-widening move an automated key must never make one call before a harvest. Audited with actor + scope. |
324
+ | `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
+ | `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
+ | `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. |
327
+
314
328
  ### Discovery Tools
315
329
 
316
330
  | Tool | Description |
package/index.js CHANGED
@@ -1852,6 +1852,10 @@ async function setLlmConfig({
1852
1852
  merge_model,
1853
1853
  embedding_api_key,
1854
1854
  embedding_model,
1855
+ chat_provider,
1856
+ chat_base_url,
1857
+ chat_api_key,
1858
+ acknowledge_key_transmission,
1855
1859
  }) {
1856
1860
  const body = {};
1857
1861
  if (api_key != null) body.api_key = api_key;
@@ -1860,6 +1864,15 @@ async function setLlmConfig({
1860
1864
  if (merge_model !== undefined) body.merge_model = merge_model;
1861
1865
  if (embedding_api_key != null) body.embedding_api_key = embedding_api_key;
1862
1866
  if (embedding_model !== undefined) body.embedding_model = embedding_model;
1867
+ // US-41.3: pluggable OpenAI-compatible chat endpoint. The server PROBES the
1868
+ // endpoint with a trivial completion before persisting, and refuses an endpoint
1869
+ // change that carries neither a matching chat_api_key nor an explicit
1870
+ // acknowledge_key_transmission.
1871
+ if (chat_provider !== undefined) body.chat_provider = chat_provider;
1872
+ if (chat_base_url !== undefined) body.chat_base_url = chat_base_url;
1873
+ if (chat_api_key != null) body.chat_api_key = chat_api_key;
1874
+ if (acknowledge_key_transmission === true)
1875
+ body.acknowledge_key_transmission = true;
1863
1876
  // PATCH (partial-merge) + EXACT user key (review #12, #13).
1864
1877
  const result = await apiCall(
1865
1878
  "PATCH",
@@ -1871,6 +1884,88 @@ async function setLlmConfig({
1871
1884
  return toContent(result);
1872
1885
  }
1873
1886
 
1887
+ // --- US-41.4: fail-closed no-egress guard -----------------------------------
1888
+ //
1889
+ // egress_posture is a READ available at AGENT role: an agent must be able to
1890
+ // verify locality with the key it already has, or the whole verify-before-harvest
1891
+ // workflow fails. WRITING local_only is asymmetric: ENABLE (tightening) is
1892
+ // orchestrator+, CLEAR (widening) is user-only, and declaring a trusted endpoint
1893
+ // is user-only. Re-pinning is agent-role on purpose — a home Ollama box or a DHCP
1894
+ // VPS changes IP routinely and recovery must not need a human.
1895
+ async function egressPosture() {
1896
+ const result = await apiCall(
1897
+ "GET",
1898
+ "/api/v1/egress/posture",
1899
+ null,
1900
+ process.env.LOOPCTL_AGENT_KEY,
1901
+ );
1902
+ return toContent(result);
1903
+ }
1904
+
1905
+ async function setLocalOnly({ project_id, acknowledge } = {}) {
1906
+ const body = {};
1907
+ if (project_id != null) body.project_id = project_id;
1908
+ if (acknowledge === true) body.acknowledge = true;
1909
+ // ENABLE = tightening = orchestrator+.
1910
+ const result = await apiCall(
1911
+ "POST",
1912
+ "/api/v1/egress/local-only",
1913
+ body,
1914
+ process.env.LOOPCTL_ORCH_KEY,
1915
+ );
1916
+ return toContent(result);
1917
+ }
1918
+
1919
+ async function clearLocalOnly({ project_id } = {}) {
1920
+ // CLEAR = self-widening = EXACT user key. An agent must never be able to
1921
+ // re-open egress one tool call before a harvest.
1922
+ const qs = project_id ? `?project_id=${encodeURIComponent(project_id)}` : "";
1923
+ const result = await apiCall(
1924
+ "DELETE",
1925
+ `/api/v1/egress/local-only${qs}`,
1926
+ null,
1927
+ process.env.LOOPCTL_USER_KEY,
1928
+ { exactKey: true },
1929
+ );
1930
+ return toContent(result);
1931
+ }
1932
+
1933
+ async function declareTrustedEndpoint({ host, purposes, note } = {}) {
1934
+ const body = { host, purposes };
1935
+ if (note != null) body.note = note;
1936
+ const result = await apiCall(
1937
+ "POST",
1938
+ "/api/v1/egress/trusted-endpoints",
1939
+ body,
1940
+ process.env.LOOPCTL_USER_KEY,
1941
+ { exactKey: true },
1942
+ );
1943
+ return toContent(result);
1944
+ }
1945
+
1946
+ async function revokeTrustedEndpoint({ host } = {}) {
1947
+ const result = await apiCall(
1948
+ "DELETE",
1949
+ `/api/v1/egress/trusted-endpoints/${encodeURIComponent(host)}`,
1950
+ null,
1951
+ process.env.LOOPCTL_USER_KEY,
1952
+ { exactKey: true },
1953
+ );
1954
+ return toContent(result);
1955
+ }
1956
+
1957
+ async function egressRepin({ host, project_id } = {}) {
1958
+ const body = { host };
1959
+ if (project_id != null) body.project_id = project_id;
1960
+ const result = await apiCall(
1961
+ "POST",
1962
+ "/api/v1/egress/repin",
1963
+ body,
1964
+ process.env.LOOPCTL_AGENT_KEY,
1965
+ );
1966
+ return toContent(result);
1967
+ }
1968
+
1874
1969
  async function knowledgeLlmUsage(args = {}) {
1875
1970
  // Query-string building lives in lib/http-helpers.js so the test suite exercises
1876
1971
  // the same from/to/limit/offset logic the server ships.
@@ -4863,6 +4958,145 @@ const TOOLS = [
4863
4958
  },
4864
4959
 
4865
4960
  // Per-tenant BYO LLM config + usage (Epic 28 residual, #179)
4961
+ {
4962
+ name: "egress_posture",
4963
+ description:
4964
+ "VERIFY BEFORE YOU HARVEST. Reports this instance's egress posture for YOUR tenant: " +
4965
+ "the resolved embedding and chat endpoints, a locality VERDICT for each " +
4966
+ "(network-local / 'tenant-declared (unverified attestation), not network-local' / " +
4967
+ "non-local), your declared trusted endpoints with their purposes, per-scope " +
4968
+ "local_only status, and any named posture defects. Endpoints are shown; KEYS NEVER " +
4969
+ "ARE. Call this BEFORE sending private documents anywhere, instead of trusting that " +
4970
+ "the operator configured things correctly. READ tool, available at AGENT role — the " +
4971
+ "key you already have. NOTE the deployment allowlist CONTENTS are operator " +
4972
+ "infrastructure and are NOT disclosed at agent role: you get only a boolean per " +
4973
+ "endpoint saying whether its verdict came from the allowlist (contents at user+). " +
4974
+ "SCOPE OF THE GUARANTEE: fail-closed enforcement covers every outbound HTTP call " +
4975
+ "made by loopctl application code on the MODEL-PROVIDER path. Webhook delivery is " +
4976
+ "not covered yet (US-41.5), and HTTP performed inside a dependency, plus this " +
4977
+ "separate mcp-server codebase, are outside the static chokepoint check.",
4978
+ inputSchema: { type: "object", properties: {}, required: [] },
4979
+ },
4980
+ {
4981
+ name: "set_local_only",
4982
+ description:
4983
+ "TIGHTEN the posture: mark a scope local_only so loopctl HARD-REFUSES any " +
4984
+ "model-provider call whose resolved endpoint is not classified local. Default is OFF " +
4985
+ "everywhere; nothing changes until a scope opts in. Scope resolution is " +
4986
+ "MOST-RESTRICTIVE-WINS (project OR tenant) and a project can NEVER relax a tenant " +
4987
+ "marking. MANDATORY PRE-FLIGHT: the call is REFUSED with 409 would_block_endpoints, " +
4988
+ "naming every endpoint that would become egress_blocked, unless you pass " +
4989
+ "acknowledge: true — because on a tenant still using vendor default endpoints this " +
4990
+ "instantly stops embedding, extraction, classification and merge, and only a " +
4991
+ "human user-role key can undo it. Requires an ORCHESTRATOR key: tightening is safe " +
4992
+ "to automate. CLEARING is a different tool (clear_local_only) and is user-only.",
4993
+ inputSchema: {
4994
+ type: "object",
4995
+ properties: {
4996
+ project_id: {
4997
+ type: "string",
4998
+ description:
4999
+ "Mark this PROJECT only (omit to mark the whole tenant). A project-less row — " +
5000
+ "a tenant-wide article, any memory — always follows the TENANT marking.",
5001
+ },
5002
+ acknowledge: {
5003
+ type: "boolean",
5004
+ description:
5005
+ "Accept the reported blocked posture and proceed. Required when the pre-flight " +
5006
+ "finds endpoints that would become egress_blocked.",
5007
+ },
5008
+ },
5009
+ required: [],
5010
+ },
5011
+ },
5012
+ {
5013
+ name: "clear_local_only",
5014
+ description:
5015
+ "WIDEN the posture: remove a scope's local_only marking, re-permitting non-local " +
5016
+ "model-provider egress for it. Requires your EXACT user-role key " +
5017
+ "(LOOPCTL_USER_KEY) — deliberately NOT available to an agent or an orchestrator, " +
5018
+ "because clearing is the self-widening move that would otherwise let an automated " +
5019
+ "key re-open egress immediately before a harvest. Audited with actor and scope.",
5020
+ inputSchema: {
5021
+ type: "object",
5022
+ properties: {
5023
+ project_id: {
5024
+ type: "string",
5025
+ description: "Clear this PROJECT's marking (omit to clear the tenant marking).",
5026
+ },
5027
+ },
5028
+ required: [],
5029
+ },
5030
+ },
5031
+ {
5032
+ name: "declare_trusted_endpoint",
5033
+ description:
5034
+ "Declare a host you attest is YOUR OWN (your VPS, tailscale funnel, or domain) so a " +
5035
+ "local_only scope can reach it. THIS IS AN UNVERIFIED TENANT ATTESTATION, NOT " +
5036
+ "NETWORK LOCALITY: loopctl does not prove you own the host, and the posture report " +
5037
+ "and custody claim label it 'tenant-declared (unverified attestation), not " +
5038
+ "network-local' — never as network-local. THREE ENFORCED CONSTRAINTS: (1) PUBLIC " +
5039
+ "ADDRESSES ONLY — a host that resolves to loopback, 0/8, 10/8, 127/8, 169.254/16, " +
5040
+ "172.16-31, 192.168/16, 100.64/10 or fdaa::/16 is REJECTED at write time and again " +
5041
+ "at pin time, whether given literally or via a public hostname that resolves there; " +
5042
+ "private-range carve-outs are available ONLY through the operator-controlled " +
5043
+ "deployment allowlist, which no role can write. (2) PURPOSE-SCOPED — a host declared " +
5044
+ "for inference does NOT authorize webhook POSTs of your content to it, nor loopctl " +
5045
+ "FETCHING tenant-supplied URLs from it (purpose 'ingest'). (3) VENDOR " +
5046
+ "HOSTS EXCLUDED (api.openai.com, api.anthropic.com). Requires your EXACT user-role " +
5047
+ "key.",
5048
+ inputSchema: {
5049
+ type: "object",
5050
+ properties: {
5051
+ host: {
5052
+ type: "string",
5053
+ description:
5054
+ "The PUBLIC hostname (or a full URL — only the authority is kept), e.g. " +
5055
+ "'ollama.example.com'.",
5056
+ },
5057
+ purposes: {
5058
+ type: "array",
5059
+ items: { type: "string", enum: ["inference", "webhook", "ingest"] },
5060
+ description:
5061
+ "What this declaration authorizes. At least one. Declarations are honoured " +
5062
+ "ONLY for their declared purposes.",
5063
+ },
5064
+ note: { type: "string", description: "Free-form operator note." },
5065
+ },
5066
+ required: ["host", "purposes"],
5067
+ },
5068
+ },
5069
+ {
5070
+ name: "revoke_trusted_endpoint",
5071
+ description:
5072
+ "Revoke a tenant-declared trusted endpoint. Invalidation is IMMEDIATE — the " +
5073
+ "declaration does not keep working for the remainder of the pin TTL. Requires your " +
5074
+ "EXACT user-role key.",
5075
+ inputSchema: {
5076
+ type: "object",
5077
+ properties: { host: { type: "string", description: "The declared host to revoke." } },
5078
+ required: ["host"],
5079
+ },
5080
+ },
5081
+ {
5082
+ name: "egress_repin",
5083
+ description:
5084
+ "Recover from a :pin_stale error. loopctl pins the IP set it classified so the " +
5085
+ "address it connects to is the address it vetted (closing DNS rebinding). When your " +
5086
+ "box gets a new lease and the address set changes, calls return the DISTINCT " +
5087
+ ":pin_stale error — NOT egress_blocked — and this tool re-resolves and re-pins the " +
5088
+ "host. Available at AGENT role BY DESIGN: home Ollama boxes, tailscale funnels and " +
5089
+ "DHCP VPSes change IP routinely, and requiring a human user-role write to recover " +
5090
+ "would contradict loopctl's agent-native, no-UI design.",
5091
+ inputSchema: {
5092
+ type: "object",
5093
+ properties: {
5094
+ host: { type: "string", description: "The host to re-pin." },
5095
+ project_id: { type: "string", description: "Scope the re-pin to a project." },
5096
+ },
5097
+ required: ["host"],
5098
+ },
5099
+ },
4866
5100
  {
4867
5101
  name: "llm_config",
4868
5102
  description:
@@ -4893,7 +5127,11 @@ const TOOLS = [
4893
5127
  "(extraction_model / classification_model / merge_model / embedding_model) are " +
4894
5128
  "free-form (any model the key permits) and default server-side when omitted. Typical " +
4895
5129
  'onboarding call: set_llm_config({api_key: "sk-ant-...", embedding_api_key: "sk-..."}). ' +
4896
- "Verify anytime with llm_config (has_api_key / has_embedding_key). REQUIRES your " +
5130
+ "PRIVATE TIER: set chat_provider 'openai_compatible' + chat_base_url + " +
5131
+ "extraction_model (+ chat_api_key unless your server is keyless) to run " +
5132
+ "extraction/classification/merge against YOUR OWN endpoint so document text " +
5133
+ "never leaves your boundary; every model it resolves to is probed before it is saved. " +
5134
+ "Verify anytime with llm_config (has_api_key / has_embedding_key / chat_provider). REQUIRES your " +
4897
5135
  "user-role key LOOPCTL_USER_KEY (minted by the human-anchored signup ceremony); if it " +
4898
5136
  "is unset the tool fails fast telling you to set it.",
4899
5137
  inputSchema: {
@@ -4907,7 +5145,14 @@ const TOOLS = [
4907
5145
  },
4908
5146
  extraction_model: {
4909
5147
  type: "string",
4910
- description: "Model id for knowledge extraction (null → server default).",
5148
+ description:
5149
+ "Model id for knowledge extraction (null → server default). REQUIRED with " +
5150
+ "chat_provider 'openai_compatible': the server default is an Anthropic model " +
5151
+ "id your endpoint cannot serve, so there is no safe fallback. It is also the " +
5152
+ "fallback for classification_model / merge_model on that provider. " +
5153
+ "HuggingFace repo ids are valid (e.g. 'meta-llama/Meta-Llama-3-8B-Instruct') " +
5154
+ "— it is sent verbatim as the OpenAI `model` field, so it must match the name " +
5155
+ "your server actually serves.",
4911
5156
  },
4912
5157
  classification_model: {
4913
5158
  type: "string",
@@ -4929,6 +5174,48 @@ const TOOLS = [
4929
5174
  description:
4930
5175
  "Embedding model id (null → server default text-embedding-3-small).",
4931
5176
  },
5177
+ chat_provider: {
5178
+ type: "string",
5179
+ enum: ["anthropic", "openai_compatible"],
5180
+ description:
5181
+ "Which provider serves the CHAT surface (ingest extraction, classification, " +
5182
+ "merge synthesis, memory promotion). Omit or 'anthropic' keeps the hardcoded " +
5183
+ "Anthropic endpoint and identical behaviour. 'openai_compatible' routes that " +
5184
+ "surface — the largest and most sensitive payload in the pipeline, your " +
5185
+ "documents' full text — to YOUR OWN endpoint instead.",
5186
+ },
5187
+ chat_base_url: {
5188
+ type: "string",
5189
+ description:
5190
+ "API base of your OpenAI-compatible server, e.g. " +
5191
+ "'https://llm.example.internal/v1' (the client appends /chat/completions). " +
5192
+ "Required with chat_provider 'openai_compatible'. PROBED with a trivial " +
5193
+ "completion per resolved model BEFORE it is saved: an unreachable host, a " +
5194
+ "rejected credential or a non-OpenAI-compatible response is a 422 and NOTHING " +
5195
+ "is persisted. A host resolving into a private/loopback/link-local range is " +
5196
+ "refused outright unless the OPERATOR allowlisted it (SSRF guard). Must be a " +
5197
+ "BARE base: no query string, no fragment, no user:pass@ credentials (this " +
5198
+ "column is NOT encrypted — anything in the URL is stored and echoed back). " +
5199
+ "Plaintext http is accepted ONLY for a host the egress policy classifies " +
5200
+ "network-local; a public http:// endpoint is refused because the request " +
5201
+ "carries your key and your documents' full text in cleartext.",
5202
+ },
5203
+ chat_api_key: {
5204
+ type: "string",
5205
+ description:
5206
+ "Credential for chat_base_url. Write-only; stored encrypted, never returned. " +
5207
+ "SEPARATE from api_key — your Anthropic key is never sent to your endpoint. " +
5208
+ "OPTIONAL: a local server that serves /chat/completions with no auth is " +
5209
+ "configured by omitting this, and no authorization header is then sent.",
5210
+ },
5211
+ acknowledge_key_transmission: {
5212
+ type: "boolean",
5213
+ description:
5214
+ "Required when CHANGING chat_base_url without supplying a matching " +
5215
+ "chat_api_key: explicitly acknowledges that the already-stored key will be " +
5216
+ "transmitted to the new host. The probe never ships an existing credential to " +
5217
+ "a new host silently. Not persisted.",
5218
+ },
4932
5219
  },
4933
5220
  required: [],
4934
5221
  },
@@ -4936,7 +5223,7 @@ const TOOLS = [
4936
5223
  {
4937
5224
  name: "knowledge_llm_usage",
4938
5225
  description:
4939
- "Per-tenant LLM token-usage summary, grouped by operation + model + source_type + " +
5226
+ "Per-tenant LLM token-usage summary, grouped by operation + model + provider + source_type + " +
4940
5227
  "day over an optional date range, newest day first, with offset/limit pagination " +
4941
5228
  "over meta.total_count. When `from` is omitted it defaults to a 90-day lookback; the " +
4942
5229
  "EFFECTIVE window is echoed in meta.from/meta.to so you can detect that older usage " +
@@ -5684,6 +5971,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5684
5971
  case "knowledge_ingestion_jobs":
5685
5972
  return await knowledgeIngestionJobs(args);
5686
5973
 
5974
+ // US-41.4 — fail-closed no-egress guard
5975
+ case "egress_posture":
5976
+ return await egressPosture();
5977
+
5978
+ case "set_local_only":
5979
+ return await setLocalOnly(args);
5980
+
5981
+ case "clear_local_only":
5982
+ return await clearLocalOnly(args);
5983
+
5984
+ case "declare_trusted_endpoint":
5985
+ return await declareTrustedEndpoint(args);
5986
+
5987
+ case "revoke_trusted_endpoint":
5988
+ return await revokeTrustedEndpoint(args);
5989
+
5990
+ case "egress_repin":
5991
+ return await egressRepin(args);
5992
+
5687
5993
  // Per-tenant BYO LLM config + usage (Epic 28, #179)
5688
5994
  case "llm_config":
5689
5995
  return await llmConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.52.0",
3
+ "version": "2.54.0",
4
4
  "description": "MCP server for loopctl \u2014 structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",