@plur-ai/mcp 0.14.0 → 0.15.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.
package/README.md CHANGED
@@ -46,26 +46,23 @@ Knowledge is stored as **engrams** — small assertions that strengthen with use
46
46
 
47
47
  ## Tools
48
48
 
49
- Your agent gets these tools automatically:
49
+ By default (lean profile), your agent gets 11 tools. Everything else is reachable through `plur_admin`:
50
50
 
51
51
  | Tool | What it does |
52
52
  |------|-------------|
53
53
  | `plur_session_start` | Start a session — injects relevant engrams for your task |
54
54
  | `plur_learn` | Store a memory — correction, preference, convention, or decision |
55
- | `plur_learn_batch` | Store many memories in one call — same dedup + policy as `plur_learn`, with per-item failure isolation |
56
55
  | `plur_recall_hybrid` | **Best default** — BM25 + embeddings merged via RRF. Zero cost. |
57
- | `plur_recall` | Keyword search (BM25 only, instant) |
58
- | `plur_inject_hybrid` | Load relevant memories for the current task |
59
56
  | `plur_feedback` | Rate a memory — trains relevance over time |
60
57
  | `plur_forget` | Retire a memory (history preserved) |
61
58
  | `plur_session_end` | End a session — captures summary and new learnings |
62
- | `plur_capture` | Record a session event |
63
- | `plur_timeline` | Query session history |
64
- | `plur_ingest` | Extract learnings from text |
65
- | `plur_sync` | Sync memory across machines via git |
66
- | `plur_packs_install` | Install a shareable memory pack |
67
- | `plur_packs_list` | List installed packs |
68
59
  | `plur_status` | System health |
60
+ | `plur_doctor` | Diagnose embedder, hybrid search, and remote-store auth |
61
+ | `plur_packs_uninstall` | Remove an installed pack |
62
+ | `plur_tensions_purge` | Clear stale/resolved tensions |
63
+ | `plur_admin` | Dispatch to any other tool: `{ action: "plur_packs_install", args: {...} }` |
64
+
65
+ Less commonly needed tools (`plur_recall`, `plur_inject_hybrid`, `plur_learn_batch`, `plur_ingest`, `plur_sync`, `plur_packs_install`, `plur_packs_list`, `plur_capture`, `plur_timeline`, and more) are all reachable via `plur_admin`. Set `PLUR_TOOL_PROFILE=full` to expose all 40 tools directly.
69
66
 
70
67
  ## Sync across machines
71
68
 
@@ -102,11 +99,9 @@ Default: `~/.plur/`. Everything is plain YAML — open it, read it, edit it.
102
99
 
103
100
  ## Benchmark
104
101
 
105
- | Metric | Score |
106
- |--------|-------|
107
- | LongMemEval overall | **86.7%** |
108
- | A/B win rate vs no memory | 89% |
109
- | House rules accuracy | 100% |
102
+ **Retrieval** (LongMemEval R@5): **76.7%** out-of-the-box · **97.0%** with openai-3-large embeddings
103
+
104
+ **Agent task impact:** Haiku + PLUR outperforms Opus *without* memory at ~10× less cost. House rules: **12–0** across Haiku, Sonnet, Opus. A/B win rate: **89%**.
110
105
 
111
106
  [Full methodology →](https://plur.ai/benchmark.html)
112
107
 
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSy
5
5
  import { join } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { homedir, platform } from "os";
8
- var VERSION = "0.14.0";
8
+ var VERSION = "0.15.0";
9
9
  var HELP = `plur-mcp v${VERSION} \u2014 persistent memory for AI agents
10
10
 
11
11
  Usage:
@@ -351,7 +351,7 @@ if (arg === "packs") {
351
351
  process.exit(0);
352
352
  }
353
353
  if (arg === "serve" || arg === void 0) {
354
- const { runStdio } = await import("./server-LAXRBVKA.js");
354
+ const { runStdio } = await import("./server-7JVOIBJV.js");
355
355
  runStdio().catch((err) => {
356
356
  console.error("Failed to start PLUR MCP server:", err);
357
357
  process.exit(1);
@@ -1,16 +1,6 @@
1
1
  // src/server.ts
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import {
5
- ListToolsRequestSchema,
6
- CallToolRequestSchema,
7
- ListResourcesRequestSchema,
8
- ReadResourceRequestSchema,
9
- ListPromptsRequestSchema,
10
- GetPromptRequestSchema,
11
- ErrorCode,
12
- McpError
13
- } from "@modelcontextprotocol/sdk/types.js";
2
+ import { Server, ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
14
4
  import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
15
5
  import { join as join2 } from "path";
16
6
  import { homedir as homedir2 } from "os";
@@ -20,7 +10,7 @@ import { Plur as Plur2, checkForUpdate } from "@plur-ai/core";
20
10
  import { existsSync, unlinkSync } from "fs";
21
11
  import { join } from "path";
22
12
  import { homedir } from "os";
23
- import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, readProjectConfig, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName } from "@plur-ai/core";
13
+ import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, readProjectConfig, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName, SUGGEST_DISPLAY_MIN_CONFIDENCE } from "@plur-ai/core";
24
14
 
25
15
  // src/telemetry.ts
26
16
  import { recordEvent, flushIfNeeded, registerFlushOnExit } from "@plur-ai/core";
@@ -34,7 +24,7 @@ function recordTelemetry(event) {
34
24
  }
35
25
 
36
26
  // src/version.ts
37
- var VERSION = "0.14.0";
27
+ var VERSION = "0.15.0";
38
28
 
39
29
  // src/tools.ts
40
30
  import { z } from "zod";
@@ -208,6 +198,7 @@ var CURSOR_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
208
198
  "plur_feedback",
209
199
  "plur_forget",
210
200
  "plur_status",
201
+ "plur_receipt",
211
202
  "plur_doctor",
212
203
  "plur_packs_uninstall",
213
204
  "plur_tensions_purge"
@@ -240,6 +231,13 @@ function buildAdminDispatchTool(all) {
240
231
  if (!target) {
241
232
  return { error: `Unknown action "${action}". Valid actions: ${adminActions.join(", ")}`, success: false, _isError: true };
242
233
  }
234
+ if (target.annotations?.destructiveHint === true) {
235
+ return {
236
+ error: `"${action}" is a destructive operation and cannot be dispatched via plur_admin \u2014 call the ${action} tool directly (it is exposed in every profile) so your client sees its destructiveHint annotation.`,
237
+ success: false,
238
+ _isError: true
239
+ };
240
+ }
243
241
  const innerArgs = args.args ?? {};
244
242
  const validated = validateToolArgs(target, innerArgs);
245
243
  if (!validated.ok) {
@@ -254,12 +252,20 @@ function buildAdminDispatchTool(all) {
254
252
  }
255
253
  };
256
254
  }
257
- function getToolDefinitions(profile = "full") {
255
+ function getToolDefinitions(profile = "lean") {
258
256
  const all = getAllToolDefinitions();
259
- if (profile !== "cursor") return all;
257
+ if (profile === "full") return all;
260
258
  const core = all.filter((t) => CURSOR_CORE_TOOL_NAMES.has(t.name));
261
259
  return [...core, buildAdminDispatchTool(all)];
262
260
  }
261
+ function receiptSummary(r) {
262
+ if (r.coverage.source === "none") {
263
+ return r.window.windowed ? `No retrievals recorded in the last ${r.window.requested_days} days.` : "No retrieval history yet \u2014 logging begins once memory is used.";
264
+ }
265
+ const since = r.window.windowed ? `the last ${r.window.requested_days} days` : `${r.coverage.complete_from}`;
266
+ const pct = Math.round(r.retrieved.activation_rate * 100);
267
+ return `Since ${since}, ${r.retrieved.taught_pairs} times a memory the user taught was retrieved into context (across ${r.retrieved.retrievals} retrievals in ${r.window.sessions} sessions; ${r.retrieved.engrams} distinct engrams). Activation ${pct}% is store COVERAGE over the logging window, not a quality score \u2014 it is expected to be low and to fall as more engrams are added.`;
268
+ }
263
269
  function getAllToolDefinitions() {
264
270
  return [
265
271
  {
@@ -319,6 +325,18 @@ function getAllToolDefinitions() {
319
325
  const scopes = remote.map((s) => `"${s.scope}"`).join(", ");
320
326
  return { scope_hint: `Stored at "${engramScope}" because no scope was passed, but a team store is configured (${scopes}). If this is team/engineering knowledge, re-learn it with an explicit scope so it reaches the shared store; keep genuinely personal notes at the default scope.` };
321
327
  };
328
+ const domainHint = (wasRouted) => {
329
+ if (typeof args.domain === "string" && args.domain.length > 0) return {};
330
+ if (explicitScope || wasRouted) return {};
331
+ let coversScopes = [];
332
+ try {
333
+ coversScopes = plur.listScopeMetadata().filter((md) => (md.covers?.length ?? 0) > 0).map((md) => md.scope);
334
+ } catch {
335
+ return {};
336
+ }
337
+ if (coversScopes.length === 0) return {};
338
+ return { domain_hint: `No domain set \u2014 without a dotted domain this engram cannot auto-route to a covers-declaring scope (${coversScopes.join(", ")}) and is harder to re-scope later. Set domain on every plur_learn, shape "<org>.<team>.<area>" (e.g. "plur.engineering.mcp") \u2014 see the domain convention in CLAUDE.md.` };
339
+ };
322
340
  const temporalEcho = (engram) => {
323
341
  const extracted = engram.structured_data?._expiry_extracted;
324
342
  return {
@@ -344,6 +362,7 @@ function getAllToolDefinitions() {
344
362
  decision: "ADD",
345
363
  ...temporalEcho(engram),
346
364
  ...scopeHint(engram.scope, !!routed),
365
+ ...domainHint(!!routed),
347
366
  ...isOutbox ? { outbox: true, warning: "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {},
348
367
  ...demoted ? { demoted: true, requested_scope: demoted.from, warning: `Sensitive content (${demoted.patterns}) detected \u2014 stored at "${demoted.to}"/private instead of the requested shared scope "${demoted.from}". If this is a false positive, re-scope deliberately.` } : {},
349
368
  ...routed ? { routed: { scope: routed.scope, confidence: routed.confidence, reason: routed.reason }, info: `No scope was provided; auto-routed to "${routed.scope}" (confidence ${routed.confidence}) because its content matched that scope's covers. Pass an explicit scope to override.` } : {}
@@ -362,6 +381,7 @@ function getAllToolDefinitions() {
362
381
  decision: "ADD",
363
382
  ...temporalEcho(engram),
364
383
  ...scopeHint(engram.scope, !!routedFallback),
384
+ ...domainHint(!!routedFallback),
365
385
  ...isOutbox ? { outbox: true } : {},
366
386
  warning: `Remote write failed (${err.message}); engram queued for retry.`
367
387
  };
@@ -433,6 +453,17 @@ function getAllToolDefinitions() {
433
453
  for (const r of results) {
434
454
  if (r.input_index !== void 0) ids[r.input_index] = r.engram.id;
435
455
  }
456
+ let batchDomainHint = {};
457
+ const noDomainCount = raw.filter((e) => !(typeof e.domain === "string" && e.domain.length > 0) && !(typeof e.scope === "string" && e.scope.length > 0)).length;
458
+ if (noDomainCount > 0) {
459
+ try {
460
+ const coversScopes = plur.listScopeMetadata().filter((md) => (md.covers?.length ?? 0) > 0).map((md) => md.scope);
461
+ if (coversScopes.length > 0) {
462
+ batchDomainHint = { domain_hint: `${noDomainCount} of ${raw.length} item(s) had no domain and no explicit scope \u2014 they cannot auto-route to a covers-declaring scope (${coversScopes.join(", ")}) and are harder to re-scope later. Set domain on every item, shape "<org>.<team>.<area>" \u2014 see the domain convention in CLAUDE.md.` };
463
+ }
464
+ } catch {
465
+ }
466
+ }
436
467
  return {
437
468
  ids,
438
469
  results: results.map((r) => ({
@@ -445,6 +476,7 @@ function getAllToolDefinitions() {
445
476
  ...r.existing_id ? { existing_id: r.existing_id } : {}
446
477
  })),
447
478
  stats,
479
+ ...batchDomainHint,
448
480
  ...failures.length > 0 ? { failures, warning: `${failures.length} of ${raw.length} engram(s) failed to persist; the rest were written.` } : {}
449
481
  };
450
482
  }
@@ -589,7 +621,9 @@ function getAllToolDefinitions() {
589
621
  handler: async (args, plur) => {
590
622
  const result = plur.inject(args.task, {
591
623
  budget: args.budget,
592
- scope: args.scope
624
+ scope: args.scope,
625
+ source: "inject",
626
+ session_id: _activeSessionId
593
627
  });
594
628
  _recordInjectionTelemetry(_activeSessionId, result.injected_packs);
595
629
  return {
@@ -619,7 +653,9 @@ function getAllToolDefinitions() {
619
653
  handler: async (args, plur) => {
620
654
  const result = await plur.injectHybrid(args.task, {
621
655
  budget: args.budget,
622
- scope: args.scope
656
+ scope: args.scope,
657
+ source: "inject",
658
+ session_id: _activeSessionId
623
659
  });
624
660
  _recordInjectionTelemetry(_activeSessionId, result.injected_packs);
625
661
  return {
@@ -970,11 +1006,19 @@ function getAllToolDefinitions() {
970
1006
  full: {
971
1007
  type: "boolean",
972
1008
  description: "Full reindex: drop the derived index (PGLite/SQLite) and rebuild from YAML. YAML is never modified. Use to recover from an out-of-sync index."
1009
+ },
1010
+ remote_type: {
1011
+ type: "string",
1012
+ enum: ["personal", "shared"],
1013
+ description: "What the sync remote is for (#640). personal (default): mirror everything non-local, private included \u2014 a solo user's own backup. shared: push ONLY shared-scope, non-private engrams \u2014 personal-family and private engrams never reach the remote. Persist the choice in config.yaml as sync.remote_type instead of passing it per call."
973
1014
  }
974
1015
  }
975
1016
  },
976
1017
  handler: async (args, plur) => {
977
- const result = plur.sync(args.remote, { full: args.full === true });
1018
+ const result = plur.sync(args.remote, {
1019
+ full: args.full === true,
1020
+ ...args.remote_type === "personal" || args.remote_type === "shared" ? { remoteType: args.remote_type } : {}
1021
+ });
978
1022
  await plur.waitForIndex();
979
1023
  const indexError = plur.lastIndexError();
980
1024
  let outbox_result;
@@ -1206,6 +1250,22 @@ function getAllToolDefinitions() {
1206
1250
  };
1207
1251
  }
1208
1252
  },
1253
+ {
1254
+ name: "plur_receipt",
1255
+ description: 'Counted report of what your memory retrieved for you: engrams stored, how many were retrieved and how often, which are most relied on, and how much of the store is dormant. Local and read-only; every figure is directly counted, never estimated. IMPORTANT when relaying to the user: `activation_rate` is COVERAGE over the logging window (\u2248 how much of the store was surfaced), NOT a quality or effectiveness score \u2014 it is naturally low and FALLS as more engrams are added, so never present it as "memory is N% effective". A `summary` line is included; prefer relaying that.',
1256
+ annotations: { title: "Memory receipt", readOnlyHint: true, idempotentHint: true },
1257
+ inputSchema: {
1258
+ type: "object",
1259
+ properties: {
1260
+ days: { type: "number", description: "Restrict to the last N days (integer). Omit for all recorded history." }
1261
+ }
1262
+ },
1263
+ handler: async (args, plur) => {
1264
+ const days = typeof args.days === "number" && Number.isFinite(args.days) && args.days >= 1 ? Math.floor(args.days) : void 0;
1265
+ const receipt = plur.receipt(days ? { days } : void 0);
1266
+ return { summary: receiptSummary(receipt), ...receipt };
1267
+ }
1268
+ },
1209
1269
  {
1210
1270
  name: "plur_doctor",
1211
1271
  description: 'Diagnose the PLUR ENGINE (embedder, hybrid search, remote-store auth) \u2014 not hook/MCP wiring. Reports whether the embedding model loaded, whether hybrid search is fully operational, and \u2014 for any configured enterprise/remote store \u2014 whether its auth is valid (probes /api/v1/me and decodes token expiry), so a dead or soon-to-expire token surfaces instead of hiding behind a "healthy" report. Run this first when recall feels off or team engrams stop syncing. Does NOT check .cursor/mcp.json, .cursor/hooks.json, or the live MCP tool count \u2014 for that, run the `plur doctor` CLI command in a terminal (a different, more thorough check with the same name).',
@@ -1456,8 +1516,9 @@ function getAllToolDefinitions() {
1456
1516
  try {
1457
1517
  const result = await plur.injectHybrid(task, {
1458
1518
  scope: tags?.length ? `tags:${tags.join(",")}` : void 0,
1459
- session_id
1519
+ session_id,
1460
1520
  // stamped on the co_injection provenance event (#452)
1521
+ source: "session_start"
1461
1522
  });
1462
1523
  _recordInjectionTelemetry(session_id, result.injected_packs);
1463
1524
  if (result.count > 0) {
@@ -1470,7 +1531,8 @@ function getAllToolDefinitions() {
1470
1531
  } catch {
1471
1532
  const result = plur.inject(task, {
1472
1533
  scope: tags?.length ? `tags:${tags.join(",")}` : void 0,
1473
- session_id
1534
+ session_id,
1535
+ source: "session_start"
1474
1536
  });
1475
1537
  _recordInjectionTelemetry(session_id, result.injected_packs);
1476
1538
  if (result.count > 0) {
@@ -1512,7 +1574,7 @@ Auto-detected project scope: "${default_scope}" (from .plur.yaml in the current
1512
1574
  } else if (scope_source === "none") {
1513
1575
  guide += `
1514
1576
 
1515
- \u26A0\uFE0F No project scope detected. plur_learn calls without explicit scope will be tagged "global" and will appear in EVERY project's future sessions. Create a .plur.yaml NOW to prevent this: scope: "project:<your-project-name>". (This is every project's PERSONAL recall context, NOT team shared stores \u2014 use an explicit shared scope like project:/group: to reach a team store.) Note: an explicit scope=global RECALL surfaces all your personal engrams, but scope=global INJECT is targeted to the global namespace only \u2014 don't be surprised if a local engram a global recall finds is absent from a global inject.`;
1577
+ \u26A0\uFE0F No project scope detected. plur_learn calls without explicit scope may AUTO-ROUTE to a registered team scope whose covers confidently match the engram's domain/tags (the response reports \`routed\` when that happens); otherwise they land at the unscoped default "global" and will appear in EVERY project's future sessions. Create a .plur.yaml NOW to prevent this: scope: "project:<your-project-name>". (This is every project's PERSONAL recall context, NOT team shared stores \u2014 use an explicit shared scope like project:/group: to reach a team store.) Note: an explicit scope=global RECALL surfaces all your personal engrams, but scope=global INJECT is targeted to the global namespace only \u2014 don't be surprised if a local engram a global recall finds is absent from a global inject.`;
1516
1578
  }
1517
1579
  if (remote_scopes.length > 0) {
1518
1580
  const safe = (x) => String(x ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
@@ -1527,9 +1589,10 @@ Auto-detected project scope: "${default_scope}" (from .plur.yaml in the current
1527
1589
 
1528
1590
  Session default scope is set to "${default_scope}". To route an engram to a remote enterprise store instead, pass scope explicitly to plur_learn (available remote scopes: ${scopeList}).` : `
1529
1591
 
1530
- Remote store scopes available: ${scopeList}. Set scope PER ENGRAM by content: when an engram is relevant to the team (engineering patterns, architecture decisions, project conventions), set scope to the matching remote scope in plur_learn. Personal preferences, local project details, and corrections specific to your workflow can be left unscoped (they land at the unscoped default, "global" \u2014 the cross-project personal namespace). Do NOT let TEAM knowledge fall back to "global" \u2014 without an explicit scope it will, and it will never reach the shared store.`;
1592
+ Remote store scopes available: ${scopeList}. Set scope PER ENGRAM by content: when an engram is relevant to the team (engineering patterns, architecture decisions, project conventions), set scope to the matching remote scope in plur_learn. Personal preferences, local project details, and corrections specific to your workflow can be left unscoped \u2014 but note an unscoped write whose domain/tags confidently match a team scope's covers AUTO-ROUTES to that shared team store (the response reports \`routed\` when that happens); otherwise it lands at the unscoped default, "global" \u2014 the cross-project personal namespace. Do NOT rely on auto-routing for TEAM knowledge \u2014 set the matching scope explicitly; a weak or absent covers match falls back to "global" and never reaches the shared store.`;
1531
1593
  try {
1532
1594
  const discoveries = await plur.discoverRemoteScopes({ timeoutMs: 3e3 });
1595
+ plur.persistScopeMetadata(discoveries);
1533
1596
  const failures = discoveries.filter((d) => !d.ok);
1534
1597
  if (failures.length > 0) {
1535
1598
  const authExpired = failures.some((f) => /\b40[13]\b/.test(f.error ?? ""));
@@ -1541,12 +1604,11 @@ Remote store scopes available: ${scopeList}. Set scope PER ENGRAM by content: wh
1541
1604
 
1542
1605
  \u26A0\uFE0F ENTERPRISE STORE UNREACHABLE: ${urls}. Reads fall back to local; team-scoped writes queue in the outbox` + (pending > 0 ? ` (${pending} pending)` : "") + ` until it recovers. Check connectivity/VPN.`;
1543
1606
  }
1544
- const unregistered = [...new Set(discoveries.filter((d) => d.ok).flatMap((d) => d.unregistered))];
1545
- if (unregistered.length > 0) {
1546
- const list = unregistered.map((s) => `"${safe(s)}"`).join(", ");
1607
+ const offerable = [...new Set(discoveries.filter((d) => d.ok).flatMap((d) => d.unregistered))].filter(isSharedScope);
1608
+ if (offerable.length > 0) {
1547
1609
  guide += `
1548
1610
 
1549
- \u{1F50E} Your token is authorized for ${unregistered.length} more scope(s) not yet registered: ${list}. Call plur_scopes_discover with register:true to add them all in one step.`;
1611
+ \u{1F50E} ${offerable.length} authorized scope(s) not yet registered. Tell the user they can run \`plur scopes\` to register or dismiss them per-scope (dismissed scopes stop being offered; \`plur scopes --reoffer\` re-surfaces them).`;
1550
1612
  }
1551
1613
  } catch {
1552
1614
  }
@@ -1753,29 +1815,33 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1753
1815
  },
1754
1816
  {
1755
1817
  name: "plur_suggest_scope",
1756
- description: 'Suggest which registered scope(s) an engram belongs in, ranked by fit. Deterministic \u2014 no LLM, no network. Scores the statement keywords, optional domain (a dotted namespace like "plur.core.security"), and tags against the covers[] each scope declares. ADVISORY ONLY: this does not route or store anything; pass the chosen scope to plur_learn yourself. Returns candidates sorted by confidence (empty when nothing matches).',
1818
+ description: 'Suggest which registered scope(s) an engram belongs in, ranked by fit. Deterministic \u2014 no LLM, no network. Scores the statement keywords, optional domain (a dotted namespace like "plur.core.security"), and tags against the covers[] each scope declares. ADVISORY ONLY: this does not route or store anything; pass the chosen scope to plur_learn yourself. Returns candidates sorted by confidence (empty when nothing matches). Candidates below min_confidence (default: scope_routing.min_confidence config, else 0.15) are suppressed \u2014 a lone coincidental keyword scores \u22480.12 and is noise, not signal (#670); pass min_confidence: 0 to see every scored candidate.',
1757
1819
  annotations: { title: "Suggest scope", readOnlyHint: true, idempotentHint: true },
1758
1820
  inputSchema: {
1759
1821
  type: "object",
1760
1822
  properties: {
1761
1823
  statement: { type: "string", description: "The engram statement to route" },
1762
1824
  domain: { type: "string", description: 'Optional dotted namespace for the engram (e.g. "plur.core.security") \u2014 strongest routing signal' },
1763
- tags: { type: "array", items: { type: "string" }, description: "Optional tags on the engram" }
1825
+ tags: { type: "array", items: { type: "string" }, description: "Optional tags on the engram" },
1826
+ min_confidence: { type: "number", minimum: 0, maximum: 1, description: "Suppress candidates below this confidence (0-1; out-of-range values are clamped). Default: scope_routing.min_confidence from config, else 0.15 \u2014 clips lone-keyword noise (\u22480.12) while keeping real multi-signal matches. Pass 0 for the unfiltered list." }
1764
1827
  },
1765
1828
  required: ["statement"]
1766
1829
  },
1767
1830
  handler: async (args, plur) => {
1831
+ const raw = args.min_confidence;
1832
+ const explicit = typeof raw === "number" && Number.isFinite(raw) ? Math.min(1, Math.max(0, raw)) : void 0;
1833
+ const minConfidence = explicit ?? plur.getScopeRoutingConfig().min_confidence ?? SUGGEST_DISPLAY_MIN_CONFIDENCE;
1768
1834
  const candidates = plur.suggestScope({
1769
1835
  statement: args.statement,
1770
1836
  domain: args.domain,
1771
1837
  tags: args.tags
1772
- });
1773
- return { candidates, count: candidates.length };
1838
+ }, { minConfidence });
1839
+ return { candidates, count: candidates.length, min_confidence: minConfidence };
1774
1840
  }
1775
1841
  },
1776
1842
  {
1777
1843
  name: "plur_scopes_discover",
1778
- description: "Discover which scopes your remote token is authorized for via the enterprise server (GET /api/v1/me), and which of those are not yet registered locally. Read-only by default; pass register:true to register all authorized-but-unregistered scopes in one step. Only shared-family scopes (group:/project:/space:/team:/org:/public) are auto-registered \u2014 personal-family scopes (global/local/user:*/agent:*) advertised by /me are skipped and surfaced in the result. Use this when you have access to multiple team scopes on one server.",
1844
+ description: "Discover which scopes your remote token is authorized for via the enterprise server (GET /api/v1/me), and which of those are not yet registered locally. Read-only by default; pass register:true to register all authorized-but-unregistered scopes in one step. Only shared-family scopes (group:/project:/space:/team:/org:/public) are auto-registered \u2014 personal-family scopes (global/local/user:*/agent:*) advertised by /me are skipped and surfaced in the result, and scopes the user has dismissed are respected (NOT registered by the batch path; register one individually via the CLI `plur scopes register <scope>` to override, which also clears the dismissal). Use this when you have access to multiple team scopes on one server.",
1779
1845
  annotations: { title: "Discover scopes", readOnlyHint: false, idempotentHint: true },
1780
1846
  inputSchema: {
1781
1847
  type: "object",
@@ -2237,6 +2303,8 @@ var INSTRUCTIONS = `PLUR is your persistent memory. Corrections, preferences, an
2237
2303
 
2238
2304
  PLUR is a GLOBAL tool \u2014 one MCP server, one engram store (~/.plur/), available in every project. Multi-project scoping uses domain/scope fields on engrams, not separate installations.
2239
2305
 
2306
+ TOOL PROFILE: by default only the core session tools are exposed directly (lean profile). Every other plur_* operation is reachable via plur_admin: { action: "<tool name>", args: {...} } \u2014 same arguments and validation as a direct call. PLUR_TOOL_PROFILE=full exposes everything directly.
2307
+
2240
2308
  SESSION LIFECYCLE:
2241
2309
  - With hooks installed (plur init): engrams are injected automatically on first message. You do NOT need to call plur_session_start \u2014 it happens via hooks. Just call plur_session_end before the conversation ends.
2242
2310
  - Without hooks: call plur_session_start at the start, plur_session_end at the end.
@@ -2291,6 +2359,7 @@ Persistent memory for AI agents. Corrections, preferences, and conventions are s
2291
2359
  | A recalled engram was wrong or irrelevant | \`plur_feedback\` with "negative" |
2292
2360
  | User says "forget X" or a memory is outdated | \`plur_forget\` |
2293
2361
  | You need to check what's stored | \`plur_status\` or \`plur_packs_list\` |
2362
+ | User asks what memory did for them / is memory working | \`plur_receipt\` (relay its \`summary\`; activation_rate is coverage, not quality) |
2294
2363
  | End of session | \`plur_session_end\` with summary and suggestions |
2295
2364
 
2296
2365
  ## Tool Categories
@@ -2331,6 +2400,7 @@ Persistent memory for AI agents. Corrections, preferences, and conventions are s
2331
2400
  - **plur_sync** \u2014 sync engrams across devices via git
2332
2401
  - **plur_sync_status** \u2014 check sync state
2333
2402
  - **plur_status** \u2014 system health
2403
+ - **plur_receipt** \u2014 counted report of what memory retrieved for the user (local, read-only)
2334
2404
 
2335
2405
  ## Scoping
2336
2406
 
@@ -2352,7 +2422,7 @@ Override with \`PLUR_PATH\` environment variable.
2352
2422
  `;
2353
2423
  async function createServer(plur, options) {
2354
2424
  const instance = plur ?? new Plur2();
2355
- const tools = getToolDefinitions(options?.profile ?? "full");
2425
+ const tools = getToolDefinitions(options?.profile ?? "lean");
2356
2426
  checkForUpdate("@plur-ai/mcp", VERSION, (r) => {
2357
2427
  if (r.updateAvailable) {
2358
2428
  console.error(`[plur] Update available: ${r.current} \u2192 ${r.latest}. Run: npx @plur-ai/mcp@latest`);
@@ -2370,7 +2440,7 @@ async function createServer(plur, options) {
2370
2440
  instructions: INSTRUCTIONS
2371
2441
  }
2372
2442
  );
2373
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
2443
+ server.setRequestHandler("tools/list", async () => ({
2374
2444
  tools: tools.map((t) => ({
2375
2445
  name: t.name,
2376
2446
  description: t.description,
@@ -2378,9 +2448,20 @@ async function createServer(plur, options) {
2378
2448
  ...t.annotations && { annotations: t.annotations }
2379
2449
  }))
2380
2450
  }));
2381
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
2451
+ server.setRequestHandler("tools/call", async (request) => {
2382
2452
  const tool = tools.find((t) => t.name === request.params.name);
2383
2453
  if (!tool) {
2454
+ const hidden = getToolDefinitions("full").find((t) => t.name === request.params.name);
2455
+ if (hidden) {
2456
+ return {
2457
+ content: [{ type: "text", text: JSON.stringify({
2458
+ error: `Tool "${request.params.name}" exists but is not directly callable under the current tool profile.`,
2459
+ success: false,
2460
+ hint: `Call it via plur_admin: { action: "${request.params.name}", args: { ... } } \u2014 same arguments, same validation, same result. To expose all tools directly, set PLUR_TOOL_PROFILE=full.`
2461
+ }) }],
2462
+ isError: true
2463
+ };
2464
+ }
2384
2465
  return {
2385
2466
  content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${request.params.name}`, success: false }) }],
2386
2467
  isError: true
@@ -2418,7 +2499,7 @@ async function createServer(plur, options) {
2418
2499
  };
2419
2500
  }
2420
2501
  });
2421
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
2502
+ server.setRequestHandler("resources/list", async () => ({
2422
2503
  resources: [
2423
2504
  {
2424
2505
  uri: "plur://guide",
@@ -2434,14 +2515,14 @@ async function createServer(plur, options) {
2434
2515
  }
2435
2516
  ]
2436
2517
  }));
2437
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2518
+ server.setRequestHandler("resources/read", async (request) => {
2438
2519
  const uri = request.params.uri;
2439
2520
  if (uri === "plur://guide") {
2440
- const cursorNote = options?.profile === "cursor" ? `
2521
+ const cursorNote = options?.profile === "cursor" || options?.profile === "lean" || options?.profile == null ? `
2441
2522
 
2442
- ## Cursor tool profile
2523
+ ## Lean tool profile (default)
2443
2524
 
2444
- Most tools above are NOT directly callable in this session \u2014 only ${[...CURSOR_CORE_TOOL_NAMES].join(", ")} are top-level tools here. Everything else in this guide is reachable through **plur_admin**: call it with \`{ action: "<tool name above>", args: {...} }\`.` : "";
2525
+ Most tools above are NOT directly callable in this session \u2014 only ${[...CURSOR_CORE_TOOL_NAMES].join(", ")} are top-level tools here. Everything else in this guide is reachable through **plur_admin**: call it with \`{ action: "<tool name above>", args: {...} }\`. Set \`PLUR_TOOL_PROFILE=full\` to expose all ${getToolDefinitions("full").length} tools directly.` : "";
2445
2526
  return {
2446
2527
  contents: [{
2447
2528
  uri: "plur://guide",
@@ -2466,9 +2547,9 @@ Most tools above are NOT directly callable in this session \u2014 only ${[...CUR
2466
2547
  }]
2467
2548
  };
2468
2549
  }
2469
- throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
2550
+ throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
2470
2551
  });
2471
- server.setRequestHandler(ListPromptsRequestSchema, async () => ({
2552
+ server.setRequestHandler("prompts/list", async () => ({
2472
2553
  prompts: [
2473
2554
  {
2474
2555
  name: "plur-getting-started",
@@ -2484,7 +2565,7 @@ Most tools above are NOT directly callable in this session \u2014 only ${[...CUR
2484
2565
  }
2485
2566
  ]
2486
2567
  }));
2487
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
2568
+ server.setRequestHandler("prompts/get", async (request) => {
2488
2569
  const name = request.params.name;
2489
2570
  if (name === "plur-getting-started") {
2490
2571
  const status = instance.status();
@@ -2529,12 +2610,13 @@ Please:
2529
2610
  }]
2530
2611
  };
2531
2612
  }
2532
- throw new McpError(ErrorCode.InvalidRequest, `Unknown prompt: ${name}`);
2613
+ throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown prompt: ${name}`);
2533
2614
  });
2534
2615
  return server;
2535
2616
  }
2536
2617
  async function runStdio() {
2537
- const profile = process.env.PLUR_TOOL_PROFILE === "cursor" ? "cursor" : "full";
2618
+ const envProfile = process.env.PLUR_TOOL_PROFILE;
2619
+ const profile = envProfile === "full" ? "full" : envProfile === "cursor" ? "cursor" : "lean";
2538
2620
  const server = await createServer(void 0, { profile });
2539
2621
  registerFlushOnExit({});
2540
2622
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plur-ai/mcp",
3
3
  "mcpName": "io.github.plur-ai/plur",
4
- "version": "0.14.0",
4
+ "version": "0.15.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "plur-mcp": "dist/index.js"
@@ -12,9 +12,11 @@
12
12
  "packs"
13
13
  ],
14
14
  "dependencies": {
15
- "@modelcontextprotocol/sdk": "^1.12.0",
15
+ "@modelcontextprotocol/server": "2.0.0-beta.4",
16
+ "@modelcontextprotocol/client": "2.0.0-beta.4",
17
+ "@modelcontextprotocol/core": "2.0.0-beta.4",
16
18
  "zod": "^3.23.0",
17
- "@plur-ai/core": "0.14.0"
19
+ "@plur-ai/core": "0.15.0"
18
20
  },
19
21
  "devDependencies": {
20
22
  "@types/node": "^25.5.0"