@plur-ai/mcp 0.9.13 → 0.10.1

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/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.9.13";
8
+ var VERSION = "0.10.1";
9
9
  var HELP = `plur-mcp v${VERSION} \u2014 persistent memory for AI agents
10
10
 
11
11
  Usage:
@@ -279,7 +279,7 @@ if (arg === "init") {
279
279
  process.exit(0);
280
280
  }
281
281
  if (arg === "serve" || arg === void 0) {
282
- const { runStdio } = await import("./server-O2CC7LEL.js");
282
+ const { runStdio } = await import("./server-XG6IEACO.js");
283
283
  runStdio().catch((err) => {
284
284
  console.error("Failed to start PLUR MCP server:", err);
285
285
  process.exit(1);
@@ -17,10 +17,21 @@ import { Plur as Plur2, checkForUpdate } from "@plur-ai/core";
17
17
  import { existsSync, unlinkSync } from "fs";
18
18
  import { join } from "path";
19
19
  import { homedir } from "os";
20
- import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, readProjectConfig } from "@plur-ai/core";
20
+ import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, readProjectConfig, isSharedScope } from "@plur-ai/core";
21
+
22
+ // src/telemetry.ts
23
+ import { recordEvent, flushIfNeeded, registerFlushOnExit } from "@plur-ai/core";
24
+ function recordTelemetry(event) {
25
+ try {
26
+ const rolledOver = recordEvent(event);
27
+ if (rolledOver) void flushIfNeeded({}).catch(() => {
28
+ });
29
+ } catch {
30
+ }
31
+ }
21
32
 
22
33
  // src/version.ts
23
- var VERSION = "0.9.13";
34
+ var VERSION = "0.10.1";
24
35
 
25
36
  // src/tools.ts
26
37
  function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
@@ -104,7 +115,7 @@ function getToolDefinitions() {
104
115
  return [
105
116
  {
106
117
  name: "plur_learn",
107
- description: "Create an engram \u2014 record a reusable learning, preference, or correction",
118
+ description: "Create an engram \u2014 record a reusable learning, preference, or correction. Multi-agent note: in an orchestration that spawns subagents, have the PARENT session own plur_learn writes \u2014 spawned subagents should return their findings as text for the parent to persist, rather than each calling plur_learn (tool availability is not guaranteed in every subagent context). See plur-ai/plur#281.",
108
119
  annotations: { title: "Learn", destructiveHint: false, idempotentHint: false },
109
120
  inputSchema: {
110
121
  type: "object",
@@ -140,11 +151,27 @@ function getToolDefinitions() {
140
151
  pinned: args.pinned,
141
152
  llm
142
153
  };
154
+ const explicitScope = typeof args.scope === "string" && args.scope.length > 0;
155
+ const scopeHint = (engramScope, wasRouted) => {
156
+ if (explicitScope || wasRouted || isSharedScope(engramScope)) return {};
157
+ let remote = [];
158
+ try {
159
+ remote = plur.getWritableRemoteScopes();
160
+ } catch {
161
+ return {};
162
+ }
163
+ if (remote.length === 0) return {};
164
+ const scopes = remote.map((s) => `"${s.scope}"`).join(", ");
165
+ 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.` };
166
+ };
143
167
  const statement = sanitizeStatement(args.statement);
144
168
  try {
145
169
  const engram = await plur.learnRouted(statement, context);
146
170
  const isOutbox = !!engram.structured_data?._outbox;
171
+ const demoted = engram.structured_data?._demoted;
172
+ const routed = engram.structured_data?._routed;
147
173
  mcpCanary.signal("learn_activity");
174
+ recordTelemetry("learn");
148
175
  return {
149
176
  id: engram.id,
150
177
  statement: engram.statement,
@@ -152,18 +179,24 @@ function getToolDefinitions() {
152
179
  type: engram.type,
153
180
  pinned: engram.pinned === true,
154
181
  decision: "ADD",
155
- ...isOutbox ? { outbox: true, warning: "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {}
182
+ ...scopeHint(engram.scope, !!routed),
183
+ ...isOutbox ? { outbox: true, warning: "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {},
184
+ ...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.` } : {},
185
+ ...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.` } : {}
156
186
  };
157
187
  } catch (err) {
158
188
  const engram = plur.learn(statement, context);
159
189
  const isOutbox = !!engram.structured_data?._outbox;
190
+ const routedFallback = engram.structured_data?._routed;
160
191
  mcpCanary.signal("learn_activity");
192
+ recordTelemetry("learn");
161
193
  return {
162
194
  id: engram.id,
163
195
  statement: engram.statement,
164
196
  scope: engram.scope,
165
197
  type: engram.type,
166
198
  decision: "ADD",
199
+ ...scopeHint(engram.scope, !!routedFallback),
167
200
  ...isOutbox ? { outbox: true } : {},
168
201
  warning: `Remote write failed (${err.message}); engram queued for retry.`
169
202
  };
@@ -172,7 +205,7 @@ function getToolDefinitions() {
172
205
  },
173
206
  {
174
207
  name: "plur_recall",
175
- description: "Query engrams by BM25 keyword matching \u2014 use plur_recall_hybrid for semantic similarity",
208
+ 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.",
176
209
  annotations: { title: "Recall (BM25)", readOnlyHint: true, idempotentHint: true },
177
210
  inputSchema: {
178
211
  type: "object",
@@ -230,6 +263,7 @@ function getToolDefinitions() {
230
263
  domain: args.domain,
231
264
  limit: effectiveLimit
232
265
  });
266
+ recordTelemetry("recall");
233
267
  const results = meta.engrams;
234
268
  let truncated = false;
235
269
  let boundedResults = results;
@@ -658,7 +692,7 @@ function getToolDefinitions() {
658
692
  },
659
693
  {
660
694
  name: "plur_sync",
661
- description: "Sync engrams via git \u2014 initializes repo on first call, commits and pushes/pulls on subsequent calls. Provide a remote URL on first call to enable cross-device sync.",
695
+ description: "Sync engrams via git AND refresh the derived index from YAML. Initializes repo on first call, commits and pushes/pulls on subsequent calls. Provide a remote URL on first call to enable cross-device sync. Pass full=true to drop-and-rebuild the index from YAML (recovery path; YAML stays untouched).",
662
696
  annotations: { title: "Sync", openWorldHint: true, destructiveHint: false, idempotentHint: true },
663
697
  inputSchema: {
664
698
  type: "object",
@@ -666,11 +700,15 @@ function getToolDefinitions() {
666
700
  remote: {
667
701
  type: "string",
668
702
  description: "Git remote URL (e.g. git@github.com:user/plur-engrams.git). Only needed on first call to set up remote."
703
+ },
704
+ full: {
705
+ type: "boolean",
706
+ 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."
669
707
  }
670
708
  }
671
709
  },
672
710
  handler: async (args, plur) => {
673
- const result = plur.sync(args.remote);
711
+ const result = plur.sync(args.remote, { full: args.full === true });
674
712
  let outbox_result;
675
713
  try {
676
714
  outbox_result = await plur.flushOutbox();
@@ -883,7 +921,7 @@ function getToolDefinitions() {
883
921
  },
884
922
  {
885
923
  name: "plur_doctor",
886
- description: "Diagnose the PLUR install. Reports whether the embedding model loaded, whether hybrid search is fully operational, and what to do if it is degraded. Run this first when recall feels off.",
924
+ description: 'Diagnose the PLUR install. 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.',
887
925
  annotations: { title: "Doctor", readOnlyHint: false, idempotentHint: false },
888
926
  inputSchema: {
889
927
  type: "object",
@@ -953,6 +991,28 @@ function getToolDefinitions() {
953
991
  if (cs.warning) remediation.push(cs.warning);
954
992
  }
955
993
  }
994
+ try {
995
+ const remotes = await plur.checkRemoteHealth({ timeoutMs: 5e3 });
996
+ for (const h of remotes) {
997
+ const expiresNote = typeof h.tokenExpiresInDays === "number" ? ` \u2014 token ${h.tokenExpiresInDays < 0 ? `expired ${-h.tokenExpiresInDays}d ago` : `expires in ${h.tokenExpiresInDays}d`}` : "";
998
+ if (h.status === "ok") {
999
+ const soon = typeof h.tokenExpiresInDays === "number" && h.tokenExpiresInDays <= 7;
1000
+ checks.push({
1001
+ check: `remote store: ${h.url}`,
1002
+ ok: !soon,
1003
+ detail: soon ? `Reachable, but token expires in ${h.tokenExpiresInDays}d \u2014 reauth soon` : `Reachable, auth valid${expiresNote}`
1004
+ });
1005
+ if (soon) remediation.push(`Remote ${h.url}: token expires in ${h.tokenExpiresInDays}d \u2014 mint a new token (<host>/me/api-keys), update ~/.plur/config.yaml, restart.`);
1006
+ } else if (h.status === "auth_expired") {
1007
+ checks.push({ check: `remote store: ${h.url}`, ok: false, detail: `AUTH FAILED${expiresNote} \u2014 team-scoped writes are queuing to the outbox, not syncing. (${h.reason ?? ""})` });
1008
+ remediation.push(`Remote ${h.url}: re-authenticate \u2014 open <host>/auth/github (or <host>/me/api-keys) in a browser, paste the token into ~/.plur/config.yaml, then restart Claude/MCP so it reloads. Queued engrams flush on next session_start.`);
1009
+ } else {
1010
+ checks.push({ check: `remote store: ${h.url}`, ok: false, detail: `Unreachable (${h.reason ?? "network error"}) \u2014 writes queue locally until it recovers.` });
1011
+ remediation.push(`Remote ${h.url}: unreachable \u2014 check connectivity/VPN. Reads fall back to local; writes queue in the outbox.`);
1012
+ }
1013
+ }
1014
+ } catch {
1015
+ }
956
1016
  return {
957
1017
  ok: checks.every((c) => c.ok),
958
1018
  checks,
@@ -990,7 +1050,14 @@ function getToolDefinitions() {
990
1050
  outbox_result = await plur.flushOutbox();
991
1051
  } catch {
992
1052
  }
993
- const remote_scopes = plur.getWritableRemoteScopes();
1053
+ const remote_scopes = plur.getWritableRemoteScopes().map((s) => {
1054
+ const md = plur.getScopeMetadata(s.scope);
1055
+ return {
1056
+ ...s,
1057
+ ...md?.description ? { description: md.description } : {},
1058
+ ...md?.covers && md.covers.length > 0 ? { covers: md.covers } : {}
1059
+ };
1060
+ });
994
1061
  const projectConfig = readProjectConfig();
995
1062
  const explicit_default_scope = args.default_scope ?? null;
996
1063
  const default_scope = explicit_default_scope ?? projectConfig.scope ?? null;
@@ -1059,15 +1126,58 @@ Auto-detected project scope: "${default_scope}" (from .plur.yaml in the current
1059
1126
  } else if (scope_source === "none") {
1060
1127
  guide += `
1061
1128
 
1062
- \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. To avoid context bleed across projects, create a .plur.yaml in this project's root with: scope: "project:<your-project-name>"`;
1129
+ \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.`;
1063
1130
  }
1064
1131
  if (remote_scopes.length > 0) {
1065
- const scopeList = remote_scopes.map((s) => `"${s.scope}"`).join(", ");
1132
+ const safe = (x) => String(x ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
1133
+ const scopeList = remote_scopes.map((s) => {
1134
+ const detail = [
1135
+ s.description ? `\u2014 ${safe(s.description)}` : "",
1136
+ s.covers && s.covers.length > 0 ? `(covers: ${s.covers.map(safe).join(", ")})` : ""
1137
+ ].filter(Boolean).join(" ");
1138
+ return detail ? `"${safe(s.scope)}" ${detail}` : `"${safe(s.scope)}"`;
1139
+ }).join("; ");
1066
1140
  guide += default_scope ? `
1067
1141
 
1068
1142
  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}).` : `
1069
1143
 
1070
- Remote store scopes available: ${scopeList}. 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 should stay at default scope (local).`;
1144
+ 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.`;
1145
+ try {
1146
+ const discoveries = await plur.discoverRemoteScopes({ timeoutMs: 3e3 });
1147
+ const failures = discoveries.filter((d) => !d.ok);
1148
+ if (failures.length > 0) {
1149
+ const authExpired = failures.some((f) => /\b40[13]\b/.test(f.error ?? ""));
1150
+ const pending = outbox_result?.failed ?? 0;
1151
+ const urls = [...new Set(failures.map((f) => f.url))].join(", ");
1152
+ guide += authExpired ? `
1153
+
1154
+ \u26A0\uFE0F ENTERPRISE STORE AUTH FAILED (token expired/invalid): ${urls}. Team-scoped engrams are NOT syncing` + (pending > 0 ? ` \u2014 ${pending} queued in the outbox` : "") + `. Reauth: open <host>/auth/github (or <host>/me/api-keys) in a browser, paste the token into ~/.plur/config.yaml, then restart Claude/MCP. Queued engrams flush on the next session_start.` : `
1155
+
1156
+ \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.`;
1157
+ }
1158
+ const unregistered = [...new Set(discoveries.filter((d) => d.ok).flatMap((d) => d.unregistered))];
1159
+ if (unregistered.length > 0) {
1160
+ const list = unregistered.map((s) => `"${safe(s)}"`).join(", ");
1161
+ guide += `
1162
+
1163
+ \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.`;
1164
+ }
1165
+ } catch {
1166
+ }
1167
+ try {
1168
+ for (const t of plur.remoteTokenExpiries()) {
1169
+ if (t.expired) {
1170
+ guide += `
1171
+
1172
+ \u26A0\uFE0F Enterprise token for ${t.url} EXPIRED ${t.expiresAt ?? ""}. Reauth and restart to resume team sync.`;
1173
+ } else if (typeof t.expiresInDays === "number" && t.expiresInDays <= 7) {
1174
+ guide += `
1175
+
1176
+ \u23F3 Enterprise token for ${t.url} expires in ${t.expiresInDays}d. Mint a fresh one (<host>/me/api-keys) before it lapses.`;
1177
+ }
1178
+ }
1179
+ } catch {
1180
+ }
1071
1181
  }
1072
1182
  return {
1073
1183
  session_id,
@@ -1186,7 +1296,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1186
1296
  },
1187
1297
  {
1188
1298
  name: "plur_stores_add",
1189
- description: "Register an additional engram store. Either filesystem (path) or remote (url+token, e.g. PLUR Enterprise).",
1299
+ description: 'Register an additional engram store. Either filesystem (path) or remote (url+token, e.g. PLUR Enterprise). One remote URL can host multiple scopes \u2014 call once per team scope you are authorized for; each registers independently. Returns status: "added" or "already_registered".',
1190
1300
  annotations: { title: "Add store", destructiveHint: false, idempotentHint: true },
1191
1301
  inputSchema: {
1192
1302
  type: "object",
@@ -1206,23 +1316,32 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1206
1316
  const token = args.token;
1207
1317
  if (!path && !url) return { error: "Either path or url must be provided" };
1208
1318
  if (path && url) return { error: "Provide path OR url, not both" };
1209
- plur.addStore(path ?? "", args.scope, {
1319
+ const requestedScope = args.scope;
1320
+ const result = plur.addStore(path ?? "", requestedScope, {
1210
1321
  shared: args.shared,
1211
1322
  readonly: args.readonly,
1212
1323
  url,
1213
1324
  token
1214
1325
  });
1326
+ const scopeDropped = result.status === "already_registered" && result.scope !== requestedScope;
1215
1327
  return {
1216
- success: true,
1328
+ success: !scopeDropped,
1329
+ status: result.status,
1217
1330
  ...path ? { path } : { url },
1218
- scope: args.scope,
1331
+ // On already_registered this is the EXISTING entry's scope — for
1332
+ // local stores (path-only identity) it may differ from the request.
1333
+ scope: result.scope,
1334
+ ...scopeDropped ? {
1335
+ requested_scope: requestedScope,
1336
+ note: `This path is already registered under scope "${result.scope}". A local store is keyed by its path, so the requested scope "${requestedScope}" was NOT added. Use a separate store file for a different scope, or remove the existing entry first.`
1337
+ } : {},
1219
1338
  kind: url ? "remote" : "filesystem"
1220
1339
  };
1221
1340
  }
1222
1341
  },
1223
1342
  {
1224
1343
  name: "plur_stores_list",
1225
- description: "List all configured engram stores with their scope, path, and engram count",
1344
+ description: "List all configured engram stores with their scope, path, and engram count. When a store declares self-describing scope metadata, its description and covers (topics the scope is the home for) are included so you can pick the right scope.",
1226
1345
  annotations: { title: "List stores", readOnlyHint: true, idempotentHint: true },
1227
1346
  inputSchema: { type: "object", properties: {} },
1228
1347
  handler: async (_args, plur) => {
@@ -1235,6 +1354,53 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1235
1354
  };
1236
1355
  }
1237
1356
  },
1357
+ {
1358
+ name: "plur_suggest_scope",
1359
+ 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).',
1360
+ annotations: { title: "Suggest scope", readOnlyHint: true, idempotentHint: true },
1361
+ inputSchema: {
1362
+ type: "object",
1363
+ properties: {
1364
+ statement: { type: "string", description: "The engram statement to route" },
1365
+ domain: { type: "string", description: 'Optional dotted namespace for the engram (e.g. "plur.core.security") \u2014 strongest routing signal' },
1366
+ tags: { type: "array", items: { type: "string" }, description: "Optional tags on the engram" }
1367
+ },
1368
+ required: ["statement"]
1369
+ },
1370
+ handler: async (args, plur) => {
1371
+ const candidates = plur.suggestScope({
1372
+ statement: args.statement,
1373
+ domain: args.domain,
1374
+ tags: args.tags
1375
+ });
1376
+ return { candidates, count: candidates.length };
1377
+ }
1378
+ },
1379
+ {
1380
+ name: "plur_scopes_discover",
1381
+ 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.",
1382
+ annotations: { title: "Discover scopes", readOnlyHint: false, idempotentHint: true },
1383
+ inputSchema: {
1384
+ type: "object",
1385
+ properties: {
1386
+ url: { type: "string", description: "Limit discovery to this remote URL (default: all configured remote stores)" },
1387
+ register: { type: "boolean", description: "Register all authorized-but-unregistered scopes (default false \u2014 discovery is read-only)" }
1388
+ }
1389
+ },
1390
+ handler: async (args, plur) => {
1391
+ const url = args.url;
1392
+ const register = args.register === true;
1393
+ const discoveries = await plur.discoverRemoteScopes({ url });
1394
+ if (discoveries.length === 0) {
1395
+ return { discovered: [], note: "No remote stores configured. Register one scope first with plur_stores_add, then discover the rest." };
1396
+ }
1397
+ if (!register) {
1398
+ return { discovered: discoveries };
1399
+ }
1400
+ const registered = await plur.registerDiscoveredScopes({ url });
1401
+ return { discovered: discoveries, registered };
1402
+ }
1403
+ },
1238
1404
  {
1239
1405
  name: "plur_promote",
1240
1406
  description: "Activate candidate engrams so they appear in injection results",
@@ -1616,6 +1782,19 @@ DURING the session:
1616
1782
  - When user states a preference ("always X", "never Y") \u2192 call plur_learn immediately
1617
1783
  - When you discover a codebase convention or pattern \u2192 call plur_learn
1618
1784
 
1785
+ SCOPE SELECTION (set scope PER engram, by content \u2014 not once per session):
1786
+ A single session produces engrams that belong in different stores. Choose the
1787
+ "scope" on EACH plur_learn call from what the engram is about:
1788
+ - Team / engineering / architecture / shared-convention knowledge \u2192 the matching
1789
+ team store scope (e.g. "group:<org>/<team>"). plur_session_start lists the
1790
+ scopes this install can write to \u2014 use them.
1791
+ - Personal preferences, your workflow, or details specific to one project \u2192 the
1792
+ default/local scope (or "project:<name>").
1793
+ - "global" is for genuinely cross-project facts only (language gotchas, tool
1794
+ quirks). Do NOT let team-relevant knowledge fall back to "global" by omitting
1795
+ scope \u2014 with a team store configured it never reaches the team store. If no
1796
+ scope fits, prefer the project/local default over global.
1797
+
1619
1798
  OPTIONAL but improves quality:
1620
1799
  - Call plur_feedback to rate which injected engrams helped (positive/negative)
1621
1800
  - Call plur_recall_hybrid before answering factual questions \u2014 the answer may be in memory
@@ -1779,9 +1958,15 @@ async function createServer(plur) {
1779
1958
  }
1780
1959
  const parsed = z.object(shape).passthrough().safeParse(args);
1781
1960
  if (!parsed.success) {
1782
- const details = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
1961
+ const receivedFields = Object.keys(args);
1962
+ const details = parsed.error.issues.map((i) => `${i.path.join(".") || "root"}: ${i.message}`).join(", ");
1963
+ const receivedNote = receivedFields.length > 0 ? `Received fields: [${receivedFields.join(", ")}].` : "Received no fields (the arguments object was empty).";
1783
1964
  return {
1784
- content: [{ type: "text", text: JSON.stringify({ error: `Invalid arguments: ${details}`, success: false }) }],
1965
+ content: [{ type: "text", text: JSON.stringify({
1966
+ 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.`,
1967
+ success: false,
1968
+ received_fields: receivedFields
1969
+ }) }],
1785
1970
  isError: true
1786
1971
  };
1787
1972
  }
@@ -1909,10 +2094,12 @@ Please:
1909
2094
  }
1910
2095
  async function runStdio() {
1911
2096
  const server = await createServer();
2097
+ registerFlushOnExit({});
1912
2098
  const transport = new StdioServerTransport();
1913
2099
  await server.connect(transport);
1914
2100
  }
1915
2101
  export {
2102
+ INSTRUCTIONS,
1916
2103
  createServer,
1917
2104
  runStdio
1918
2105
  };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@plur-ai/mcp",
3
- "version": "0.9.13",
3
+ "mcpName": "io.github.plur-ai/plur",
4
+ "version": "0.10.1",
4
5
  "type": "module",
5
6
  "bin": {
6
7
  "plur-mcp": "dist/index.js"
@@ -13,7 +14,7 @@
13
14
  "dependencies": {
14
15
  "@modelcontextprotocol/sdk": "^1.12.0",
15
16
  "zod": "^3.23.0",
16
- "@plur-ai/core": "0.9.13"
17
+ "@plur-ai/core": "0.10.0"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@types/node": "^25.5.0"