loopctl-mcp-server 2.53.1 → 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 +1 -1
  2. package/index.js +69 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -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)
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",
@@ -5114,7 +5127,11 @@ const TOOLS = [
5114
5127
  "(extraction_model / classification_model / merge_model / embedding_model) are " +
5115
5128
  "free-form (any model the key permits) and default server-side when omitted. Typical " +
5116
5129
  'onboarding call: set_llm_config({api_key: "sk-ant-...", embedding_api_key: "sk-..."}). ' +
5117
- "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 " +
5118
5135
  "user-role key LOOPCTL_USER_KEY (minted by the human-anchored signup ceremony); if it " +
5119
5136
  "is unset the tool fails fast telling you to set it.",
5120
5137
  inputSchema: {
@@ -5128,7 +5145,14 @@ const TOOLS = [
5128
5145
  },
5129
5146
  extraction_model: {
5130
5147
  type: "string",
5131
- 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.",
5132
5156
  },
5133
5157
  classification_model: {
5134
5158
  type: "string",
@@ -5150,6 +5174,48 @@ const TOOLS = [
5150
5174
  description:
5151
5175
  "Embedding model id (null → server default text-embedding-3-small).",
5152
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
+ },
5153
5219
  },
5154
5220
  required: [],
5155
5221
  },
@@ -5157,7 +5223,7 @@ const TOOLS = [
5157
5223
  {
5158
5224
  name: "knowledge_llm_usage",
5159
5225
  description:
5160
- "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 + " +
5161
5227
  "day over an optional date range, newest day first, with offset/limit pagination " +
5162
5228
  "over meta.total_count. When `from` is omitted it defaults to a 90-day lookback; the " +
5163
5229
  "EFFECTIVE window is echoed in meta.from/meta.to so you can detect that older usage " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.53.1",
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",