@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 +10 -15
- package/dist/index.js +2 -2
- package/dist/{server-LAXRBVKA.js → server-7JVOIBJV.js} +127 -45
- package/package.json +5 -3
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
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-
|
|
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/
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
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.
|
|
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 = "
|
|
255
|
+
function getToolDefinitions(profile = "lean") {
|
|
258
256
|
const all = getAllToolDefinitions();
|
|
259
|
-
if (profile
|
|
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, {
|
|
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
|
|
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 (
|
|
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
|
|
1545
|
-
if (
|
|
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}
|
|
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 ?? "
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
##
|
|
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
|
|
2550
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
|
2470
2551
|
});
|
|
2471
|
-
server.setRequestHandler(
|
|
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(
|
|
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
|
|
2613
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown prompt: ${name}`);
|
|
2533
2614
|
});
|
|
2534
2615
|
return server;
|
|
2535
2616
|
}
|
|
2536
2617
|
async function runStdio() {
|
|
2537
|
-
const
|
|
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.
|
|
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/
|
|
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.
|
|
19
|
+
"@plur-ai/core": "0.15.0"
|
|
18
20
|
},
|
|
19
21
|
"devDependencies": {
|
|
20
22
|
"@types/node": "^25.5.0"
|