loopctl-mcp-server 2.87.0 → 2.89.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 +3 -2
  2. package/index.js +102 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -320,7 +320,7 @@ exhausting a paginated walk is not the same as the row being absent.
320
320
  | `knowledge_conflicts` | List potential-conflict article pairs — published articles flagged "too similar to comfortably coexist" by the auto-linker / nightly lint sweep, highest-overlap first. The KB only FLAGS the pair; it does NOT decide redundancy-vs-contradiction — that's your call with live context. Each entry has both articles (id/title/status/category) + similarity. Then merge (supersede one, `knowledge_create` the merged article, or PATCH) or reconcile if they genuinely disagree. Paginated with `total_count` in meta. Agent role. Optional: `limit` (default 50, max 1000, clamped), `offset`, `origin` (`system` or `asserted` — asserted rows lead the default ordering, so pass `system` to review machine-flagged pairs alone). |
321
321
  | `knowledge_assert_conflict` | **ASSERT** a conflict between two articles the system never flagged — the way to contest an article you just deliberately refuted. `knowledge_resolve_conflict` only reaches pairs the AUTO-LINKER flagged by similarity, which is exactly wrong for a correction: the pair is minutes old (the nightly linker has not run) and a good correction argues about the CONCLUSION, so it may never be similar enough to be flagged at all. The pair then appears in `knowledge_conflicts` with `origin: "asserted"` and your claim attached, and in both articles' `potential_conflicts`. **It retires, hides and down-ranks nothing**, and does not remove either article from curated answers (that still needs a system flag). **And you cannot judge your own assertion** — `knowledge_resolve_conflict` returns `409 self_asserted_conflict` to the asserting key, because you named both ids; another key decides. Idempotent per pair (`created: false` on a re-assert; never overwrites a system flag's provenance). Agent role. Required: `source_article_id`, `target_article_id`, `evidence`. Optional: `classification`, `proposed_authoritative_article_id`. |
322
322
  | `knowledge_resolve_conflict` | Record YOUR verdict on a potential-conflict pair (from `knowledge_conflicts`). Dispositions: `dismiss` (false positive, drops from queue), `supersede` (one wins — pass `authoritative_article_id`; nightly executor links + retires loser, only at `confidence:"high"`, reversible/audited), `merge` (at high confidence an LLM synthesizes both into ONE new DRAFT, sources preserved, never auto-published). Non-destructive at agent role — you record intent, the privileged nightly job executes. Only pairs with a real flag are reachable; for a pair that was never flagged, use `knowledge_assert_conflict` first — and note that a DIFFERENT key must then record the verdict. Last-write-wins per pair. **`supersede` is the one disposition that retires an article unattended**, so its `confidence` is capped server-side: an agent-role `"high"` is recorded as `"medium"` (`data.requested_confidence` + `note` say so) and the pair stays in `knowledge_conflicts` until an orchestrator+ key records it at high; a `high` supersede also REQUIRES `evidence` (422 without it). `merge` is never capped. Sent on `LOOPCTL_AGENT_KEY`, so a pair YOU asserted answers `409 self_asserted_conflict` here by design — another principal (a second session, an orchestrator, a human operator) records that verdict. Required: `source_article_id`, `target_article_id`, `disposition`. Optional: `authoritative_article_id`, `classification`, `evidence` (required for a high supersede), `confidence`. |
323
- | `knowledge_create` | Create a new knowledge article. File findings, document patterns, or record decisions. **Published immediately by default** (visible per `metadata.visibility` — default `owner` for agent authors, only visible to that agent; `shared` for visibility to all agents) — the response `note` says which outcome occurred. Pass `draft: true` to stage it for later review instead (publish afterwards with `knowledge_publish`). Pass `metadata: {visibility: "shared"}` to make the article visible to other agents; higher roles can set visibility and agent_id explicitly. Pass `idempotency_key` for idempotent capture (re-creating with the same key is a no-op returning the existing article — no partial duplicates). RESERVED TAG NAMESPACE: a tag starting with `idem-` must be `idem-<family>-<digest>` (digest = 12 or 40 lowercase hex chars, e.g. `idem-url-7ebe1ca33431`) or the write is rejected 422 — never silently rewritten; put topics outside that prefix and use `idempotency_key` for idempotent capture. Optional: `category`, `tags`, `project_id`, `draft`, `idempotency_key`, `source_type`, `source_id`, `metadata`. |
323
+ | `knowledge_create` | Create a new knowledge article. File findings, document patterns, or record decisions. **Published immediately by default** (visible per `metadata.visibility` — default `owner` for agent authors, only visible to that agent; `shared` for visibility to all agents) — the response `note` says which outcome occurred. Pass `draft: true` to stage it for later review instead (publish afterwards with `knowledge_publish`). Pass `metadata: {visibility: "shared"}` to make the article visible to other agents; higher roles can set visibility and agent_id explicitly. Pass `idempotency_key` for idempotent capture (re-creating with the same key is a no-op returning the existing article — no partial duplicates). A `deduplicated: true` response carries `content_drift` / `title_drift` when the server supports them (an older loopctl omits the keys — absent means UNKNOWN, never false; the keys' presence is the capability signal, there is no server version to compare against): true means the payload you just sent DIFFERED from the returned article and was discarded. Which side moved is not decidable from your end (the stored article may have been curated or machine-retitled since your last capture), so read it first, and use `knowledge_update` on the returned `data.id` only if that row is your own prior capture and your version is still the intended one. When `gate.verdict` is `duplicate` the returned row was matched by SIMILARITY with no self-exclusion — it may be an article you did not write, or your own earlier capture — so PATCH it only if it is your own prior capture, else merge into it or re-send under a DIFFERENT title with `force: true` (the same title answers 409 title_conflict). RESERVED TAG NAMESPACE: a tag starting with `idem-` must be `idem-<family>-<digest>` (digest = 12 or 40 lowercase hex chars, e.g. `idem-url-7ebe1ca33431`) or the write is rejected 422 — never silently rewritten; put topics outside that prefix and use `idempotency_key` for idempotent capture. Optional: `category`, `tags`, `project_id`, `draft`, `idempotency_key`, `source_type`, `source_id`, `metadata`. |
324
324
  | `knowledge_update` | Edit an EXISTING article IN PLACE, **preserving its ID** (IDs are load-bearing — cited in project CLAUDE.mds and cross-links). Fold in a new fact, tidy a hub, retag, or reclassify without churning a new row. Send only the fields to change; `tags` REPLACES the whole array. A changed body/tags re-triggers embedding + auto-linking. Agent role — KB-content curation (non-destructive + audited; the in-place edit overwrites the prior body, so it is not reversible either); visibility-scoped, so another agent's private/owner memory 404s. The reserved `idem-` tag namespace applies here too (see `knowledge_create`). Required: `article_id`. Optional: `title`, `body`, `category`, `tags`, `metadata`. |
325
325
  | `knowledge_okf_export` | **Requires `LOOPCTL_USER_KEY`.** Export the wiki as a portable OKF (Open Knowledge Format) v0.1 bundle of markdown files. Writes to `out_dir`, or returns `{files, meta}` inline. |
326
326
  | `knowledge_okf_import` | **Requires `LOOPCTL_USER_KEY`.** Import an OKF v0.1 bundle from a local directory. Creates or (with `merge`) updates articles; tolerates and preserves unknown frontmatter. |
@@ -384,7 +384,8 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
384
384
  | `knowledge_agent_usage` | Per-agent knowledge usage: total reads, unique articles, top read articles. Required: exactly one of `api_key_id` (credential) or `agent_id` (logical identity). Optional: `limit`, `since_days`. See Wiki Attribution section. |
385
385
  | `knowledge_unused_articles` | Published articles with zero accesses in the window. Optional: `days_unused` (default 30), `limit` (default 50, max 200). |
386
386
  | `knowledge_curation_log` | Concise human-readable log of KB CURATION adjustments — novelty-gate decisions (`gate_duplicate`/`gate_draft`/`gate_skip`) 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`. |
387
- | `knowledge_retrieval_metrics` | Daily retrieval-PRECISION time series: for each day, the share of RECORDED surfaced 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. **Denominators (#582):** `precision` = `followed_through`/`searched`, where `searched` counts RECORDED surfaced RESULTS — one row per result put in front of the agent, capped at the first 20 per call (cap enforced by `Loopctl.Knowledge.Analytics.max_recorded_search_results/0`) — not search calls (`results_recorded` is the same number named for its unit). That cap makes `precision` precision@20: a call returning more contributes only 20, and an open of a result ranked beyond the cap is in neither term. The per-CALL rate is the separate `search_follow_through` = `searches_with_follow_through`/`searches` (distinct QUERY-BEARING calls — query-less `list`/`list_keyset` enumeration pages are excluded). `results_returned` is the true un-truncated count for those same calls. The four call-level fields are filtered per ROW, not per day, so a day mixing pre-#582 or enumeration rows with real searches reports a PARTIAL figure rather than 0 — and `results_returned` is NOT comparable to `searched`. Zero-result and keyless searches are unrecordable, so every ratio here is an upper bound. **`precision` ALONE rises when a search returns FEWER results** — its denominator counts surfaced RESULTS, while the two call-level rates divide CALL counts a narrower page does not shrink — so never optimise `precision` without the absolute `followed_through`. Both follow-through rates are biased DOWN by the recording cap (opens beyond rank 20 are invisible) and UP by crediting every search in the window that surfaced the opened article. **WHICH FOLLOW-THROUGH RATE TO QUOTE:** two are published over DIFFERENT populations, and picking the wrong one misstates agent behaviour by roughly 3.4x. `search_follow_through` is over every query-bearing call that survives the infrastructure exclusion — which still INCLUDES the recall hook and the session-start auto-query, channels that cannot follow through by construction — so read it as BLENDED and use it for total traffic. `scored_follow_through` = `searches_scored_with_follow_through`/`searches_scored` is over the calls carrying a session identity AND coming from a channel that can react to a result, and **IT is the rate to quote when asking whether AGENTS are consuming the KB**. It is `null` when nothing was scoreable, never `0.0` — zero would assert agents searched and opened nothing when the truth is the instrument could not see. Measured live for 2026-08-19..29: 10.8% blended against 38.0% scored, because the recall hook alone was 1,234 of that window's 1,708 calls. Most recent day first. Requires orchestrator role. Optional: `limit` (default 30, max 365), `offset`. |
387
+ | `knowledge_retrieval_metrics` | Daily retrieval-PRECISION time series: for each day, the share of RECORDED surfaced 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. **Denominators (#582):** `precision` = `followed_through`/`searched`, where `searched` counts RECORDED surfaced RESULTS — one row per result put in front of the agent, capped at the first 20 per call (cap enforced by `Loopctl.Knowledge.Analytics.max_recorded_search_results/0`) — not search calls (`results_recorded` is the same number named for its unit). That cap makes `precision` precision@20: a call returning more contributes only 20, and an open of a result ranked beyond the cap is in neither term. The per-CALL rate is the separate `search_follow_through` = `searches_with_follow_through`/`searches` (distinct QUERY-BEARING calls — query-less `list`/`list_keyset` enumeration pages are excluded). `results_returned` is the true un-truncated count for those same calls. The four call-level fields are filtered per ROW, not per day, so a day mixing pre-#582 or enumeration rows with real searches reports a PARTIAL figure rather than 0 — and `results_returned` is NOT comparable to `searched`. Zero-result and keyless searches are unrecordable, so every ratio here is an upper bound. **`precision` ALONE rises when a search returns FEWER results** — its denominator counts surfaced RESULTS, while the two call-level rates divide CALL counts a narrower page does not shrink — so never optimise `precision` without the absolute `followed_through`. Both follow-through rates are biased DOWN by the recording cap (opens beyond rank 20 are invisible) and UP by crediting every search in the window that surfaced the opened article. **WHICH FOLLOW-THROUGH RATE TO QUOTE:** two are published over DIFFERENT populations, and picking the wrong one misstates agent behaviour by roughly 3.4x. `search_follow_through` is over every query-bearing call that survives the infrastructure exclusion — which still INCLUDES the recall hook and the session-start auto-query, channels that cannot follow through by construction — so read it as BLENDED and use it for total traffic. `scored_follow_through` = `searches_scored_with_follow_through`/`searches_scored` is over the calls carrying a session identity AND coming from a channel that can react to a result, and **IT is the rate to quote when asking whether AGENTS are consuming the KB**. It is `null` when nothing was scoreable, never `0.0` — zero would assert agents searched and opened nothing when the truth is the instrument could not see. Measured live for 2026-08-19..29: 10.8% blended against 38.0% scored, because the recall hook alone was 1,234 of that window's 1,708 calls. Most recent day first. Requires orchestrator role. Optional: `limit` (default 30, max 365), `offset`. Every ratio here divides `search_events` / `article_access_events` columns, so a NULL column shrinks a denominator instead of reporting itself — check `knowledge_search_coverage` before reading a figure here as agent behaviour rather than instrumentation. |
388
+ | `knowledge_search_coverage` | Which DECLARED columns of `search_events` are actually being filled, per search surface, over a bounded window. `search_events` shipped correct and nearly blind — 2 of its first 133 rows carried any `client_*` context, discoverable only by an audit nobody was scheduled to run — so a coverage PROFILE per `tool` declares the columns a correctly-instrumented caller should supply, and a surface emitting NOTHING reads as `rows: 0` (which no audit over existing rows can produce). **It cannot prove a PRESENT column is a CORRECT one:** one MCP process serves a session and every agent it dispatches with an environment frozen at spawn, so it labels every search `main` — such a row is 100% covered here and still wrong about the only thing `client_kind` exists to say. **Populations, not one denominator:** each column names the rows that COULD have carried it — `all`, `ran` (excludes `outcome=rejected`, which has no `mode_used`/`duration_ms` by construction), `agent` (rows carrying `client_kind` or `client_session_id`, i.e. rows that really came through the MCP client; the recall hook and smoke tests call the API directly and can never supply `client_*`). `share_missing` is `null`, never `0.0`, on an empty population. **`client_context` is the one to read FIRST:** the `agent` denominator is built from two of the columns it scores, so a client that sends NOTHING empties it and every `client_*` line reads a clean `0/0` — that figure is the same count over `all`, and it is where a fleet gone blind reports itself (a high share on `memory_recall` is the recall hook and expected). **Required vs enrichable:** `required` is fillable at record time so a miss is a defect; `enrichable` (`client_model`, `client_effort`, `agent_id`) is what no client can send and `mix loopctl.enrich_search_events` fills offline on a schedule — read a recent enrichable share as a floor, since a window ending near now measures the enrichment's lag. **Unprofiled:** every `tool` with rows and no profile is listed (`null` included), and `rows_total` covers the whole window, so a new surface cannot be silently dropped. Requires orchestrator role. Optional: `days` (default 30, max 366), `to` (ISO8601 date or datetime, exclusive upper bound; a bare date is read at 00:00:00Z, and an unparseable value is a 400 rather than a silent fallback to now). |
388
389
 
389
390
  ### Egress / Privacy Tools (US-41.4)
390
391
 
package/index.js CHANGED
@@ -2504,6 +2504,18 @@ async function knowledgeRetrievalMetrics({ limit, offset } = {}) {
2504
2504
  return toContent(result);
2505
2505
  }
2506
2506
 
2507
+ async function knowledgeSearchCoverage({ days, to } = {}) {
2508
+ const params = new URLSearchParams();
2509
+ if (days != null) params.set("days", String(days));
2510
+ if (to) params.set("to", to);
2511
+ const qs = params.toString();
2512
+ const path = qs
2513
+ ? `/api/v1/knowledge/analytics/search-coverage?${qs}`
2514
+ : "/api/v1/knowledge/analytics/search-coverage";
2515
+ const result = await apiCall("GET", path, null, process.env.LOOPCTL_ORCH_KEY);
2516
+ return toContent(result);
2517
+ }
2518
+
2507
2519
  async function knowledgeCurationLog({ kind, since, limit, offset } = {}) {
2508
2520
  const params = new URLSearchParams();
2509
2521
  if (kind) params.set("kind", kind);
@@ -5255,7 +5267,21 @@ const TOOLS = [
5255
5267
  "existing article with the same title AND an identical body (ignoring surrounding whitespace), the " +
5256
5268
  "server returns that existing article idempotently (HTTP 200) instead of a 422. A same-title create " +
5257
5269
  "with a DIFFERENT body returns 409 title_conflict — do not retry; choose a different title or PATCH " +
5258
- "the existing article.",
5270
+ "the existing article. CONTENT DRIFT: a `deduplicated: true` response carries " +
5271
+ "`content_drift` / `title_drift` when the server supports them (an older loopctl omits the " +
5272
+ "keys — treat ABSENT as unknown, never as false; presence of the keys IS the capability " +
5273
+ "signal, there is no server version to compare against); true means the payload you just " +
5274
+ "sent DIFFERS from the returned article and was DISCARDED. Which SIDE moved is not decidable " +
5275
+ "from your end — you may have edited, or the stored article may have been curated by someone " +
5276
+ "else or machine-retitled since your last capture — so READ the returned article first " +
5277
+ "(knowledge_get on `data.id`). Only THEN, and only if the returned row is your own prior " +
5278
+ "capture, apply your version with knowledge_update. When the response also carries " +
5279
+ "`gate.verdict: \"duplicate\"` the returned row was matched by SIMILARITY with no " +
5280
+ "self-exclusion, so it may be an article you did not write OR your own earlier capture: read " +
5281
+ "it, PATCH only your own prior capture, else merge into it or re-send under a DIFFERENT " +
5282
+ "title with force: true (the same title answers 409 title_conflict). Sending `body` as " +
5283
+ "null or a non-string also reports content_drift: the dedup short-circuits before validation, " +
5284
+ "so that is how a broken extraction surfaces instead of reading as in-sync.",
5259
5285
  inputSchema: {
5260
5286
  type: "object",
5261
5287
  properties: {
@@ -5314,7 +5340,10 @@ const TOOLS = [
5314
5340
  description:
5315
5341
  "Optional: stable per-article key for idempotent capture (max 255). Re-creating " +
5316
5342
  "with the same key is a no-op that returns a reference to the existing article " +
5317
- "(deduplicated; id only, not its body) instead of a partial duplicate. Use a " +
5343
+ "(deduplicated; id only, not its body) instead of a partial duplicate a changed " +
5344
+ "body is NOT applied and NOT refused, but IS reported as content_drift so a " +
5345
+ "re-running sourcer keeps working while a genuine edit is not lost silently (read the " +
5346
+ "stored article before overwriting it — the other side may have moved). Use a " +
5318
5347
  "HIGH-ENTROPY value (e.g. a content hash) — it is a per-tenant lookup key, not a " +
5319
5348
  "secret, so a guessable key lets another agent in your tenant probe which keys " +
5320
5349
  "exist. Distinct from source_type/source_id (which mark a shared source).",
@@ -6935,6 +6964,11 @@ const TOOLS = [
6935
6964
  "within a window). A proxy for whether retrieval is improving — watch it trend up as " +
6936
6965
  "the corpus is de-duplicated, better navigated (MOCs), and conflict-resolved. Most " +
6937
6966
  "recent day first. Requires orchestrator role.\n\n" +
6967
+ "Every ratio here divides columns of search_events / article_access_events, so a " +
6968
+ "NULL column silently shrinks a denominator rather than reporting itself. " +
6969
+ "knowledge_search_coverage is the companion that says which of those columns are " +
6970
+ "actually being filled, per surface — check it before treating a figure here as " +
6971
+ "a measurement of agent behaviour rather than of instrumentation.\n\n" +
6938
6972
  // The cap is enforced by Loopctl.Knowledge.Analytics.max_recorded_search_results/0
6939
6973
  // (Elixir); this JS string cannot interpolate it, so change both together.
6940
6974
  "Denominators (#582): precision = followed_through / searched, and `searched` counts " +
@@ -7033,6 +7067,69 @@ const TOOLS = [
7033
7067
  required: [],
7034
7068
  },
7035
7069
  },
7070
+ {
7071
+ name: "knowledge_search_coverage",
7072
+ description:
7073
+ "Report which DECLARED columns of search_events are actually being filled, per search " +
7074
+ "surface, over a bounded window. Requires orchestrator role.\n\n" +
7075
+ "WHY IT EXISTS: search_events shipped correct and nearly blind — 2 of its first 133 " +
7076
+ "rows carried any client_* context, and that was discoverable only by an audit nobody " +
7077
+ "was scheduled to run. A declared coverage PROFILE per tool names the columns a " +
7078
+ "correctly-instrumented caller is expected to supply, so a surface emitting NOTHING " +
7079
+ "shows up as rows: 0, which no audit over existing rows can produce. Prior art: " +
7080
+ "MemoRizz v0.8.0 declares the evidence stages a task type must emit and reports the " +
7081
+ "missing stage instead of leaving it to an audit.\n\n" +
7082
+ "WHAT IT CANNOT PROVE: that a PRESENT column is a CORRECT one. client_kind is the " +
7083
+ "worked example — one MCP process serves a session and every agent it dispatches with " +
7084
+ "an environment frozen at spawn, so it labels every search 'main'. Such a row is 100% " +
7085
+ "covered here and still wrong about the only thing that column exists to say; the " +
7086
+ "offline enrichment is what refines it. It also cannot see a search path that records " +
7087
+ "NO row at all.\n\n" +
7088
+ "POPULATIONS, NOT ONE DENOMINATOR: each column names the rows that COULD have carried " +
7089
+ "it, reported as scope/population beside every count. 'all' is every row; 'ran' " +
7090
+ "excludes outcome=rejected (a rejected call never ran, so it has no mode_used and no " +
7091
+ "duration_ms by construction); 'agent' is rows carrying client_kind or " +
7092
+ "client_session_id, i.e. rows that really came through the MCP client — the recall " +
7093
+ "hook and smoke tests call the API directly and can never supply client_*, so scoring " +
7094
+ "them would measure loopctl's own automation. share_missing is null, never 0.0, on an " +
7095
+ "empty population.\n\n" +
7096
+ "CLIENT_CONTEXT: the 'agent' denominator is built from two of the columns it scores, " +
7097
+ "so a client that sends NOTHING empties it and every client_* line then reads a clean " +
7098
+ "0/0. Each profile therefore also carries client_context, scored over 'all', whose " +
7099
+ "missing is the rows that carried NO client context at all — that is where a fleet gone " +
7100
+ "blind reports itself. A high share on memory_recall is the recall hook and expected; a " +
7101
+ "high share on knowledge_search is not.\n\n" +
7102
+ "REQUIRED vs ENRICHABLE: required is what a client or the server can fill at record " +
7103
+ "time, so a miss is a defect. enrichable (client_model, client_effort, agent_id) is " +
7104
+ "what no client can send — the first two do not exist in the MCP server's spawn " +
7105
+ "environment and are filled offline by mix loopctl.enrich_search_events, and agent_id " +
7106
+ "is server-derived from a key that may own no agent. That enrichment runs on a " +
7107
+ "schedule, so a window ending near now measures its LAG: read a recent enrichable " +
7108
+ "share as a floor.\n\n" +
7109
+ "UNPROFILED: every tool value with rows and no declared profile is listed with its row " +
7110
+ "count, null included. rows_total counts the whole window, so rows_total minus the sum " +
7111
+ "of profile rows is exactly the unprofiled traffic — a new surface cannot be silently " +
7112
+ "dropped from the accounting.",
7113
+ inputSchema: {
7114
+ type: "object",
7115
+ properties: {
7116
+ days: {
7117
+ type: "integer",
7118
+ description: "Window length in days back from `to` (default 30, max 366). Clamped, never rejected.",
7119
+ minimum: 1,
7120
+ maximum: 366,
7121
+ },
7122
+ to: {
7123
+ type: "string",
7124
+ description:
7125
+ "ISO8601 date or datetime, exclusive upper bound. Default now. A bare date is " +
7126
+ "read at 00:00:00Z. The window is [from, to). Rejected with 400 when it cannot " +
7127
+ "be parsed — never silently replaced with now.",
7128
+ },
7129
+ },
7130
+ required: [],
7131
+ },
7132
+ },
7036
7133
  {
7037
7134
  name: "knowledge_analytics_top",
7038
7135
  description:
@@ -8225,6 +8322,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
8225
8322
  case "knowledge_retrieval_metrics":
8226
8323
  return await knowledgeRetrievalMetrics(args);
8227
8324
 
8325
+ case "knowledge_search_coverage":
8326
+ return await knowledgeSearchCoverage(args);
8327
+
8228
8328
  case "knowledge_analytics_top":
8229
8329
  return await knowledgeAnalyticsTop(args);
8230
8330
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.87.0",
3
+ "version": "2.89.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",