@cachly-dev/mcp-server 0.10.72 → 0.10.74

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.
package/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  <img src="https://img.shields.io/badge/GDPR-EU%20servers-green" alt="GDPR: EU servers" />
23
23
  </a>
24
24
  &nbsp;
25
- <img src="https://img.shields.io/badge/105%20MCP%20tools-violet" alt="105 MCP tools" />
25
+ <img src="https://img.shields.io/badge/107%20MCP%20tools-violet" alt="107 MCP tools" />
26
26
  &nbsp;
27
27
  <img src="https://img.shields.io/badge/License-Apache--2.0-yellow" alt="License: Apache-2.0" />
28
28
  </p>
@@ -2,13 +2,21 @@ import { execSync } from 'node:child_process';
2
2
  import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
3
3
  import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration, ckgUpsertServiceNode } from '../ckg.js';
4
4
  import { safeJsonParse, scanKeys } from '../utils.js';
5
- import { getRole, ROLE_BADGE } from './team.js';
5
+ import { getRole, ROLE_BADGE, getScopes, lessonVisibleToScope } from './team.js';
6
6
  import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
7
7
  import { rerankByQuality } from '../rerank.js';
8
8
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
9
9
  // ── Changelog (shown once per version in session_start) ──────────────────────
10
- const MCP_VERSION = '0.10.72';
10
+ const MCP_VERSION = '0.10.73';
11
11
  const WHATS_NEW = {
12
+ '0.10.73': [
13
+ `🔐 **Team visibility scopes + one-command init + external bench**`,
14
+ ` 🆕 \`team_grant_scope\` / \`team_scopes\` — scope lessons to a sub-team (group="security"); only members + admins recall them`,
15
+ ` 🏷️ \`learn_from_attempts(group="...")\` — orthogonal to private; smart_recall enforces it per requester`,
16
+ ` ⚡ \`init\` is now zero-arg + idempotent (reuses saved creds, only writes what changed, <60s)`,
17
+ ` 📦 \`npm run bench:external\` — prove recall-lift on a third-party-labeled corpus (portable JSON format)`,
18
+ ` 📊 107 MCP tools`,
19
+ ],
12
20
  '0.10.72': [
13
21
  `👑 **Role model — admin · reviewer · contributor · viewer (Phase 3)**`,
14
22
  ` 🆕 \`team_assign_role\` — establish governance; first call bootstraps, admins manage the rest`,
@@ -114,7 +122,7 @@ export const BRAIN_TOOL_NAMES = new Set([
114
122
  export async function handleBrainTool(name, args, getConnection, apiFetch) {
115
123
  switch (name) {
116
124
  case 'learn_from_attempts': {
117
- const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', } = args;
125
+ const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', group = '', } = args;
118
126
  const redis = await getConnection(instance_id);
119
127
  const ts = new Date().toISOString();
120
128
  // ── Structured template hints ──────────────────────────────────────────
@@ -233,6 +241,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
233
241
  depends_on,
234
242
  ...(author ? { author } : {}),
235
243
  ...(service ? { service } : {}),
244
+ ...(group ? { group: String(group).toLowerCase().trim() } : {}),
236
245
  visibility,
237
246
  recall_count: recallCount,
238
247
  ts,
@@ -585,6 +594,15 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
585
594
  ? rawContextFiles.filter((f) => typeof f === 'string')
586
595
  : [];
587
596
  const redis = await getConnection(instance_id);
597
+ // Team-level visibility scopes: resolve the requester's group memberships +
598
+ // admin status once, so group-scoped lessons only surface for members/admins.
599
+ let requesterScopes = new Set();
600
+ let requesterIsAdmin = false;
601
+ if (requester) {
602
+ requesterScopes = await getScopes(redis, instance_id, requester).catch(() => new Set());
603
+ const reqRole = await getRole(redis, instance_id, requester).catch(() => null);
604
+ requesterIsAdmin = reqRole === 'admin';
605
+ }
588
606
  // ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
589
607
  const rawMatches = await keywordSearch(redis, ['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'], query, 10);
590
608
  // ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
@@ -758,13 +776,18 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
758
776
  }
759
777
  }
760
778
  }
761
- // Filter private lessons — they are only accessible via exact recall_best_solution
779
+ // Filter private lessons (recall_best_solution only) + enforce team scopes:
780
+ // group-scoped lessons surface only for members of that group (admins see all).
762
781
  const hybridResults = [...hybridMap.values()]
763
782
  .filter(r => {
764
783
  if (!r.key.startsWith('cachly:lesson:best:'))
765
784
  return true;
766
785
  const ld = safeJsonParse(r.content, {});
767
- return ld.visibility !== 'private';
786
+ if (ld.visibility === 'private')
787
+ return false;
788
+ if (!lessonVisibleToScope(ld.group, requesterScopes, requesterIsAdmin))
789
+ return false;
790
+ return true;
768
791
  })
769
792
  .sort((a, b) => b.hybridScore - a.hybridScore);
770
793
  // ── Build output ──────────────────────────────────────────────────────────