loopctl-mcp-server 2.40.0 → 2.41.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 +6 -1
  2. package/index.js +158 -0
  3. 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 73 typed MCP tools so AI coding agents (Claude Code, etc.) can interact with loopctl without writing curl commands.
5
+ Wraps the loopctl REST API into 76 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
 
@@ -198,6 +198,8 @@ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycl
198
198
  | How many articles? (counts only) | `knowledge_stats` |
199
199
  | Browse the catalog (lightweight id/title/category) | `knowledge_index` |
200
200
  | Find by topic / relevance (ranked, published-only, lags writes) | `knowledge_search` |
201
+ | One trustworthy answer + provenance (curated-first, retrieval fallback) | `knowledge_hybrid_search` |
202
+ | Survey a topic cheaply (capped stubs) then open only what you need | `knowledge_progressive_index` + `knowledge_progressive_drill` |
201
203
  | Enumerate / dedup / repair, or "does X exist?" (full fields, lag-free, all-status) | `knowledge_list` |
202
204
 
203
205
  | Tool | Description |
@@ -207,6 +209,9 @@ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycl
207
209
  | `knowledge_count` | Count articles matching filters **without returning rows** within agent's visible set. Agent callers see only their own and `shared` articles. Same filters as `knowledge_list` (`category`, `status`, `tags`, `match`, `source_type`, `source_id`, `idempotency_key`, `project_id`). With `tags`+`match: all` (+`status`) → "how many published articles tagged both X and Y (that I can see)". Returns `{ count }`. |
208
210
  | `knowledge_facets` | Count articles grouped by **distinct tag** within agent's visible set, no rows. Agent callers see only their own and `shared` articles. `tag_prefix` (e.g. `book-`) gives the distinct count of a tag family plus per-member totals. Returns `{ data: { tag: count }, meta: { distinct_count } }`. Optional: `category`, `status`, `tags`, `match`, `project_id`, `limit`. |
209
211
  | `knowledge_search` | Search the knowledge wiki by topic (keyword, semantic, or combined). Returns snippets. **Ranked, published-only, and LAGS writes by minutes while embeddings index — do NOT use for existence/idempotency/dedup checks (a fresh write false-negatives); use `knowledge_list` for that.** `q` is optional when `tags`/`category` are supplied — that **list mode** returns the complete filtered set paginated via `offset`/`limit` over `meta.total_count`. `meta.total_count` is mode-dependent — read `meta.total_count_scope` (`keyword_matches`/`ranked_corpus`/`merged_candidates`/`filtered_set`) and don't use a relevance-mode count to size the wiki (use `knowledge_list` or `knowledge_stats`). Optional: `project_id`, `story_id` for attribution. |
212
+ | `knowledge_hybrid_search` | Resolve a topic to a **single best answer with provenance** (US-31.4). Runs combined keyword+semantic over the full ranked pool, then decides whether a governed **curated** source actually answers. `meta.provenance` is `curated` (trust it — the canonical article is first in `data`, `meta.curated_article_id` points at it) or `retrieved` (best fuzzy match, `curated_article_id` null); `meta.confidence` is the winner's absolute score. Prefer over `knowledge_search` when you want one trustworthy answer, not a list to triage. Degrades to keyword-only like `knowledge_search` when embeddings are unavailable. Required: `query`. Optional: `project_id`, `category`, `tags`, `match`, `limit`, `offset`. |
213
+ | `knowledge_progressive_index` | Progressive disclosure — a **cheap, capped index** of what's relevant to a topic (compact stubs: `id`/`title`/`category`/`summary`, **no bodies**), curated-preferred and hub-enriched, capped at top-K (`meta.truncated` when the pool exceeded it). Survey a topic without flooding context, then open only what you need via `knowledge_progressive_drill`. Required: `topic`. Optional: `category`, `limit`. |
214
+ | `knowledge_progressive_drill` | Open one stub from `knowledge_progressive_index` — returns the **full article body** for the given id, scope-enforced. Resolves both tenant-owned articles and published system canonicals (the same set the index surfaces). Required: `article_id`. |
210
215
  | `knowledge_list` | List articles (`id`, `title`, `category`, `status`, `tags`, `source_type`, `source_id`, `idempotency_key`, timestamps), filtered + paginated. **Body-less summary by default** (safe to page up to `limit=1000`); pass `include_body: true` to also return `body`, in which case the page is bounded by a ~5 MB byte budget — continue via `meta.next_offset` while `meta.has_more`. **Lag-free, all-status** read of the DB of record — unlike `knowledge_search` (ranked, published-only, lags writes) and `knowledge_index` (id/title/category only). The right tool to enumerate/dedup/repair and for idempotency/existence checks: filter by `tags`, `source_type`+`source_id`, or `idempotency_key` and read `meta.total_count` (exact). Single full body → `knowledge_get`; relevant bodies → `knowledge_context`; bulk dump → `knowledge_export`. Optional: `project_id`, `category`, `status`, `tags`, `source_type`, `source_id`, `idempotency_key`, `offset`, `limit`, `include_body`. |
211
216
  | `knowledge_get` | Get full article content by ID. Use after search to read an article in detail. Optional: `project_id`, `story_id` for attribution. |
212
217
  | `knowledge_context` | Get relevance-and-recency-ranked full articles for a task query. Best knowledge for your current context. **Agent-memory scoping**: `memory_types` (comma-separated, OR — observation/finding/summary/decision/question/task), `agents` (comma-separated agent_ids, OR), `conversation_id` (exact) filter on article `metadata` (JSONB `@>`). Optional: `project_id`, `story_id` for attribution, `limit`, `recency_weight`. |
package/index.js CHANGED
@@ -962,6 +962,52 @@ async function knowledgeSearch({ q, project_id, story_id, category, tags, match,
962
962
  return withRemediationNotice(result);
963
963
  }
964
964
 
965
+ async function knowledgeHybridSearch({ query, project_id, category, tags, match, limit, offset }) {
966
+ const bodyPayload = { query };
967
+ if (project_id) bodyPayload.project_id = project_id;
968
+ if (category) bodyPayload.category = category;
969
+ if (tags) bodyPayload.tags = tags;
970
+ if (match) bodyPayload.match = match;
971
+ if (limit != null) bodyPayload.limit = limit;
972
+ if (offset != null) bodyPayload.offset = offset;
973
+
974
+ const result = await apiCall(
975
+ "POST",
976
+ "/api/v1/knowledge/hybrid_search",
977
+ bodyPayload,
978
+ process.env.LOOPCTL_AGENT_KEY,
979
+ );
980
+ // meta.provenance ("curated" | "retrieved") is the load-bearing field and rides
981
+ // through toContent verbatim. Same missing-embedding-key remediation surfacing as
982
+ // knowledge_search, since hybrid runs the combined pool underneath.
983
+ return withRemediationNotice(result);
984
+ }
985
+
986
+ async function knowledgeProgressiveIndex({ topic, category, limit }) {
987
+ const params = new URLSearchParams();
988
+ if (topic != null) params.set("topic", topic);
989
+ if (category) params.set("category", category);
990
+ if (limit != null) params.set("limit", String(limit));
991
+
992
+ const result = await apiCall(
993
+ "GET",
994
+ `/api/v1/knowledge/progressive_index?${params}`,
995
+ null,
996
+ process.env.LOOPCTL_AGENT_KEY,
997
+ );
998
+ return toContent(result);
999
+ }
1000
+
1001
+ async function knowledgeProgressiveDrill({ article_id }) {
1002
+ const result = await apiCall(
1003
+ "GET",
1004
+ `/api/v1/knowledge/progressive/${article_id}`,
1005
+ null,
1006
+ process.env.LOOPCTL_AGENT_KEY,
1007
+ );
1008
+ return toContent(result);
1009
+ }
1010
+
965
1011
  async function knowledgeList({
966
1012
  project_id,
967
1013
  category,
@@ -3005,6 +3051,109 @@ const TOOLS = [
3005
3051
  required: [],
3006
3052
  },
3007
3053
  },
3054
+ {
3055
+ name: "knowledge_hybrid_search",
3056
+ description:
3057
+ "Resolve a topic to a SINGLE best answer WITH provenance — the hybrid " +
3058
+ "retrieval entrypoint. Runs the combined keyword+semantic search over the full " +
3059
+ "ranked pool, then decides whether a governed CURATED source actually answers. " +
3060
+ "The verdict is meta.provenance: 'curated' means a curated, canonical article " +
3061
+ "answers — TRUST IT (it is guaranteed first in the results and meta.curated_article_id " +
3062
+ "points at it); 'retrieved' means no curated source cleared the bar, so the answer " +
3063
+ "is the best semantic/keyword match (a fuzzy fallback — verify before relying on it, " +
3064
+ "meta.curated_article_id is null). meta.confidence is the winning candidate's absolute " +
3065
+ "score for its provenance class. Prefer this over knowledge_search when you want ONE " +
3066
+ "trustworthy answer plus its provenance rather than a ranked list to triage yourself; " +
3067
+ "use knowledge_search when you want to browse/enumerate matches. Additive — existing " +
3068
+ "knowledge tools are unchanged. If semantic ranking is unavailable it degrades to " +
3069
+ "keyword-only (meta.fallback/fallback_reason), same as knowledge_search.",
3070
+ inputSchema: {
3071
+ type: "object",
3072
+ properties: {
3073
+ query: {
3074
+ type: "string",
3075
+ description: "The topic/question to resolve (max 500 characters). Required.",
3076
+ },
3077
+ project_id: {
3078
+ type: "string",
3079
+ format: "uuid",
3080
+ description: "Optional: scope to a project UUID.",
3081
+ },
3082
+ category: {
3083
+ type: "string",
3084
+ description: "Optional: filter by category.",
3085
+ },
3086
+ tags: {
3087
+ type: "string",
3088
+ description: "Optional: comma-separated tags to filter by.",
3089
+ },
3090
+ match: {
3091
+ type: "string",
3092
+ enum: ["any", "all"],
3093
+ description: "Optional: tag match mode — 'any' (default, OR) or 'all' (AND, every tag).",
3094
+ },
3095
+ limit: {
3096
+ type: "integer",
3097
+ description: "Optional: max results to return (default 10).",
3098
+ },
3099
+ offset: {
3100
+ type: "integer",
3101
+ description: "Optional: results to skip for pagination (default 0).",
3102
+ },
3103
+ },
3104
+ required: ["query"],
3105
+ },
3106
+ },
3107
+ {
3108
+ name: "knowledge_progressive_index",
3109
+ description:
3110
+ "Progressive disclosure — get a CHEAP, capped index of what's relevant to a topic " +
3111
+ "(compact stubs: id/title/category/summary, NO bodies), then open only what you need " +
3112
+ "with knowledge_progressive_drill. Curated sources are preferred and hub-linked " +
3113
+ "neighbors are enriched in; results are capped at a configured top-K (meta.truncated " +
3114
+ "is true when the candidate pool exceeded it). Use this to survey a topic without " +
3115
+ "flooding your context with full article bodies — it's the index half of the hybrid " +
3116
+ "interface. Follow up on a stub's id with knowledge_progressive_drill to read the body.",
3117
+ inputSchema: {
3118
+ type: "object",
3119
+ properties: {
3120
+ topic: {
3121
+ type: "string",
3122
+ description: "The topic to index (max 500 characters). Required.",
3123
+ },
3124
+ category: {
3125
+ type: "string",
3126
+ description: "Optional: filter by category.",
3127
+ },
3128
+ limit: {
3129
+ type: "integer",
3130
+ description: "Optional: top-K override (clamped to the configured cap).",
3131
+ },
3132
+ },
3133
+ required: ["topic"],
3134
+ },
3135
+ },
3136
+ {
3137
+ name: "knowledge_progressive_drill",
3138
+ description:
3139
+ "Drill into one stub from knowledge_progressive_index — returns the FULL article " +
3140
+ "body for the given id, scope-enforced. Resolves both tenant-owned articles and " +
3141
+ "published system canonicals (the same set the index surfaces). This is the drill " +
3142
+ "half of progressive disclosure: index cheaply, then open only the article(s) you " +
3143
+ "need. (knowledge_get works for tenant articles too; use this to also reach the " +
3144
+ "system canonicals the progressive index can surface.)",
3145
+ inputSchema: {
3146
+ type: "object",
3147
+ properties: {
3148
+ article_id: {
3149
+ type: "string",
3150
+ format: "uuid",
3151
+ description: "The UUID of the article to open (from a progressive index stub).",
3152
+ },
3153
+ },
3154
+ required: ["article_id"],
3155
+ },
3156
+ },
3008
3157
  {
3009
3158
  name: "knowledge_get",
3010
3159
  description:
@@ -4654,6 +4803,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4654
4803
  case "knowledge_search":
4655
4804
  return await knowledgeSearch(args);
4656
4805
 
4806
+ case "knowledge_hybrid_search":
4807
+ return await knowledgeHybridSearch(args);
4808
+
4809
+ case "knowledge_progressive_index":
4810
+ return await knowledgeProgressiveIndex(args);
4811
+
4812
+ case "knowledge_progressive_drill":
4813
+ return await knowledgeProgressiveDrill(args);
4814
+
4657
4815
  case "knowledge_list":
4658
4816
  return await knowledgeList(args);
4659
4817
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.40.0",
3
+ "version": "2.41.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",