loopctl-mcp-server 2.24.0 → 2.26.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 (2) hide show
  1. package/index.js +73 -6
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -848,6 +848,7 @@ async function knowledgeCreate({
848
848
  tags,
849
849
  project_id,
850
850
  draft,
851
+ force,
851
852
  source_type,
852
853
  source_id,
853
854
  idempotency_key,
@@ -868,6 +869,10 @@ async function knowledgeCreate({
868
869
  // knowledge_publish.
869
870
  if (draft) payload.draft = true;
870
871
 
872
+ // The server-side novelty gate dedups the proposal against the corpus (verdict in
873
+ // the response `gate`). force:true bypasses it.
874
+ if (force) payload.force = true;
875
+
871
876
  const result = await apiCall(
872
877
  "POST",
873
878
  "/api/v1/articles",
@@ -1022,9 +1027,9 @@ async function knowledgeBulkDelete({
1022
1027
 
1023
1028
  async function knowledgeDrafts({ limit, offset, project_id }) {
1024
1029
  const params = new URLSearchParams();
1025
- // Pass `limit` through verbatim (like knowledge_list/index/search) so the
1026
- // server honors it up to its max page size and returns 400 above it rather
1027
- // than silently clamping client-side, which would truncate draft enumeration.
1030
+ // Pass `limit` through verbatim (like knowledge_list/index/search) so the server
1031
+ // honors it up to its max page size, clamping above it server-side rather than
1032
+ // silently clamping client-side (which would truncate draft enumeration).
1028
1033
  if (limit != null) params.set("limit", String(limit));
1029
1034
  if (offset != null) params.set("offset", String(offset));
1030
1035
  if (project_id) params.set("project_id", project_id);
@@ -1033,6 +1038,18 @@ async function knowledgeDrafts({ limit, offset, project_id }) {
1033
1038
  return toContent(result);
1034
1039
  }
1035
1040
 
1041
+ async function knowledgeConflicts({ limit, offset }) {
1042
+ const params = new URLSearchParams();
1043
+ if (limit != null) params.set("limit", String(limit));
1044
+ if (offset != null) params.set("offset", String(offset));
1045
+ const qs = params.toString();
1046
+ const path = qs
1047
+ ? `/api/v1/knowledge/conflicts?${qs}`
1048
+ : "/api/v1/knowledge/conflicts";
1049
+ const result = await apiCall("GET", path, null, process.env.LOOPCTL_AGENT_KEY);
1050
+ return toContent(result);
1051
+ }
1052
+
1036
1053
  async function knowledgeLint({ project_id, stale_days, min_coverage, max_per_category }) {
1037
1054
  const params = new URLSearchParams();
1038
1055
  if (stale_days != null) params.set("stale_days", String(stale_days));
@@ -2565,6 +2582,13 @@ const TOOLS = [
2565
2582
  "to agents (search/index/context) right away — no separate publish step is needed. Pass " +
2566
2583
  "draft: true to stage the article for later review instead; the response `note` says which " +
2567
2584
  "outcome occurred, and a draft can be published afterwards with knowledge_publish. " +
2585
+ "NOVELTY GATE (default ON): the server semantically dedups your proposal against the published " +
2586
+ "corpus and returns a `gate.verdict` — `duplicate` means a near-identical article already exists, " +
2587
+ "so NOTHING was created (HTTP 200, `deduplicated: true`); read/update the article at `data.id` " +
2588
+ "instead (its `gate.similarity` ~1.0). `gated_to_draft` means high overlap, so the article was " +
2589
+ "created as a DRAFT (not published) with the near-neighbors in metadata.proposal_novelty for you " +
2590
+ "to merge or publish. `created` means it was novel and went through normally. Pass force: true to " +
2591
+ "bypass the gate when you intentionally want an article near an existing one. " +
2568
2592
  "Concurrency-safe: if a create races/retries against an " +
2569
2593
  "existing article with the same title AND an identical body (ignoring surrounding whitespace), the " +
2570
2594
  "server returns that existing article idempotently (HTTP 200) instead of a 422. A same-title create " +
@@ -2600,6 +2624,13 @@ const TOOLS = [
2600
2624
  "Optional: stage as a draft instead of publishing on create (default false → " +
2601
2625
  "published immediately). Publish later with knowledge_publish.",
2602
2626
  },
2627
+ force: {
2628
+ type: "boolean",
2629
+ description:
2630
+ "Optional: bypass the novelty gate (default false). When true, the server skips " +
2631
+ "semantic dedup and creates on the requested path even if a near-duplicate exists. " +
2632
+ "Use only when you've already checked and intend an article close to an existing one.",
2633
+ },
2603
2634
  idempotency_key: {
2604
2635
  type: "string",
2605
2636
  description:
@@ -2833,8 +2864,8 @@ const TOOLS = [
2833
2864
  description:
2834
2865
  "List draft (unpublished) knowledge articles. Requires orchestrator role. " +
2835
2866
  "Returns paginated drafts with total_count in meta. Paginate via offset/limit " +
2836
- "(limit honored up to 1000; a limit above the max is rejected with 400, not " +
2837
- "silently clamped).",
2867
+ "(limit honored up to 1000; a limit above the max is clamped to the maximum, " +
2868
+ "never rejected, so pagination stays complete).",
2838
2869
  inputSchema: {
2839
2870
  type: "object",
2840
2871
  properties: {
@@ -2842,7 +2873,7 @@ const TOOLS = [
2842
2873
  type: "integer",
2843
2874
  description:
2844
2875
  "Max drafts per page (default 20, max 1000). A limit above the max is " +
2845
- "rejected with 400not silently clamped — so offset pagination stays complete.",
2876
+ "clamped to the maximum never rejected — so offset pagination stays complete.",
2846
2877
  default: 20,
2847
2878
  minimum: 1,
2848
2879
  maximum: 1000,
@@ -2861,6 +2892,39 @@ const TOOLS = [
2861
2892
  required: [],
2862
2893
  },
2863
2894
  },
2895
+ {
2896
+ name: "knowledge_conflicts",
2897
+ description:
2898
+ "List potential-conflict article pairs — published articles flagged 'too similar " +
2899
+ "to comfortably coexist' by the auto-linker / nightly lint sweep, highest-overlap " +
2900
+ "first. The KB only FLAGS the pair (via a mechanical similarity threshold); it does " +
2901
+ "NOT decide whether it's a redundancy to merge or a real contradiction — that's your " +
2902
+ "call, with the live context. Each entry has the two articles (id/title/status/" +
2903
+ "category) and their similarity. Read both, then merge (supersede one, knowledge_create " +
2904
+ "the merged article, or PATCH) or, if they genuinely disagree, reconcile. Paginated " +
2905
+ "with total_count in meta. Agent role.",
2906
+ inputSchema: {
2907
+ type: "object",
2908
+ properties: {
2909
+ limit: {
2910
+ type: "integer",
2911
+ description:
2912
+ "Max pairs per page (default 50, max 1000). A limit above the max is clamped " +
2913
+ "to the maximum — never rejected — so offset pagination stays complete.",
2914
+ default: 50,
2915
+ minimum: 1,
2916
+ maximum: 1000,
2917
+ },
2918
+ offset: {
2919
+ type: "integer",
2920
+ description: "Pagination offset. Default 0.",
2921
+ default: 0,
2922
+ minimum: 0,
2923
+ },
2924
+ },
2925
+ required: [],
2926
+ },
2927
+ },
2864
2928
  {
2865
2929
  name: "knowledge_lint",
2866
2930
  description:
@@ -3481,6 +3545,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3481
3545
  case "knowledge_drafts":
3482
3546
  return await knowledgeDrafts(args);
3483
3547
 
3548
+ case "knowledge_conflicts":
3549
+ return await knowledgeConflicts(args);
3550
+
3484
3551
  case "knowledge_lint":
3485
3552
  return await knowledgeLint(args);
3486
3553
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.24.0",
3
+ "version": "2.26.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",