@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.
|
|
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-
|
|
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.
|
|
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
|
-
...
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
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}.
|
|
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:
|
|
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
|
-
|
|
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:
|
|
1328
|
+
success: !scopeDropped,
|
|
1329
|
+
status: result.status,
|
|
1217
1330
|
...path ? { path } : { url },
|
|
1218
|
-
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
|
|
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({
|
|
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
|
-
"
|
|
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.
|
|
17
|
+
"@plur-ai/core": "0.10.0"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@types/node": "^25.5.0"
|