@plur-ai/mcp 0.11.0 → 0.12.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 +1 -0
- package/dist/index.js +9 -3
- package/dist/{server-M7GBKD4M.js → server-6CNGJXSW.js} +285 -84
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Your agent gets these tools automatically:
|
|
|
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 |
|
|
55
56
|
| `plur_recall_hybrid` | **Best default** — BM25 + embeddings merged via RRF. Zero cost. |
|
|
56
57
|
| `plur_recall` | Keyword search (BM25 only, instant) |
|
|
57
58
|
| `plur_inject_hybrid` | Load relevant memories for the current task |
|
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.12.0";
|
|
9
9
|
var HELP = `plur-mcp v${VERSION} \u2014 persistent memory for AI agents
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
@@ -65,6 +65,12 @@ var PLUR_HOOKS = {
|
|
|
65
65
|
matcher: "auto|manual",
|
|
66
66
|
hooks: [{ type: "command", command: `${CLI} hook-inject --rehydrate`, timeout: 15 }]
|
|
67
67
|
}],
|
|
68
|
+
// Auto-close the memory lifecycle at session end (Claude Code SessionEnd,
|
|
69
|
+
// shipped v1.0.85) — captures a closing episode and cleans up the session
|
|
70
|
+
// checkpoint even if the agent forgot to call plur_session_end (#217).
|
|
71
|
+
SessionEnd: [{
|
|
72
|
+
hooks: [{ type: "command", command: `${CLI} hook-session-end`, timeout: 5 }]
|
|
73
|
+
}],
|
|
68
74
|
// --- Contextual injection ---
|
|
69
75
|
PreToolUse: [
|
|
70
76
|
{ matcher: "EnterPlanMode", hooks: [{ type: "command", command: `${CLI} hook-inject --event plan_mode`, timeout: 10 }] },
|
|
@@ -98,7 +104,7 @@ Hooks inject engrams automatically on every first message \u2014 you do not need
|
|
|
98
104
|
2. **Learn**: When corrected or discovering something new, call \`plur_learn\` immediately
|
|
99
105
|
3. **Recall**: Before answering factual questions, call \`plur_recall_hybrid\` \u2014 check memory first
|
|
100
106
|
4. **Feedback**: Rate injected engrams with \`plur_feedback\` (positive/negative) \u2014 trains relevance
|
|
101
|
-
5. **End**: Call \`plur_session_end\` with summary + engram_suggestions
|
|
107
|
+
5. **End**: Call \`plur_session_end\` with summary + engram_suggestions \u2014 a SessionEnd hook auto-closes the lifecycle if you forget, but calling it yourself captures higher-quality learnings
|
|
102
108
|
|
|
103
109
|
Do not ask permission to use these tools \u2014 they are your memory system.
|
|
104
110
|
|
|
@@ -279,7 +285,7 @@ if (arg === "init") {
|
|
|
279
285
|
process.exit(0);
|
|
280
286
|
}
|
|
281
287
|
if (arg === "serve" || arg === void 0) {
|
|
282
|
-
const { runStdio } = await import("./server-
|
|
288
|
+
const { runStdio } = await import("./server-6CNGJXSW.js");
|
|
283
289
|
runStdio().catch((err) => {
|
|
284
290
|
console.error("Failed to start PLUR MCP server:", err);
|
|
285
291
|
process.exit(1);
|
|
@@ -31,9 +31,10 @@ function recordTelemetry(event) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// src/version.ts
|
|
34
|
-
var VERSION = "0.
|
|
34
|
+
var VERSION = "0.12.0";
|
|
35
35
|
|
|
36
36
|
// src/tools.ts
|
|
37
|
+
import { z } from "zod";
|
|
37
38
|
function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
|
|
38
39
|
return async (prompt) => {
|
|
39
40
|
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
@@ -55,6 +56,73 @@ function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
|
|
|
55
56
|
return data.choices?.[0]?.message?.content ?? "";
|
|
56
57
|
};
|
|
57
58
|
}
|
|
59
|
+
function jsonSchemaPropToZod(prop) {
|
|
60
|
+
if (!prop || typeof prop !== "object") return z.unknown();
|
|
61
|
+
const variants = prop.anyOf ?? prop.oneOf;
|
|
62
|
+
if (Array.isArray(variants) && variants.length > 0) {
|
|
63
|
+
const zodVariants = variants.map(jsonSchemaPropToZod);
|
|
64
|
+
if (zodVariants.length === 1) return zodVariants[0];
|
|
65
|
+
return z.union(zodVariants);
|
|
66
|
+
}
|
|
67
|
+
if (prop.type === "string") return prop.enum ? z.enum(prop.enum) : z.string();
|
|
68
|
+
if (prop.type === "number" || prop.type === "integer") return z.number();
|
|
69
|
+
if (prop.type === "boolean") return z.boolean();
|
|
70
|
+
if (prop.type === "array") {
|
|
71
|
+
const itemSchema = prop.items ? jsonSchemaPropToZod(prop.items) : z.unknown();
|
|
72
|
+
return z.preprocess((val) => {
|
|
73
|
+
if (typeof val !== "string") return val;
|
|
74
|
+
const trimmed = val.trim();
|
|
75
|
+
if (trimmed.startsWith("[")) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(trimmed);
|
|
78
|
+
return Array.isArray(parsed) ? parsed : val;
|
|
79
|
+
} catch {
|
|
80
|
+
return val;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (prop.items?.type === "string") {
|
|
84
|
+
return trimmed.length === 0 ? [] : trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
85
|
+
}
|
|
86
|
+
return val;
|
|
87
|
+
}, z.array(itemSchema));
|
|
88
|
+
}
|
|
89
|
+
if (prop.type === "object" && prop.properties) {
|
|
90
|
+
const shape = {};
|
|
91
|
+
for (const [k, p] of Object.entries(prop.properties)) {
|
|
92
|
+
const field = jsonSchemaPropToZod(p);
|
|
93
|
+
shape[k] = prop.required?.includes(k) ? field : field.optional();
|
|
94
|
+
}
|
|
95
|
+
return z.object(shape).passthrough();
|
|
96
|
+
}
|
|
97
|
+
return z.unknown();
|
|
98
|
+
}
|
|
99
|
+
function validateToolArgs(tool, rawArgs) {
|
|
100
|
+
const schema = tool.inputSchema;
|
|
101
|
+
if (!schema?.properties) return { ok: true, data: rawArgs };
|
|
102
|
+
const shape = {};
|
|
103
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
104
|
+
const field = jsonSchemaPropToZod(prop);
|
|
105
|
+
shape[key] = schema.required?.includes(key) ? field : field.optional();
|
|
106
|
+
}
|
|
107
|
+
const parsed = z.object(shape).passthrough().safeParse(rawArgs);
|
|
108
|
+
if (!parsed.success) {
|
|
109
|
+
const receivedFields = Object.keys(rawArgs);
|
|
110
|
+
const details = parsed.error.issues.map((i) => `${i.path.join(".") || "root"}: ${i.message}`).join(", ");
|
|
111
|
+
const hasArrayParam = Object.values(schema.properties ?? {}).some((p) => p?.type === "array");
|
|
112
|
+
const arrayBugHint = receivedFields.length === 0 && hasArrayParam ? ' Known client-side bug (plur-ai/plur#297): some MCP clients drop the entire arguments payload when an array-typed parameter is included. Retry passing array parameters as a JSON string (e.g. tags: "[\\"a\\",\\"b\\"]") or a comma-separated string (tags: "a, b") \u2014 the server coerces both back into arrays.' : "";
|
|
113
|
+
const receivedNote = receivedFields.length > 0 ? `Received fields: [${receivedFields.join(", ")}].` : "Received no fields (the arguments object was empty).";
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
errorPayload: {
|
|
117
|
+
error: `Invalid arguments: ${details}. ${receivedNote} The call reached the server \u2014 this is a malformed-arguments error, not a transport failure. Fix the field(s) named above and retry; do not abandon the call.` + arrayBugHint,
|
|
118
|
+
success: false,
|
|
119
|
+
received_fields: receivedFields,
|
|
120
|
+
_isError: true
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { ok: true, data: parsed.data };
|
|
125
|
+
}
|
|
58
126
|
var PLUR_GUIDE_EMPTY = `## PLUR \u2014 Empty Store
|
|
59
127
|
|
|
60
128
|
You have **0 engrams**. This session's learnings will be lost unless you store them.
|
|
@@ -111,7 +179,67 @@ mcpCanary.expect({
|
|
|
111
179
|
description: "Learning from corrections",
|
|
112
180
|
fix: "Call plur_learn when corrected. If using hooks, verify they are installed."
|
|
113
181
|
});
|
|
114
|
-
|
|
182
|
+
var CURSOR_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
183
|
+
"plur_session_start",
|
|
184
|
+
"plur_session_end",
|
|
185
|
+
"plur_learn",
|
|
186
|
+
"plur_recall_hybrid",
|
|
187
|
+
"plur_feedback",
|
|
188
|
+
"plur_forget",
|
|
189
|
+
"plur_status",
|
|
190
|
+
"plur_doctor",
|
|
191
|
+
"plur_packs_uninstall",
|
|
192
|
+
"plur_tensions_purge"
|
|
193
|
+
]);
|
|
194
|
+
function buildAdminDispatchTool(all) {
|
|
195
|
+
const byName = new Map(all.map((t) => [t.name, t]));
|
|
196
|
+
const adminActions = all.map((t) => t.name).filter((n) => !CURSOR_CORE_TOOL_NAMES.has(n)).sort();
|
|
197
|
+
return {
|
|
198
|
+
name: "plur_admin",
|
|
199
|
+
description: `Dispatch for less-common PLUR operations (packs, sync, tensions, stores, timeline, ingest, and more), collapsed into one tool so Cursor's ~40-tool-per-workspace limit is not exhausted by PLUR alone. Set "action" to the underlying tool name and "args" to that tool's normal arguments. Valid actions: ${adminActions.join(", ")}.`,
|
|
200
|
+
annotations: { title: "Admin dispatch", readOnlyHint: false },
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
// No `enum` here — an invalid action must reach the handler's custom
|
|
205
|
+
// Unknown-action message with the full valid-actions list, not fail
|
|
206
|
+
// at top-level schema validation with a generic "Invalid arguments"
|
|
207
|
+
// error. If `enum: adminActions` were set, the top-level
|
|
208
|
+
// CallToolRequestSchema handler's Zod validation would reject
|
|
209
|
+
// unknown actions before this handler's `if (!target)` branch ever
|
|
210
|
+
// ran.
|
|
211
|
+
action: { type: "string", description: "Which underlying plur_* tool to invoke" },
|
|
212
|
+
args: { type: "object", description: "Arguments for the chosen action, matching that tool's normal input schema", additionalProperties: true }
|
|
213
|
+
},
|
|
214
|
+
required: ["action"]
|
|
215
|
+
},
|
|
216
|
+
handler: async (args, plur) => {
|
|
217
|
+
const action = args.action;
|
|
218
|
+
const target = byName.get(action);
|
|
219
|
+
if (!target) {
|
|
220
|
+
return { error: `Unknown action "${action}". Valid actions: ${adminActions.join(", ")}`, success: false, _isError: true };
|
|
221
|
+
}
|
|
222
|
+
const innerArgs = args.args ?? {};
|
|
223
|
+
const validated = validateToolArgs(target, innerArgs);
|
|
224
|
+
if (!validated.ok) {
|
|
225
|
+
return { ...validated.errorPayload, error: `${action}: ${validated.errorPayload.error}` };
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
return await target.handler(validated.data, plur);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const message = err?.message ?? String(err);
|
|
231
|
+
throw new Error(`${action}: ${message}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function getToolDefinitions(profile = "full") {
|
|
237
|
+
const all = getAllToolDefinitions();
|
|
238
|
+
if (profile !== "cursor") return all;
|
|
239
|
+
const core = all.filter((t) => CURSOR_CORE_TOOL_NAMES.has(t.name));
|
|
240
|
+
return [...core, buildAdminDispatchTool(all)];
|
|
241
|
+
}
|
|
242
|
+
function getAllToolDefinitions() {
|
|
115
243
|
return [
|
|
116
244
|
{
|
|
117
245
|
name: "plur_learn",
|
|
@@ -219,6 +347,82 @@ function getToolDefinitions() {
|
|
|
219
347
|
}
|
|
220
348
|
}
|
|
221
349
|
},
|
|
350
|
+
{
|
|
351
|
+
name: "plur_learn_batch",
|
|
352
|
+
description: "Create many engrams in one call \u2014 the batch form of plur_learn. Accepts an array of engram objects and writes them sequentially through the SAME dedup + policy pipeline as plur_learn (content-hash NOOP \u2192 semantic recall \u2192 LLM ADD/UPDATE/MERGE decision). Dedup also applies WITHIN the batch: a statement duplicating an earlier item in the same array resolves to NOOP against it. Returns the created/affected engram ids in input order, the per-item decisions, aggregate stats, and any per-item failures \u2014 a single bad item does not abort the batch. Use this when an orchestration fans out and wants to persist consolidated findings without N separate calls. LLM dedup calls are capped (default 50, override with max_llm_calls) to bound bulk-import cost. Note: unlike plur_learn, batch items take the LOCAL learn path \u2014 remote-scope auto-routing (learnRouted) is not applied per item, so for shared/remote-store writes prefer plur_learn or pass an explicit local scope. See plur-ai/plur#281.",
|
|
353
|
+
annotations: { title: "Learn (batch)", destructiveHint: false, idempotentHint: false },
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
engrams: {
|
|
358
|
+
type: "array",
|
|
359
|
+
description: "Engram objects to persist. Each requires `statement`; the other fields mirror plur_learn.",
|
|
360
|
+
items: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {
|
|
363
|
+
statement: { type: "string", description: "The knowledge assertion to store" },
|
|
364
|
+
type: { type: "string", enum: ["behavioral", "terminological", "procedural", "architectural"], description: "Category of the engram" },
|
|
365
|
+
scope: { type: "string", description: "Namespace, e.g. global, project:myapp" },
|
|
366
|
+
domain: { type: "string", description: "Domain tag, e.g. software.deployment" },
|
|
367
|
+
tags: { type: "array", items: { type: "string" }, description: "Searchable keyword tags \u2014 contribute to BM25/embedding recall" },
|
|
368
|
+
rationale: { type: "string", description: "Why this knowledge matters \u2014 also enters the search corpus" },
|
|
369
|
+
source: { type: "string", description: "Origin of this knowledge (URL, conversation ref, etc.)" },
|
|
370
|
+
pinned: { type: "boolean", description: "Always-load flag. Use sparingly: meta-rules, safety conventions, core principles." },
|
|
371
|
+
commitment: { type: "string", enum: ["exploring", "leaning", "decided", "locked"], description: "How firmly the user has committed (default: leaning)" },
|
|
372
|
+
valid_from: { type: "string", description: "ISO date (YYYY-MM-DD) the knowledge becomes valid" },
|
|
373
|
+
valid_until: { type: "string", description: "ISO date (YYYY-MM-DD) the knowledge expires" }
|
|
374
|
+
},
|
|
375
|
+
required: ["statement"]
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
max_llm_calls: { type: "number", description: "Max LLM dedup calls across the whole batch (default 50). Once spent, remaining items use the cheap hash/cosine path. Pass a large number to opt out." }
|
|
379
|
+
},
|
|
380
|
+
required: ["engrams"]
|
|
381
|
+
},
|
|
382
|
+
handler: async (args, plur) => {
|
|
383
|
+
const llm = getLlmFunction();
|
|
384
|
+
const raw = Array.isArray(args.engrams) ? args.engrams : [];
|
|
385
|
+
if (raw.length === 0) {
|
|
386
|
+
return { ids: [], results: [], stats: { added: 0, updated: 0, merged: 0, noops: 0, failed: 0 }, failures: [], warning: "No engrams provided \u2014 pass a non-empty `engrams` array." };
|
|
387
|
+
}
|
|
388
|
+
const items = raw.map((e) => ({
|
|
389
|
+
statement: sanitizeStatement(e.statement),
|
|
390
|
+
context: {
|
|
391
|
+
type: e.type,
|
|
392
|
+
scope: e.scope,
|
|
393
|
+
domain: e.domain,
|
|
394
|
+
source: e.source,
|
|
395
|
+
tags: e.tags,
|
|
396
|
+
rationale: e.rationale,
|
|
397
|
+
commitment: e.commitment,
|
|
398
|
+
pinned: e.pinned,
|
|
399
|
+
valid_from: e.valid_from,
|
|
400
|
+
valid_until: e.valid_until
|
|
401
|
+
}
|
|
402
|
+
}));
|
|
403
|
+
const maxLlmCalls = typeof args.max_llm_calls === "number" ? args.max_llm_calls : void 0;
|
|
404
|
+
const { results, stats, failures } = await plur.learnBatch(
|
|
405
|
+
items,
|
|
406
|
+
llm,
|
|
407
|
+
maxLlmCalls !== void 0 ? { maxLlmCalls } : void 0
|
|
408
|
+
);
|
|
409
|
+
mcpCanary.signal("learn_activity");
|
|
410
|
+
recordTelemetry("learn");
|
|
411
|
+
return {
|
|
412
|
+
ids: results.map((r) => r.engram.id),
|
|
413
|
+
results: results.map((r) => ({
|
|
414
|
+
id: r.engram.id,
|
|
415
|
+
statement: r.engram.statement,
|
|
416
|
+
scope: r.engram.scope,
|
|
417
|
+
type: r.engram.type,
|
|
418
|
+
decision: r.decision,
|
|
419
|
+
...r.existing_id ? { existing_id: r.existing_id } : {}
|
|
420
|
+
})),
|
|
421
|
+
stats,
|
|
422
|
+
...failures.length > 0 ? { failures, warning: `${failures.length} of ${raw.length} engram(s) failed to persist; the rest were written.` } : {}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
},
|
|
222
426
|
{
|
|
223
427
|
name: "plur_recall",
|
|
224
428
|
description: "Query engrams by BM25 keyword matching \u2014 use plur_recall_hybrid for semantic similarity. Note: a project-scope filter also returns personal-family engrams (local, global, user:*, agent:*); an explicit scope=global recall returns ALL personal-family engrams \u2014 wider than scope=global INJECT, which is targeted to the global namespace only.",
|
|
@@ -242,14 +446,18 @@ function getToolDefinitions() {
|
|
|
242
446
|
limit: args.limit
|
|
243
447
|
});
|
|
244
448
|
return {
|
|
245
|
-
results: results.map((e) =>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
449
|
+
results: results.map((e) => {
|
|
450
|
+
const supersededBy = e.relations?.superseded_by;
|
|
451
|
+
const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
|
|
452
|
+
return {
|
|
453
|
+
id: e.id,
|
|
454
|
+
statement: e.statement + annotation,
|
|
455
|
+
type: e.type,
|
|
456
|
+
scope: e.scope,
|
|
457
|
+
domain: e.domain,
|
|
458
|
+
retrieval_strength: e.activation.retrieval_strength
|
|
459
|
+
};
|
|
460
|
+
}),
|
|
253
461
|
count: results.length
|
|
254
462
|
};
|
|
255
463
|
}
|
|
@@ -305,9 +513,11 @@ function getToolDefinitions() {
|
|
|
305
513
|
const response = {
|
|
306
514
|
results: boundedResults.map((e) => {
|
|
307
515
|
const raw = e;
|
|
516
|
+
const supersededBy = e.relations?.superseded_by;
|
|
517
|
+
const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
|
|
308
518
|
const base = {
|
|
309
519
|
id: e.id,
|
|
310
|
-
statement: e.statement,
|
|
520
|
+
statement: e.statement + annotation,
|
|
311
521
|
type: e.type,
|
|
312
522
|
scope: e.scope,
|
|
313
523
|
domain: e.domain,
|
|
@@ -964,7 +1174,7 @@ function getToolDefinitions() {
|
|
|
964
1174
|
},
|
|
965
1175
|
{
|
|
966
1176
|
name: "plur_doctor",
|
|
967
|
-
description: 'Diagnose the PLUR
|
|
1177
|
+
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).',
|
|
968
1178
|
annotations: { title: "Doctor", readOnlyHint: false, idempotentHint: false },
|
|
969
1179
|
inputSchema: {
|
|
970
1180
|
type: "object",
|
|
@@ -1056,6 +1266,23 @@ function getToolDefinitions() {
|
|
|
1056
1266
|
}
|
|
1057
1267
|
}
|
|
1058
1268
|
checks.push({ check: "reranker available", ok: rerankerOk, detail: rerankerDetail });
|
|
1269
|
+
if (rerankerOk) {
|
|
1270
|
+
try {
|
|
1271
|
+
const fitResult = await plur.checkRerankerFit({ rerankerName });
|
|
1272
|
+
const sep = fitResult.separability.toFixed(3);
|
|
1273
|
+
checks.push({
|
|
1274
|
+
check: "reranker domain fit",
|
|
1275
|
+
ok: fitResult.fit,
|
|
1276
|
+
detail: fitResult.n_pairs === 0 ? "Not enough engrams to evaluate fit (< 2) \u2014 assuming fit" : fitResult.fit ? `Good fit \u2014 separability ${sep} on ${fitResult.n_pairs} pairs (threshold \u2265 0.05)` : `Poor fit \u2014 separability ${sep} on ${fitResult.n_pairs} pairs (threshold \u2265 0.05). Reranker may be net-negative on this store's domain mix.`
|
|
1277
|
+
});
|
|
1278
|
+
if (!fitResult.fit && fitResult.n_pairs > 0) {
|
|
1279
|
+
remediation.push(
|
|
1280
|
+
`Reranker "${rerankerName}" shows poor separability (${sep}) on this store's engrams \u2014 it may be scoring irrelevant pairs higher than relevant ones. Consider unsetting PLUR_RERANKER or switching to a different tier. The fit check compares same-domain vs cross-domain pair scores; low separability means the model lacks signal on your content.`
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
} catch {
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1059
1286
|
try {
|
|
1060
1287
|
let evalStatus;
|
|
1061
1288
|
let freshlyRun = false;
|
|
@@ -1510,11 +1737,27 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1510
1737
|
if (discoveries.length === 0) {
|
|
1511
1738
|
return { discovered: [], note: "No remote stores configured. Register one scope first with plur_stores_add, then discover the rest." };
|
|
1512
1739
|
}
|
|
1740
|
+
const enrich = (d) => {
|
|
1741
|
+
if (!d.ok) return d;
|
|
1742
|
+
const byScope = new Map(d.metadata.map((m) => [m.scope, m]));
|
|
1743
|
+
const registeredSet = new Set(d.registered);
|
|
1744
|
+
const scopes = d.authorized.map((scope) => {
|
|
1745
|
+
const m = byScope.get(scope);
|
|
1746
|
+
return {
|
|
1747
|
+
scope,
|
|
1748
|
+
registered: registeredSet.has(scope),
|
|
1749
|
+
...m?.description ? { description: m.description } : {},
|
|
1750
|
+
...m && m.covers.length ? { covers: m.covers } : {}
|
|
1751
|
+
};
|
|
1752
|
+
});
|
|
1753
|
+
return { ...d, scopes };
|
|
1754
|
+
};
|
|
1755
|
+
const discovered = discoveries.map(enrich);
|
|
1513
1756
|
if (!register) {
|
|
1514
|
-
return { discovered
|
|
1757
|
+
return { discovered };
|
|
1515
1758
|
}
|
|
1516
1759
|
const registered = await plur.registerDiscoveredScopes({ url });
|
|
1517
|
-
return { discovered
|
|
1760
|
+
return { discovered, registered };
|
|
1518
1761
|
}
|
|
1519
1762
|
},
|
|
1520
1763
|
{
|
|
@@ -1931,7 +2174,6 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1931
2174
|
}
|
|
1932
2175
|
|
|
1933
2176
|
// src/server.ts
|
|
1934
|
-
import { z } from "zod";
|
|
1935
2177
|
var INSTRUCTIONS = `PLUR is your persistent memory. Corrections, preferences, and conventions persist across sessions as engrams.
|
|
1936
2178
|
|
|
1937
2179
|
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.
|
|
@@ -2049,49 +2291,9 @@ Use \`scope\` to namespace engrams per project:
|
|
|
2049
2291
|
|
|
2050
2292
|
Override with \`PLUR_PATH\` environment variable.
|
|
2051
2293
|
`;
|
|
2052
|
-
function
|
|
2053
|
-
if (!prop || typeof prop !== "object") return z.unknown();
|
|
2054
|
-
const variants = prop.anyOf ?? prop.oneOf;
|
|
2055
|
-
if (Array.isArray(variants) && variants.length > 0) {
|
|
2056
|
-
const zodVariants = variants.map(jsonSchemaPropToZod);
|
|
2057
|
-
if (zodVariants.length === 1) return zodVariants[0];
|
|
2058
|
-
return z.union(zodVariants);
|
|
2059
|
-
}
|
|
2060
|
-
if (prop.type === "string") return prop.enum ? z.enum(prop.enum) : z.string();
|
|
2061
|
-
if (prop.type === "number" || prop.type === "integer") return z.number();
|
|
2062
|
-
if (prop.type === "boolean") return z.boolean();
|
|
2063
|
-
if (prop.type === "array") {
|
|
2064
|
-
const itemSchema = prop.items ? jsonSchemaPropToZod(prop.items) : z.unknown();
|
|
2065
|
-
return z.preprocess((val) => {
|
|
2066
|
-
if (typeof val !== "string") return val;
|
|
2067
|
-
const trimmed = val.trim();
|
|
2068
|
-
if (trimmed.startsWith("[")) {
|
|
2069
|
-
try {
|
|
2070
|
-
const parsed = JSON.parse(trimmed);
|
|
2071
|
-
return Array.isArray(parsed) ? parsed : val;
|
|
2072
|
-
} catch {
|
|
2073
|
-
return val;
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
if (prop.items?.type === "string") {
|
|
2077
|
-
return trimmed.length === 0 ? [] : trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2078
|
-
}
|
|
2079
|
-
return val;
|
|
2080
|
-
}, z.array(itemSchema));
|
|
2081
|
-
}
|
|
2082
|
-
if (prop.type === "object" && prop.properties) {
|
|
2083
|
-
const shape = {};
|
|
2084
|
-
for (const [k, p] of Object.entries(prop.properties)) {
|
|
2085
|
-
const field = jsonSchemaPropToZod(p);
|
|
2086
|
-
shape[k] = prop.required?.includes(k) ? field : field.optional();
|
|
2087
|
-
}
|
|
2088
|
-
return z.object(shape).passthrough();
|
|
2089
|
-
}
|
|
2090
|
-
return z.unknown();
|
|
2091
|
-
}
|
|
2092
|
-
async function createServer(plur) {
|
|
2294
|
+
async function createServer(plur, options) {
|
|
2093
2295
|
const instance = plur ?? new Plur2();
|
|
2094
|
-
const tools = getToolDefinitions();
|
|
2296
|
+
const tools = getToolDefinitions(options?.profile ?? "full");
|
|
2095
2297
|
checkForUpdate("@plur-ai/mcp", VERSION, (r) => {
|
|
2096
2298
|
if (r.updateAvailable) {
|
|
2097
2299
|
console.error(`[plur] Update available: ${r.current} \u2192 ${r.latest}. Run: npx @plur-ai/mcp@latest`);
|
|
@@ -2128,33 +2330,26 @@ async function createServer(plur) {
|
|
|
2128
2330
|
mcpCanary.tick();
|
|
2129
2331
|
try {
|
|
2130
2332
|
let args = request.params.arguments ?? {};
|
|
2131
|
-
const
|
|
2132
|
-
if (
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
}
|
|
2138
|
-
const parsed = z.object(shape).passthrough().safeParse(args);
|
|
2139
|
-
if (!parsed.success) {
|
|
2140
|
-
const receivedFields = Object.keys(args);
|
|
2141
|
-
const details = parsed.error.issues.map((i) => `${i.path.join(".") || "root"}: ${i.message}`).join(", ");
|
|
2142
|
-
const receivedNote = receivedFields.length > 0 ? `Received fields: [${receivedFields.join(", ")}].` : "Received no fields (the arguments object was empty).";
|
|
2143
|
-
const hasArrayParam = Object.values(schema.properties).some((p) => p?.type === "array");
|
|
2144
|
-
const arrayBugHint = receivedFields.length === 0 && hasArrayParam ? ' Known client-side bug (plur-ai/plur#297): some MCP clients drop the entire arguments payload when an array-typed parameter is included. Retry passing array parameters as a JSON string (e.g. tags: "[\\"a\\",\\"b\\"]") or a comma-separated string (tags: "a, b") \u2014 the server coerces both back into arrays.' : "";
|
|
2145
|
-
return {
|
|
2146
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
2147
|
-
error: `Invalid arguments: ${details}. ${receivedNote} The call reached the server \u2014 this is a malformed-arguments error, not a transport failure. Fix the field(s) named above and retry; do not abandon the call.` + arrayBugHint,
|
|
2148
|
-
success: false,
|
|
2149
|
-
received_fields: receivedFields
|
|
2150
|
-
}) }],
|
|
2151
|
-
isError: true
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
args = parsed.data;
|
|
2333
|
+
const validated = validateToolArgs(tool, args);
|
|
2334
|
+
if (!validated.ok) {
|
|
2335
|
+
return {
|
|
2336
|
+
content: [{ type: "text", text: JSON.stringify(validated.errorPayload) }],
|
|
2337
|
+
isError: true
|
|
2338
|
+
};
|
|
2155
2339
|
}
|
|
2340
|
+
args = validated.data;
|
|
2156
2341
|
const result = await tool.handler(args, instance);
|
|
2157
|
-
|
|
2342
|
+
let payload = result;
|
|
2343
|
+
let resultIsError = false;
|
|
2344
|
+
if (result && typeof result === "object" && result._isError === true) {
|
|
2345
|
+
resultIsError = true;
|
|
2346
|
+
const { _isError, ...rest } = result;
|
|
2347
|
+
payload = rest;
|
|
2348
|
+
}
|
|
2349
|
+
return {
|
|
2350
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
2351
|
+
...resultIsError ? { isError: true } : {}
|
|
2352
|
+
};
|
|
2158
2353
|
} catch (err) {
|
|
2159
2354
|
const message = err?.message ?? String(err);
|
|
2160
2355
|
server.sendLoggingMessage({ level: "error", data: `Tool ${request.params.name} failed: ${message}` });
|
|
@@ -2183,11 +2378,16 @@ async function createServer(plur) {
|
|
|
2183
2378
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
2184
2379
|
const uri = request.params.uri;
|
|
2185
2380
|
if (uri === "plur://guide") {
|
|
2381
|
+
const cursorNote = options?.profile === "cursor" ? `
|
|
2382
|
+
|
|
2383
|
+
## Cursor tool profile
|
|
2384
|
+
|
|
2385
|
+
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: {...} }\`.` : "";
|
|
2186
2386
|
return {
|
|
2187
2387
|
contents: [{
|
|
2188
2388
|
uri: "plur://guide",
|
|
2189
2389
|
mimeType: "text/markdown",
|
|
2190
|
-
text: GUIDE_RESOURCE
|
|
2390
|
+
text: GUIDE_RESOURCE + cursorNote
|
|
2191
2391
|
}]
|
|
2192
2392
|
};
|
|
2193
2393
|
}
|
|
@@ -2275,7 +2475,8 @@ Please:
|
|
|
2275
2475
|
return server;
|
|
2276
2476
|
}
|
|
2277
2477
|
async function runStdio() {
|
|
2278
|
-
const
|
|
2478
|
+
const profile = process.env.PLUR_TOOL_PROFILE === "cursor" ? "cursor" : "full";
|
|
2479
|
+
const server = await createServer(void 0, { profile });
|
|
2279
2480
|
registerFlushOnExit({});
|
|
2280
2481
|
const transport = new StdioServerTransport();
|
|
2281
2482
|
await server.connect(transport);
|
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.12.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"plur-mcp": "dist/index.js"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
16
16
|
"zod": "^3.23.0",
|
|
17
|
-
"@plur-ai/core": "0.
|
|
17
|
+
"@plur-ai/core": "0.12.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^25.5.0"
|