@plur-ai/mcp 0.15.0 → 0.16.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.
@@ -1,17 +1,3 @@
1
- // src/server.ts
2
- import { Server, ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
3
- import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
4
- import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
5
- import { join as join2 } from "path";
6
- import { homedir as homedir2 } from "os";
7
- import { Plur as Plur2, checkForUpdate } from "@plur-ai/core";
8
-
9
- // src/tools.ts
10
- import { existsSync, unlinkSync } from "fs";
11
- import { join } from "path";
12
- import { homedir } from "os";
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";
14
-
15
1
  // src/telemetry.ts
16
2
  import { recordEvent, flushIfNeeded, registerFlushOnExit } from "@plur-ai/core";
17
3
  function recordTelemetry(event) {
@@ -24,9 +10,13 @@ function recordTelemetry(event) {
24
10
  }
25
11
 
26
12
  // src/version.ts
27
- var VERSION = "0.15.0";
13
+ var VERSION = "0.16.0";
28
14
 
29
15
  // src/tools.ts
16
+ import { existsSync, unlinkSync } from "fs";
17
+ import { join } from "path";
18
+ import { homedir } from "os";
19
+ 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";
30
20
  import { z } from "zod";
31
21
  function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
32
22
  return async (prompt) => {
@@ -49,6 +39,95 @@ function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
49
39
  return data.choices?.[0]?.message?.content ?? "";
50
40
  };
51
41
  }
42
+ var recallHandler = async (args, plur) => {
43
+ const mode = args.mode ?? "hybrid";
44
+ if (mode === "keyword") {
45
+ const results = await plur.recall(args.query, {
46
+ scope: args.scope,
47
+ domain: args.domain,
48
+ limit: args.limit
49
+ });
50
+ return {
51
+ results: results.map((e) => {
52
+ const supersededBy = e.relations?.superseded_by;
53
+ const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
54
+ return {
55
+ id: e.id,
56
+ statement: e.statement + annotation,
57
+ type: e.type,
58
+ scope: e.scope,
59
+ domain: e.domain,
60
+ retrieval_strength: e.activation.retrieval_strength
61
+ };
62
+ }),
63
+ count: results.length,
64
+ mode: "keyword"
65
+ };
66
+ }
67
+ const budget = args.budget;
68
+ const cap = budget?.max_results ?? args.limit ?? 20;
69
+ const fetchLimit = budget?.max_results != null ? cap + 1 : cap;
70
+ const meta = await plur.recallHybridWithMeta(args.query, {
71
+ scope: args.scope,
72
+ domain: args.domain,
73
+ limit: fetchLimit
74
+ });
75
+ recordTelemetry("recall");
76
+ const truncatedByCount = budget?.max_results != null && meta.engrams.length > cap;
77
+ let truncated = truncatedByCount;
78
+ let boundedResults = truncatedByCount ? meta.engrams.slice(0, cap) : meta.engrams;
79
+ if (budget?.max_tokens) {
80
+ let tokenCount = 0;
81
+ const withinBudget = [];
82
+ for (const e of boundedResults) {
83
+ const tokens = Math.ceil(e.statement.length / 4) + 20;
84
+ if (tokenCount + tokens > budget.max_tokens) {
85
+ truncated = true;
86
+ break;
87
+ }
88
+ withinBudget.push(e);
89
+ tokenCount += tokens;
90
+ }
91
+ boundedResults = withinBudget;
92
+ }
93
+ const includeEpisodes = args.include_episodes === true;
94
+ const response = {
95
+ results: boundedResults.map((e) => {
96
+ const raw = e;
97
+ const supersededBy = e.relations?.superseded_by;
98
+ const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
99
+ const base = {
100
+ id: e.id,
101
+ statement: e.statement + annotation,
102
+ type: e.type,
103
+ scope: e.scope,
104
+ domain: e.domain,
105
+ retrieval_strength: e.activation.retrieval_strength
106
+ };
107
+ if (includeEpisodes && raw.episode_ids?.length > 0) {
108
+ const episodes = plur.timeline({ search: "" });
109
+ base.episodes = episodes.filter((ep) => raw.episode_ids.includes(ep.id)).map((ep) => ({ id: ep.id, summary: ep.summary, timestamp: ep.timestamp }));
110
+ }
111
+ return base;
112
+ }),
113
+ count: boundedResults.length,
114
+ truncated,
115
+ mode: meta.mode
116
+ };
117
+ if (meta.mode === "hybrid-degraded") {
118
+ response.warning = `Embedding layer unavailable \u2014 results are BM25-only. Run plur_doctor for diagnosis. Last error: ${meta.embedderError ?? "unknown"}`;
119
+ }
120
+ if (resolveRerankerName() !== "off") {
121
+ response.reranked = meta.reranked ?? 0;
122
+ const rr = plur.rerankerStatus();
123
+ if (boundedResults.length > 0 && (meta.reranked ?? 0) === 0 && rr.lastError) {
124
+ const corruptNote = rr.lastErrorKind === "corrupt-cache" ? " The model cache looks corrupt (truncated download) \u2014 purge and re-download, see plur_doctor." : "";
125
+ response.reranker_warning = `PLUR_RERANKER is set but the reranker did not engage \u2014 results are RRF-only (fusion order, no cross-encoder rerank).${corruptNote} Last error: ${rr.lastError}. Run plur_doctor for diagnosis.`;
126
+ }
127
+ }
128
+ return response;
129
+ };
130
+ var RECALL_HYBRID_DEPRECATION = "plur_recall_hybrid is deprecated since 0.16 \u2014 use plur_recall (mode defaults to hybrid). This alias will be removed in 0.18.";
52
131
  function jsonSchemaPropToZod(prop) {
53
132
  if (!prop || typeof prop !== "object") return z.unknown();
54
133
  const variants = prop.anyOf ?? prop.oneOf;
@@ -73,7 +152,10 @@ function jsonSchemaPropToZod(prop) {
73
152
  return val;
74
153
  }
75
154
  }
76
- if (prop.items?.type === "string") {
155
+ const items = prop.items;
156
+ const itemVariants = items?.anyOf ?? items?.oneOf;
157
+ const itemsAcceptString = items?.type === "string" || Array.isArray(itemVariants) && itemVariants.some((v) => v?.type === "string");
158
+ if (itemsAcceptString) {
77
159
  return trimmed.length === 0 ? [] : trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
78
160
  }
79
161
  return val;
@@ -102,7 +184,10 @@ function validateToolArgs(tool, rawArgs) {
102
184
  const receivedFields = Object.keys(rawArgs);
103
185
  const details = parsed.error.issues.map((i) => `${i.path.join(".") || "root"}: ${i.message}`).join(", ");
104
186
  const hasArrayParam = Object.values(schema.properties ?? {}).some((p) => p?.type === "array");
105
- 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.' : "";
187
+ const missingArrayParams = parsed.error.issues.filter((i) => i.code === "invalid_type" && i.received === "undefined").map((i) => String(i.path[0] ?? "")).filter((k) => schema.properties?.[k]?.type === "array");
188
+ const totalDrop = receivedFields.length === 0 && hasArrayParam;
189
+ const partialDrop = missingArrayParams.length > 0;
190
+ const arrayBugHint = totalDrop || partialDrop ? " Known client-side bug (plur-ai/plur#297): some MCP clients drop " + (partialDrop ? `array-typed parameters from a large arguments payload while keeping the earlier fields (here: ${missingArrayParams.join(", ")}). This is size-sensitive \u2014 the same call often succeeds with a shorter payload, so shrink the other fields (e.g. a briefer summary) as well. ` : "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.' : "";
106
191
  const receivedNote = receivedFields.length > 0 ? `Received fields: [${receivedFields.join(", ")}].` : "Received no fields (the arguments object was empty).";
107
192
  return {
108
193
  ok: false,
@@ -143,7 +228,7 @@ var PLUR_GUIDE = `## PLUR Quick Start
143
228
 
144
229
  ### Core Tools
145
230
  - **plur_learn** \u2014 record corrections, preferences, patterns (CALL THIS OFTEN)
146
- - **plur_recall_hybrid** \u2014 search engrams by topic
231
+ - **plur_recall** \u2014 search engrams by topic (default: hybrid BM25 + embeddings; use mode:"keyword" for BM25-only)
147
232
  - **plur_forget** \u2014 retire an outdated engram`;
148
233
  function getLlmFunction() {
149
234
  const openaiKey = process.env.OPENAI_API_KEY;
@@ -180,12 +265,22 @@ function _cleanExpiredSessions() {
180
265
  if (new Date(state.started_at).getTime() < cutoff) _sessionTelemetry.delete(id);
181
266
  }
182
267
  }
183
- var _activeSessionId;
268
+ function _implicitSessionId() {
269
+ _cleanExpiredSessions();
270
+ if (_sessionTelemetry.size !== 1) return void 0;
271
+ return _sessionTelemetry.keys().next().value;
272
+ }
273
+ function _resolveInjectionSession(args) {
274
+ const explicit = args.session_id;
275
+ if (typeof explicit === "string" && explicit.length > 0) return explicit;
276
+ return _implicitSessionId();
277
+ }
184
278
  function _recordInjectionTelemetry(session_id, injected_packs) {
185
- if (!session_id || !injected_packs) return;
279
+ if (!session_id) return;
186
280
  const state = _sessionTelemetry.get(session_id);
187
281
  if (!state) return;
188
282
  state.injection_calls++;
283
+ if (!injected_packs) return;
189
284
  for (const [pack, count] of Object.entries(injected_packs)) {
190
285
  state.pack_counts[pack] = (state.pack_counts[pack] ?? 0) + count;
191
286
  }
@@ -194,7 +289,7 @@ var CURSOR_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
194
289
  "plur_session_start",
195
290
  "plur_session_end",
196
291
  "plur_learn",
197
- "plur_recall_hybrid",
292
+ "plur_recall",
198
293
  "plur_feedback",
199
294
  "plur_forget",
200
295
  "plur_status",
@@ -368,7 +463,7 @@ function getAllToolDefinitions() {
368
463
  ...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.` } : {}
369
464
  };
370
465
  } catch (err) {
371
- const engram = plur.learn(statement, context);
466
+ const engram = await plur.learn(statement, context);
372
467
  const isOutbox = !!engram.structured_data?._outbox;
373
468
  const routedFallback = engram.structured_data?._routed;
374
469
  mcpCanary.signal("learn_activity");
@@ -483,47 +578,28 @@ function getAllToolDefinitions() {
483
578
  },
484
579
  {
485
580
  name: "plur_recall",
486
- 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.",
487
- annotations: { title: "Recall (BM25)", readOnlyHint: true, idempotentHint: true },
581
+ description: 'Search engrams by topic. Default mode is hybrid (BM25 + local embeddings via RRF) \u2014 set mode:"keyword" for BM25-only. No API calls, fully local. 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.',
582
+ annotations: { title: "Recall", readOnlyHint: true, idempotentHint: true },
488
583
  inputSchema: {
489
584
  type: "object",
490
585
  properties: {
491
586
  query: { type: "string", description: "Search query to find relevant engrams" },
587
+ mode: { type: "string", enum: ["hybrid", "keyword"], description: "Search mode \u2014 hybrid (default): BM25 + embeddings via RRF; keyword: BM25-only (faster, embeddings-independent). budget, caller_session_id and include_episodes apply to hybrid mode only \u2014 in keyword mode use limit to bound results." },
492
588
  scope: { type: "string", description: "Filter by scope (also includes global)" },
493
589
  domain: { type: "string", description: "Filter by domain prefix" },
494
590
  limit: { type: "number", description: "Max results to return (default 20)" },
495
- budget: { type: "object", description: "Budget constraints for sub-agents", properties: { max_tokens: { type: "number" }, max_results: { type: "number" } } },
496
- caller_session_id: { type: "string", description: "Caller session ID for budget enforcement" }
591
+ budget: { type: "object", description: 'Budget constraints for sub-agents. Hybrid mode only \u2014 ignored when mode:"keyword".', properties: { max_tokens: { type: "number" }, max_results: { type: "number" }, ttl_seconds: { type: "number" } } },
592
+ caller_session_id: { type: "string", description: 'Session ID of calling agent for budget enforcement. Hybrid mode only \u2014 ignored when mode:"keyword".' },
593
+ include_episodes: { type: "boolean", description: 'If true, include linked episode summaries for each engram (SP2 episodic anchoring). Hybrid mode only \u2014 ignored when mode:"keyword".' }
497
594
  },
498
595
  required: ["query"]
499
596
  },
500
- handler: async (args, plur) => {
501
- const results = plur.recall(args.query, {
502
- scope: args.scope,
503
- domain: args.domain,
504
- limit: args.limit
505
- });
506
- return {
507
- results: results.map((e) => {
508
- const supersededBy = e.relations?.superseded_by;
509
- const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
510
- return {
511
- id: e.id,
512
- statement: e.statement + annotation,
513
- type: e.type,
514
- scope: e.scope,
515
- domain: e.domain,
516
- retrieval_strength: e.activation.retrieval_strength
517
- };
518
- }),
519
- count: results.length
520
- };
521
- }
597
+ handler: recallHandler
522
598
  },
523
599
  {
524
600
  name: "plur_recall_hybrid",
525
- description: "Hybrid search \u2014 BM25 + local embeddings merged via Reciprocal Rank Fusion. No API calls, fully local. Best default for most use cases.",
526
- annotations: { title: "Recall (hybrid)", readOnlyHint: true, idempotentHint: true },
601
+ description: "[Deprecated since 0.16 \u2014 use plur_recall (mode defaults to hybrid). Alias kept for backwards compatibility; removal earliest 0.18.] Hybrid search \u2014 BM25 + local embeddings merged via Reciprocal Rank Fusion. No API calls, fully local.",
602
+ annotations: { title: "Recall (hybrid) [deprecated alias]", readOnlyHint: true, idempotentHint: true },
527
603
  inputSchema: {
528
604
  type: "object",
529
605
  properties: {
@@ -537,72 +613,12 @@ function getAllToolDefinitions() {
537
613
  },
538
614
  required: ["query"]
539
615
  },
616
+ // True forwarder — delegates to the canonical plur_recall handler and
617
+ // prepends the deprecation notice. No duplicated budget/episode/
618
+ // reranker logic, so a fix to plur_recall reaches this alias too.
540
619
  handler: async (args, plur) => {
541
- const budget = args.budget;
542
- const effectiveLimit = budget?.max_results ?? args.limit ?? 20;
543
- const meta = await plur.recallHybridWithMeta(args.query, {
544
- scope: args.scope,
545
- domain: args.domain,
546
- limit: effectiveLimit
547
- });
548
- recordTelemetry("recall");
549
- const results = meta.engrams;
550
- let truncated = false;
551
- let boundedResults = results;
552
- if (budget?.max_results && results.length > budget.max_results) {
553
- boundedResults = results.slice(0, budget.max_results);
554
- truncated = true;
555
- }
556
- if (budget?.max_tokens) {
557
- let tokenCount = 0;
558
- const withinBudget = [];
559
- for (const e of boundedResults) {
560
- const tokens = Math.ceil(e.statement.length / 4) + 20;
561
- if (tokenCount + tokens > budget.max_tokens) {
562
- truncated = true;
563
- break;
564
- }
565
- withinBudget.push(e);
566
- tokenCount += tokens;
567
- }
568
- boundedResults = withinBudget;
569
- }
570
- const includeEpisodes = args.include_episodes === true;
571
- const response = {
572
- results: boundedResults.map((e) => {
573
- const raw = e;
574
- const supersededBy = e.relations?.superseded_by;
575
- const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
576
- const base = {
577
- id: e.id,
578
- statement: e.statement + annotation,
579
- type: e.type,
580
- scope: e.scope,
581
- domain: e.domain,
582
- retrieval_strength: e.activation.retrieval_strength
583
- };
584
- if (includeEpisodes && raw.episode_ids?.length > 0) {
585
- const episodes = plur.timeline({ search: "" });
586
- base.episodes = episodes.filter((ep) => raw.episode_ids.includes(ep.id)).map((ep) => ({ id: ep.id, summary: ep.summary, timestamp: ep.timestamp }));
587
- }
588
- return base;
589
- }),
590
- count: boundedResults.length,
591
- truncated,
592
- mode: meta.mode
593
- };
594
- if (meta.mode === "hybrid-degraded") {
595
- response.warning = `Embedding layer unavailable \u2014 results are BM25-only. Run plur_doctor for diagnosis. Last error: ${meta.embedderError ?? "unknown"}`;
596
- }
597
- if (resolveRerankerName() !== "off") {
598
- response.reranked = meta.reranked ?? 0;
599
- const rr = plur.rerankerStatus();
600
- if (boundedResults.length > 0 && (meta.reranked ?? 0) === 0 && rr.lastError) {
601
- const corruptNote = rr.lastErrorKind === "corrupt-cache" ? " The model cache looks corrupt (truncated download) \u2014 purge and re-download, see plur_doctor." : "";
602
- response.reranker_warning = `PLUR_RERANKER is set but the reranker did not engage \u2014 results are RRF-only (fusion order, no cross-encoder rerank).${corruptNote} Last error: ${rr.lastError}. Run plur_doctor for diagnosis.`;
603
- }
604
- }
605
- return response;
620
+ const result = await recallHandler({ ...args, mode: "hybrid" }, plur);
621
+ return { deprecated: RECALL_HYBRID_DEPRECATION, ...result };
606
622
  }
607
623
  },
608
624
  {
@@ -614,18 +630,20 @@ function getAllToolDefinitions() {
614
630
  properties: {
615
631
  task: { type: "string", description: "The task description to inject context for" },
616
632
  budget: { type: "number", description: "Token budget for injection (default 2000)" },
617
- scope: { type: "string", description: "Scope filter for engram selection" }
633
+ scope: { type: "string", description: "Scope filter for engram selection" },
634
+ session_id: { type: "string", description: "Session this injection belongs to (from plur_session_start). Optional when one session is open; required for correct attribution when several are." }
618
635
  },
619
636
  required: ["task"]
620
637
  },
621
638
  handler: async (args, plur) => {
622
- const result = plur.inject(args.task, {
639
+ const session_id = _resolveInjectionSession(args);
640
+ const result = await plur.inject(args.task, {
623
641
  budget: args.budget,
624
642
  scope: args.scope,
625
643
  source: "inject",
626
- session_id: _activeSessionId
644
+ session_id
627
645
  });
628
- _recordInjectionTelemetry(_activeSessionId, result.injected_packs);
646
+ _recordInjectionTelemetry(session_id, result.injected_packs);
629
647
  return {
630
648
  directives: result.directives,
631
649
  consider: result.consider,
@@ -646,18 +664,20 @@ function getAllToolDefinitions() {
646
664
  properties: {
647
665
  task: { type: "string", description: "The task description to inject context for" },
648
666
  budget: { type: "number", description: "Token budget for injection (default 2000)" },
649
- scope: { type: "string", description: "Scope filter for engram selection" }
667
+ scope: { type: "string", description: "Scope filter for engram selection" },
668
+ session_id: { type: "string", description: "Session this injection belongs to (from plur_session_start). Optional when one session is open; required for correct attribution when several are." }
650
669
  },
651
670
  required: ["task"]
652
671
  },
653
672
  handler: async (args, plur) => {
673
+ const session_id = _resolveInjectionSession(args);
654
674
  const result = await plur.injectHybrid(args.task, {
655
675
  budget: args.budget,
656
676
  scope: args.scope,
657
677
  source: "inject",
658
- session_id: _activeSessionId
678
+ session_id
659
679
  });
660
- _recordInjectionTelemetry(_activeSessionId, result.injected_packs);
680
+ _recordInjectionTelemetry(session_id, result.injected_packs);
661
681
  return {
662
682
  directives: result.directives,
663
683
  consider: result.consider,
@@ -737,7 +757,7 @@ function getAllToolDefinitions() {
737
757
  },
738
758
  handler: async (args, plur) => {
739
759
  if (args.list === true) {
740
- const pinned = plur.listPinned();
760
+ const pinned = await plur.listPinned();
741
761
  return {
742
762
  count: pinned.length,
743
763
  pinned: pinned.map((e) => ({ id: e.id, statement: e.statement, scope: e.scope, domain: e.domain }))
@@ -767,7 +787,7 @@ function getAllToolDefinitions() {
767
787
  },
768
788
  handler: async (args, plur) => {
769
789
  if (args.id) {
770
- const engram = plur.getById(args.id);
790
+ const engram = await plur.getById(args.id);
771
791
  if (engram) {
772
792
  if (engram.status === "retired") return { success: false, error: `Already retired: ${args.id}` };
773
793
  await plur.forget(args.id);
@@ -777,7 +797,7 @@ function getAllToolDefinitions() {
777
797
  return { success: true, retired: { id: args.id } };
778
798
  }
779
799
  if (args.search) {
780
- const matches = plur.recall(args.search, { limit: 100 });
800
+ const matches = await plur.recall(args.search, { limit: 100 });
781
801
  if (matches.length === 0) return { success: false, error: `No active engrams matching "${args.search}"` };
782
802
  if (matches.length === 1) {
783
803
  await plur.forget(matches[0].id);
@@ -876,7 +896,7 @@ function getAllToolDefinitions() {
876
896
  required: ["content"]
877
897
  },
878
898
  handler: async (args, plur) => {
879
- const candidates = plur.ingest(args.content, {
899
+ const candidates = await plur.ingest(args.content, {
880
900
  source: args.source,
881
901
  extract_only: args.extract_only,
882
902
  scope: args.scope,
@@ -895,32 +915,32 @@ function getAllToolDefinitions() {
895
915
  },
896
916
  {
897
917
  name: "plur_packs_preview",
898
- description: "Preview a pack before installing \u2014 shows manifest, engram list, security scan, and warnings. Always call this before plur_packs_install to let the user review what they are importing.",
918
+ description: "Preview a pack before installing \u2014 shows manifest, engram list, security scan, and warnings. Always call this before plur_packs_install to let the user review what they are importing. Accepts a local directory path or an https:// URL pointing to a .tar.gz archive.",
899
919
  annotations: { title: "Preview pack", readOnlyHint: true, idempotentHint: true },
900
920
  inputSchema: {
901
921
  type: "object",
902
922
  properties: {
903
- source: { type: "string", description: "Path to the pack directory to preview" }
923
+ source: { type: "string", description: "Path to the pack directory, or an https:// URL to a .tar.gz pack archive" }
904
924
  },
905
925
  required: ["source"]
906
926
  },
907
927
  handler: async (args, plur) => {
908
- return plur.previewPack(args.source);
928
+ return await plur.previewPack(args.source);
909
929
  }
910
930
  },
911
931
  {
912
932
  name: "plur_packs_install",
913
- description: "Install an engram pack from a directory path. Runs a mandatory security scan (blocks if secrets found), detects conflicts with existing engrams, and records install metadata in the registry. Call plur_packs_preview first to show the user what the pack contains.",
933
+ description: "Install an engram pack from a local directory path or an https:// URL pointing to a .tar.gz archive. Runs a mandatory security scan (blocks if secrets found), detects conflicts with existing engrams, and records install metadata in the registry. Call plur_packs_preview first to show the user what the pack contains.",
914
934
  annotations: { title: "Install pack", destructiveHint: false, idempotentHint: true },
915
935
  inputSchema: {
916
936
  type: "object",
917
937
  properties: {
918
- source: { type: "string", description: "Path to the pack directory to install" }
938
+ source: { type: "string", description: "Path to the pack directory, or an https:// URL to a .tar.gz pack archive" }
919
939
  },
920
940
  required: ["source"]
921
941
  },
922
942
  handler: async (args, plur) => {
923
- const result = plur.installPack(args.source);
943
+ const result = await plur.installPack(args.source);
924
944
  return {
925
945
  installed: result.installed,
926
946
  name: result.name,
@@ -1015,16 +1035,18 @@ function getAllToolDefinitions() {
1015
1035
  }
1016
1036
  },
1017
1037
  handler: async (args, plur) => {
1018
- const result = plur.sync(args.remote, {
1038
+ const result = await plur.sync(args.remote, {
1019
1039
  full: args.full === true,
1020
1040
  ...args.remote_type === "personal" || args.remote_type === "shared" ? { remoteType: args.remote_type } : {}
1021
1041
  });
1022
1042
  await plur.waitForIndex();
1023
1043
  const indexError = plur.lastIndexError();
1024
1044
  let outbox_result;
1045
+ let outbox_error;
1025
1046
  try {
1026
1047
  outbox_result = await plur.flushOutbox();
1027
- } catch {
1048
+ } catch (err) {
1049
+ outbox_error = err.message;
1028
1050
  }
1029
1051
  return {
1030
1052
  ...result,
@@ -1038,6 +1060,10 @@ function getAllToolDefinitions() {
1038
1060
  pending: outbox_result.failed,
1039
1061
  warnings: outbox_result.expired_warnings
1040
1062
  }
1063
+ } : {},
1064
+ ...outbox_error ? {
1065
+ outbox_error,
1066
+ outbox_warning: `The outbox flush failed \u2014 ${outbox_error}. Engrams routed to a remote store are still queued locally and were NOT pushed. They retry on the next session_start or plur_sync.`
1041
1067
  } : {}
1042
1068
  };
1043
1069
  }
@@ -1077,11 +1103,11 @@ function getAllToolDefinitions() {
1077
1103
  args.llm_api_key,
1078
1104
  args.llm_model
1079
1105
  );
1080
- const sourceEngrams = plur.list({
1106
+ const sourceEngrams = await plur.list({
1081
1107
  domain: args.domain,
1082
1108
  scope: args.scope
1083
1109
  });
1084
- const existingMetas = plur.list().filter((e) => e.id.startsWith("META-"));
1110
+ const existingMetas = (await plur.list()).filter((e) => e.id.startsWith("META-"));
1085
1111
  const result = await extractMetaEngrams(sourceEngrams, llm, {
1086
1112
  run_validation: args.run_validation,
1087
1113
  existing_metas: existingMetas
@@ -1089,7 +1115,7 @@ function getAllToolDefinitions() {
1089
1115
  const isDryRun = args.dry_run === true;
1090
1116
  let saveStats = null;
1091
1117
  if (!isDryRun && result.results.length > 0) {
1092
- saveStats = plur.saveMetaEngrams(result.results);
1118
+ saveStats = await plur.saveMetaEngrams(result.results);
1093
1119
  }
1094
1120
  return {
1095
1121
  engrams_analyzed: result.engrams_analyzed,
@@ -1125,7 +1151,7 @@ function getAllToolDefinitions() {
1125
1151
  }
1126
1152
  },
1127
1153
  handler: async (args, plur) => {
1128
- const allEngrams = plur.list();
1154
+ const allEngrams = await plur.list();
1129
1155
  const metaEngrams = allEngrams.filter((e) => e.id.startsWith("META-"));
1130
1156
  const minConfidence = args.min_confidence ?? 0;
1131
1157
  const levelFilter = args.hierarchy_level;
@@ -1176,20 +1202,20 @@ function getAllToolDefinitions() {
1176
1202
  required: ["meta_engram_id", "test_domain", "llm_base_url", "llm_api_key"]
1177
1203
  },
1178
1204
  handler: async (args, plur) => {
1179
- const allEngrams = plur.list();
1205
+ const allEngrams = await plur.list();
1180
1206
  const meta = allEngrams.find((e) => e.id === args.meta_engram_id);
1181
1207
  if (!meta) {
1182
1208
  throw new Error(`Meta-engram not found: ${args.meta_engram_id}`);
1183
1209
  }
1184
1210
  const testDomain = args.test_domain;
1185
- const testEngrams = plur.list({ domain: testDomain });
1211
+ const testEngrams = await plur.list({ domain: testDomain });
1186
1212
  const llm = makeHttpLlm(
1187
1213
  args.llm_base_url,
1188
1214
  args.llm_api_key,
1189
1215
  args.llm_model
1190
1216
  );
1191
1217
  const result = await validateMetaEngram(meta, testEngrams, testDomain, llm);
1192
- plur.updateEngram(meta);
1218
+ await plur.updateEngram(meta);
1193
1219
  return {
1194
1220
  meta_engram_id: result.meta_engram_id,
1195
1221
  test_domain: result.test_domain,
@@ -1214,7 +1240,7 @@ function getAllToolDefinitions() {
1214
1240
  }
1215
1241
  },
1216
1242
  handler: async (args, plur) => {
1217
- const status = plur.status({
1243
+ const status = await plur.status({
1218
1244
  domain: args.domain,
1219
1245
  created_after: args.created_after
1220
1246
  });
@@ -1246,7 +1272,7 @@ function getAllToolDefinitions() {
1246
1272
  behind: minorVersionsBehind(versionCheck.current, versionCheck.latest)
1247
1273
  }
1248
1274
  } : {},
1249
- capabilities: mcpCanary.status()
1275
+ capabilities: await mcpCanary.status()
1250
1276
  };
1251
1277
  }
1252
1278
  },
@@ -1262,7 +1288,7 @@ function getAllToolDefinitions() {
1262
1288
  },
1263
1289
  handler: async (args, plur) => {
1264
1290
  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);
1291
+ const receipt = await plur.receipt(days ? { days } : void 0);
1266
1292
  return { summary: receiptSummary(receipt), ...receipt };
1267
1293
  }
1268
1294
  },
@@ -1282,7 +1308,7 @@ function getAllToolDefinitions() {
1282
1308
  plur.resetEmbedder();
1283
1309
  plur.resetReranker();
1284
1310
  }
1285
- const status = plur.status();
1311
+ const status = await plur.status();
1286
1312
  const before = plur.embedderStatus();
1287
1313
  if (!before.disabled) {
1288
1314
  try {
@@ -1385,7 +1411,7 @@ function getAllToolDefinitions() {
1385
1411
  evalStatus = { result: run.result, stale: false };
1386
1412
  freshlyRun = !run.cached;
1387
1413
  } else {
1388
- evalStatus = plur.rerankerEvalStatus(rerankerName);
1414
+ evalStatus = await plur.rerankerEvalStatus(rerankerName);
1389
1415
  }
1390
1416
  if (!evalStatus) {
1391
1417
  checks.push({
@@ -1418,7 +1444,7 @@ function getAllToolDefinitions() {
1418
1444
  });
1419
1445
  }
1420
1446
  }
1421
- const canaryStatuses = mcpCanary.status();
1447
+ const canaryStatuses = await mcpCanary.status();
1422
1448
  for (const cs of canaryStatuses) {
1423
1449
  if (!cs.healthy) {
1424
1450
  checks.push({ check: `capability: ${cs.capability}`, ok: false, detail: cs.warning });
@@ -1480,16 +1506,17 @@ function getAllToolDefinitions() {
1480
1506
  const task = args.task;
1481
1507
  const tags = args.tags;
1482
1508
  _cleanExpiredSessions();
1483
- _activeSessionId = session_id;
1484
1509
  _sessionTelemetry.set(session_id, {
1485
1510
  pack_counts: {},
1486
1511
  injection_calls: 0,
1487
1512
  started_at: (/* @__PURE__ */ new Date()).toISOString()
1488
1513
  });
1489
1514
  let outbox_result;
1515
+ let outbox_error;
1490
1516
  try {
1491
1517
  outbox_result = await plur.flushOutbox();
1492
- } catch {
1518
+ } catch (err) {
1519
+ outbox_error = err.message;
1493
1520
  }
1494
1521
  const remote_scopes = plur.getWritableRemoteScopes().map((s) => {
1495
1522
  const md = plur.getScopeMetadata(s.scope);
@@ -1504,7 +1531,7 @@ function getAllToolDefinitions() {
1504
1531
  const default_scope = explicit_default_scope ?? projectConfig.scope ?? null;
1505
1532
  const scope_source = explicit_default_scope ? "caller" : projectConfig.scope ? "project-config" : "none";
1506
1533
  plur.setSessionScope(default_scope);
1507
- const status = plur.status();
1534
+ const status = await plur.status();
1508
1535
  const store_stats = {
1509
1536
  engram_count: status.engram_count,
1510
1537
  episode_count: status.episode_count,
@@ -1529,7 +1556,7 @@ function getAllToolDefinitions() {
1529
1556
  engrams = { text: lines.join("\n"), count: result.count, injected_ids: result.injected_ids };
1530
1557
  }
1531
1558
  } catch {
1532
- const result = plur.inject(task, {
1559
+ const result = await plur.inject(task, {
1533
1560
  scope: tags?.length ? `tags:${tags.join(",")}` : void 0,
1534
1561
  session_id,
1535
1562
  source: "session_start"
@@ -1647,6 +1674,10 @@ Remote store scopes available: ${scopeList}. Set scope PER ENGRAM by content: wh
1647
1674
  warnings: outbox_result.expired_warnings
1648
1675
  }
1649
1676
  } : {},
1677
+ ...outbox_error ? {
1678
+ outbox_error,
1679
+ outbox_warning: `The outbox flush failed \u2014 ${outbox_error}. Engrams routed to a remote store are still queued locally and were NOT pushed. They retry on the next session_start or plur_sync.`
1680
+ } : {},
1650
1681
  // Version staleness warning (issue #151)
1651
1682
  ...version_warning ? { version_warning, version: VERSION } : {}
1652
1683
  };
@@ -1712,7 +1743,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1712
1743
  `engram_suggestions[${i}] must be a string or {statement: string, type?: string}, got ${typeof s}`
1713
1744
  );
1714
1745
  }
1715
- plur.learn(statement, { type });
1746
+ await plur.learn(statement, { type });
1716
1747
  engrams_created++;
1717
1748
  }
1718
1749
  }
@@ -1728,7 +1759,6 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1728
1759
  } : void 0;
1729
1760
  if (session_id) {
1730
1761
  _sessionTelemetry.delete(session_id);
1731
- if (_activeSessionId === session_id) _activeSessionId = void 0;
1732
1762
  }
1733
1763
  try {
1734
1764
  const plurDir = process.env.PLUR_PATH ?? join(homedir(), ".plur");
@@ -1743,7 +1773,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1743
1773
  }
1744
1774
  } catch {
1745
1775
  }
1746
- const status = plur.status();
1776
+ const status = await plur.status();
1747
1777
  return {
1748
1778
  engrams_created,
1749
1779
  episode_id: episode.id,
@@ -1805,7 +1835,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1805
1835
  inputSchema: { type: "object", properties: {} },
1806
1836
  handler: async (_args, plur) => {
1807
1837
  const stores = await plur.listStoresAsync();
1808
- const outboxCount = plur.outboxCount();
1838
+ const outboxCount = await plur.outboxCount();
1809
1839
  return {
1810
1840
  stores,
1811
1841
  count: stores.length,
@@ -1831,7 +1861,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1831
1861
  const raw = args.min_confidence;
1832
1862
  const explicit = typeof raw === "number" && Number.isFinite(raw) ? Math.min(1, Math.max(0, raw)) : void 0;
1833
1863
  const minConfidence = explicit ?? plur.getScopeRoutingConfig().min_confidence ?? SUGGEST_DISPLAY_MIN_CONFIDENCE;
1834
- const candidates = plur.suggestScope({
1864
+ const candidates = await plur.suggestScope({
1835
1865
  statement: args.statement,
1836
1866
  domain: args.domain,
1837
1867
  tags: args.tags
@@ -1897,7 +1927,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1897
1927
  const promoted = [];
1898
1928
  const errors = [];
1899
1929
  for (const id of targetIds) {
1900
- const engram = plur.getById(id);
1930
+ const engram = await plur.getById(id);
1901
1931
  if (!engram) {
1902
1932
  errors.push({ id, error: "Not found" });
1903
1933
  continue;
@@ -1914,7 +1944,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1914
1944
  engram.activation.retrieval_strength = 0.7;
1915
1945
  engram.activation.storage_strength = 1;
1916
1946
  engram.activation.last_accessed = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1917
- plur.updateEngram(engram);
1947
+ await plur.updateEngram(engram);
1918
1948
  promoted.push({ id, statement: engram.statement });
1919
1949
  }
1920
1950
  return { promoted, errors, success: errors.length === 0 };
@@ -1959,12 +1989,12 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1959
1989
  if (args.action === "resolve") {
1960
1990
  const winner = args.winner;
1961
1991
  if (!winner) throw new Error('action:"resolve" requires winner (the engram id to keep)');
1962
- const { record, retired_id } = plur.resolveTension(id, winner);
1992
+ const { record, retired_id } = await plur.resolveTension(id, winner);
1963
1993
  return { record, retired: retired_id, message: `Tension ${id} resolved: ${winner} wins, ${retired_id} retired.` };
1964
1994
  }
1965
1995
  throw new Error(`Unknown action: ${args.action}. Use confirm, dismiss, or resolve.`);
1966
1996
  }
1967
- const engrams = plur.list({
1997
+ const engrams = await plur.list({
1968
1998
  scope: args.scope,
1969
1999
  domain: args.domain
1970
2000
  });
@@ -1988,7 +2018,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1988
2018
  temporal_discount: args.temporal_discount ?? tensionsConfig.temporal_discount,
1989
2019
  ...persist ? { exclude_pairs: new Set(plur.suppressedTensionPairKeys()) } : {}
1990
2020
  });
1991
- const persisted = persist && result.tensions.length > 0 ? plur.recordTensions(result.tensions) : void 0;
2021
+ const persisted = persist && result.tensions.length > 0 ? await plur.recordTensions(result.tensions) : void 0;
1992
2022
  return {
1993
2023
  pairs_checked: result.pairs_checked,
1994
2024
  count: result.new_tensions,
@@ -2041,7 +2071,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
2041
2071
  annotations: { title: "Purge Tensions", destructiveHint: true, idempotentHint: true },
2042
2072
  inputSchema: { type: "object", properties: {} },
2043
2073
  handler: async (_args, plur) => {
2044
- const result = plur.purgeTensions();
2074
+ const result = await plur.purgeTensions();
2045
2075
  return {
2046
2076
  purged_conflict_refs: result.purged_count,
2047
2077
  engrams_modified: result.engrams_modified,
@@ -2064,7 +2094,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
2064
2094
  required: ["episode_id"]
2065
2095
  },
2066
2096
  handler: async (args, plur) => {
2067
- const engram = plur.episodeToEngram(args.episode_id, {
2097
+ const engram = await plur.episodeToEngram(args.episode_id, {
2068
2098
  scope: args.scope,
2069
2099
  domain: args.domain,
2070
2100
  tags: args.tags
@@ -2101,7 +2131,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
2101
2131
  };
2102
2132
  }
2103
2133
  const { listHistoryMonths, readHistory } = await import("@plur-ai/core");
2104
- const status = plur.status();
2134
+ const status = await plur.status();
2105
2135
  const months = listHistoryMonths(status.storage_root);
2106
2136
  const allEvents = [];
2107
2137
  for (const month of months.reverse()) {
@@ -2174,7 +2204,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
2174
2204
  },
2175
2205
  handler: async (args, plur) => {
2176
2206
  const name = args.name;
2177
- let engrams = plur.list({
2207
+ let engrams = await plur.list({
2178
2208
  domain: args.filter_domain,
2179
2209
  scope: args.filter_scope
2180
2210
  });
@@ -2188,9 +2218,9 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
2188
2218
  if (filterType) {
2189
2219
  engrams = engrams.filter((e) => e.type === filterType);
2190
2220
  }
2191
- const { homedir: homedir3 } = await import("os");
2192
- const { join: join3 } = await import("path");
2193
- const outputDir = args.output_dir || join3(homedir3(), "plur-packs", name);
2221
+ const { homedir: homedir2 } = await import("os");
2222
+ const { join: join2 } = await import("path");
2223
+ const outputDir = args.output_dir || join2(homedir2(), "plur-packs", name);
2194
2224
  const result = plur.exportPack(engrams, outputDir, {
2195
2225
  name,
2196
2226
  version: "1.0.0",
@@ -2255,7 +2285,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
2255
2285
  }
2256
2286
  },
2257
2287
  handler: async (args, plur) => {
2258
- const status = plur.status();
2288
+ const status = await plur.status();
2259
2289
  const storagePath = status.storage_root;
2260
2290
  if (!args.force_regenerate) {
2261
2291
  const cached = getProfileForInjection(storagePath);
@@ -2268,7 +2298,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
2268
2298
  }
2269
2299
  const model = args.llm_model ?? selectModelForOperation("profile", status.config?.llm);
2270
2300
  const llm = makeHttpLlm(args.llm_base_url, args.llm_api_key, model);
2271
- const engrams = plur.list({ scope: args.scope });
2301
+ const engrams = await plur.list({ scope: args.scope });
2272
2302
  const profile = await generateProfile(engrams, llm, storagePath, status.config?.profile?.cache_ttl_hours ?? 24);
2273
2303
  return { profile, source: "generated", engram_count: engrams.length, model };
2274
2304
  }
@@ -2276,367 +2306,11 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
2276
2306
  ];
2277
2307
  }
2278
2308
 
2279
- // src/server.ts
2280
- function serverPidPath(baseDir) {
2281
- return join2(baseDir ?? join2(homedir2(), ".plur"), "server.pid");
2282
- }
2283
- function readEnterpriseToken(baseDir) {
2284
- const configPath = join2(baseDir ?? join2(homedir2(), ".plur"), "config.json");
2285
- if (!existsSync2(configPath)) return void 0;
2286
- try {
2287
- const cfg = JSON.parse(readFileSync(configPath, "utf8"));
2288
- const ent = cfg?.enterprise;
2289
- if (!ent || typeof ent.url !== "string" || typeof ent.token !== "string") return void 0;
2290
- return { url: ent.url, token: ent.token, username: ent.username };
2291
- } catch {
2292
- return void 0;
2293
- }
2294
- }
2295
- var _pendingReload = false;
2296
- function isPendingReload() {
2297
- return _pendingReload;
2298
- }
2299
- function clearPendingReload() {
2300
- _pendingReload = false;
2301
- }
2302
- var INSTRUCTIONS = `PLUR is your persistent memory. Corrections, preferences, and conventions persist across sessions as engrams.
2303
-
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.
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
-
2308
- SESSION LIFECYCLE:
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.
2310
- - Without hooks: call plur_session_start at the start, plur_session_end at the end.
2311
-
2312
- DURING the session:
2313
- - When user corrects you ("no, use X not Y") \u2192 call plur_learn immediately
2314
- - When user states a preference ("always X", "never Y") \u2192 call plur_learn immediately
2315
- - When you discover a codebase convention or pattern \u2192 call plur_learn
2316
-
2317
- SCOPE SELECTION (set scope PER engram, by content \u2014 not once per session):
2318
- A single session produces engrams that belong in different stores. Choose the
2319
- "scope" on EACH plur_learn call from what the engram is about:
2320
- - Team / engineering / architecture / shared-convention knowledge \u2192 the matching
2321
- team store scope (e.g. "group:<org>/<team>"). plur_session_start lists the
2322
- scopes this install can write to \u2014 use them.
2323
- - Personal preferences, your workflow, or details specific to one project \u2192 the
2324
- default/local scope (or "project:<name>").
2325
- - "global" is for genuinely cross-project facts only (language gotchas, tool
2326
- quirks). Do NOT let team-relevant knowledge fall back to "global" by omitting
2327
- scope \u2014 with a team store configured it never reaches the team store. If no
2328
- scope fits, prefer the project/local default over global.
2329
-
2330
- OPTIONAL but improves quality:
2331
- - Call plur_feedback to rate which injected engrams helped (positive/negative)
2332
- - Call plur_recall_hybrid before answering factual questions \u2014 the answer may be in memory
2333
-
2334
- Do not ask permission to use these tools \u2014 they are your memory system.
2335
-
2336
- Setup: If this is a fresh install, suggest the user run: npx @plur-ai/mcp init
2337
- This installs hooks for automatic injection + session management. One-time global setup.`;
2338
- var GUIDE_RESOURCE = `# PLUR \u2014 Agent Guide
2339
-
2340
- ## What is PLUR?
2341
-
2342
- Persistent memory for AI agents. Corrections, preferences, and conventions are stored as **engrams** \u2014 small assertions that strengthen with use and decay when irrelevant (ACT-R model). Storage is plain YAML on disk. Search is fully local (BM25 + embeddings). Zero API calls.
2343
-
2344
- ## Quick Start
2345
-
2346
- 1. \`plur_session_start\` \u2014 start a session, inject relevant context
2347
- 2. \`plur_learn\` \u2014 store a new learning
2348
- 3. \`plur_feedback\` \u2014 rate injected engrams
2349
- 4. \`plur_session_end\` \u2014 capture summary and new learnings
2350
-
2351
- ## When to Call Each Tool
2352
-
2353
- | Trigger | Tool |
2354
- |---------|------|
2355
- | Session starts | \`plur_session_start\` with task description |
2356
- | User corrects you | \`plur_learn\` with the correction |
2357
- | User states preference ("always X", "never Y") | \`plur_learn\` with scope and type |
2358
- | You used a recalled engram successfully | \`plur_feedback\` with "positive" |
2359
- | A recalled engram was wrong or irrelevant | \`plur_feedback\` with "negative" |
2360
- | User says "forget X" or a memory is outdated | \`plur_forget\` |
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) |
2363
- | End of session | \`plur_session_end\` with summary and suggestions |
2364
-
2365
- ## Tool Categories
2366
-
2367
- ### Session Management
2368
- - **plur_session_start** \u2014 start a session, inject relevant context
2369
- - **plur_session_end** \u2014 end a session, capture summary and new learnings
2370
-
2371
- ### Core Memory
2372
- - **plur_learn** \u2014 store a correction, preference, or convention
2373
- - **plur_recall** \u2014 BM25 keyword search
2374
- - **plur_recall_hybrid** \u2014 BM25 + embeddings (recommended default)
2375
- - **plur_feedback** \u2014 rate an engram (trains relevance)
2376
- - **plur_forget** \u2014 retire an outdated engram
2377
- - **plur_promote** \u2014 activate a candidate engram
2378
-
2379
- ### Context Injection
2380
- - **plur_inject** \u2014 select engrams for a task (BM25)
2381
- - **plur_inject_hybrid** \u2014 select engrams for a task (BM25 + embeddings, recommended)
2382
-
2383
- ### Episodic Timeline
2384
- - **plur_capture** \u2014 record what happened in a session
2385
- - **plur_timeline** \u2014 query past episodes
2386
-
2387
- ### Knowledge Management
2388
- - **plur_ingest** \u2014 extract engrams from text content
2389
- - **plur_packs_install** \u2014 install curated engram packs
2390
- - **plur_packs_list** \u2014 list installed packs
2391
- - **plur_packs_export** \u2014 export engrams as a shareable pack
2392
-
2393
- ### Multi-Store
2394
- - **plur_stores_add** \u2014 register an additional engram store
2395
- - **plur_stores_list** \u2014 list all configured stores
2396
-
2397
- **Note:** Multi-store is currently config-only. Recall and inject search the primary store. Cross-store search coming in a future release.
2398
-
2399
- ### Sync & Status
2400
- - **plur_sync** \u2014 sync engrams across devices via git
2401
- - **plur_sync_status** \u2014 check sync state
2402
- - **plur_status** \u2014 system health
2403
- - **plur_receipt** \u2014 counted report of what memory retrieved for the user (local, read-only)
2404
-
2405
- ## Scoping
2406
-
2407
- Use \`scope\` to namespace engrams per project:
2408
- - \`scope: "global"\` \u2014 applies everywhere (default)
2409
- - \`scope: "project:my-app"\` \u2014 applies only to my-app
2410
- - Scoped recall automatically includes global engrams
2411
-
2412
- ## Storage
2413
-
2414
- \`\`\`
2415
- ~/.plur/
2416
- \u251C\u2500\u2500 engrams.yaml # learned knowledge
2417
- \u251C\u2500\u2500 episodes.yaml # session timeline
2418
- \u2514\u2500\u2500 config.yaml # settings
2419
- \`\`\`
2420
-
2421
- Override with \`PLUR_PATH\` environment variable.
2422
- `;
2423
- async function createServer(plur, options) {
2424
- const instance = plur ?? new Plur2();
2425
- const tools = getToolDefinitions(options?.profile ?? "lean");
2426
- checkForUpdate("@plur-ai/mcp", VERSION, (r) => {
2427
- if (r.updateAvailable) {
2428
- console.error(`[plur] Update available: ${r.current} \u2192 ${r.latest}. Run: npx @plur-ai/mcp@latest`);
2429
- }
2430
- });
2431
- const server = new Server(
2432
- { name: "plur-mcp", version: VERSION },
2433
- {
2434
- capabilities: {
2435
- tools: {},
2436
- resources: {},
2437
- prompts: {},
2438
- logging: {}
2439
- },
2440
- instructions: INSTRUCTIONS
2441
- }
2442
- );
2443
- server.setRequestHandler("tools/list", async () => ({
2444
- tools: tools.map((t) => ({
2445
- name: t.name,
2446
- description: t.description,
2447
- inputSchema: t.inputSchema,
2448
- ...t.annotations && { annotations: t.annotations }
2449
- }))
2450
- }));
2451
- server.setRequestHandler("tools/call", async (request) => {
2452
- const tool = tools.find((t) => t.name === request.params.name);
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
- }
2465
- return {
2466
- content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${request.params.name}`, success: false }) }],
2467
- isError: true
2468
- };
2469
- }
2470
- mcpCanary.tick();
2471
- try {
2472
- let args = request.params.arguments ?? {};
2473
- const validated = validateToolArgs(tool, args);
2474
- if (!validated.ok) {
2475
- return {
2476
- content: [{ type: "text", text: JSON.stringify(validated.errorPayload) }],
2477
- isError: true
2478
- };
2479
- }
2480
- args = validated.data;
2481
- const result = await tool.handler(args, instance);
2482
- let payload = result;
2483
- let resultIsError = false;
2484
- if (result && typeof result === "object" && result._isError === true) {
2485
- resultIsError = true;
2486
- const { _isError, ...rest } = result;
2487
- payload = rest;
2488
- }
2489
- return {
2490
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
2491
- ...resultIsError ? { isError: true } : {}
2492
- };
2493
- } catch (err) {
2494
- const message = err?.message ?? String(err);
2495
- server.sendLoggingMessage({ level: "error", data: `Tool ${request.params.name} failed: ${message}` });
2496
- return {
2497
- content: [{ type: "text", text: JSON.stringify({ error: message, success: false }) }],
2498
- isError: true
2499
- };
2500
- }
2501
- });
2502
- server.setRequestHandler("resources/list", async () => ({
2503
- resources: [
2504
- {
2505
- uri: "plur://guide",
2506
- name: "PLUR Agent Guide",
2507
- description: "Complete reference for all PLUR tools, when to use them, scoping, and storage",
2508
- mimeType: "text/markdown"
2509
- },
2510
- {
2511
- uri: "plur://status",
2512
- name: "PLUR Status",
2513
- description: "Live system health \u2014 engram count, episode count, pack count, storage path",
2514
- mimeType: "application/json"
2515
- }
2516
- ]
2517
- }));
2518
- server.setRequestHandler("resources/read", async (request) => {
2519
- const uri = request.params.uri;
2520
- if (uri === "plur://guide") {
2521
- const cursorNote = options?.profile === "cursor" || options?.profile === "lean" || options?.profile == null ? `
2522
-
2523
- ## Lean tool profile (default)
2524
-
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.` : "";
2526
- return {
2527
- contents: [{
2528
- uri: "plur://guide",
2529
- mimeType: "text/markdown",
2530
- text: GUIDE_RESOURCE + cursorNote
2531
- }]
2532
- };
2533
- }
2534
- if (uri === "plur://status") {
2535
- const status = instance.status();
2536
- return {
2537
- contents: [{
2538
- uri: "plur://status",
2539
- mimeType: "application/json",
2540
- text: JSON.stringify({
2541
- engram_count: status.engram_count,
2542
- episode_count: status.episode_count,
2543
- pack_count: status.pack_count,
2544
- storage_root: status.storage_root,
2545
- version: VERSION
2546
- }, null, 2)
2547
- }]
2548
- };
2549
- }
2550
- throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
2551
- });
2552
- server.setRequestHandler("prompts/list", async () => ({
2553
- prompts: [
2554
- {
2555
- name: "plur-getting-started",
2556
- description: "Step-by-step guide to set up and start using PLUR memory"
2557
- },
2558
- {
2559
- name: "plur-session-start",
2560
- description: "Load relevant context for a task \u2014 call at the start of each session",
2561
- arguments: [
2562
- { name: "task", description: "Brief description of the task or goal", required: true },
2563
- { name: "scope", description: "Project scope (e.g. project:my-app)", required: false }
2564
- ]
2565
- }
2566
- ]
2567
- }));
2568
- server.setRequestHandler("prompts/get", async (request) => {
2569
- const name = request.params.name;
2570
- if (name === "plur-getting-started") {
2571
- const status = instance.status();
2572
- return {
2573
- description: "Get started with PLUR memory",
2574
- messages: [{
2575
- role: "user",
2576
- content: {
2577
- type: "text",
2578
- text: `I just set up PLUR. Here's my current status:
2579
-
2580
- - Engrams stored: ${status.engram_count}
2581
- - Episodes recorded: ${status.episode_count}
2582
- - Packs installed: ${status.pack_count}
2583
- - Storage: ${status.storage_root}
2584
-
2585
- ${status.engram_count === 0 ? `I have no memories yet. Help me get started by:
2586
- 1. Teaching me a coding preference or convention (I'll use plur_learn)
2587
- 2. Then recalling it to verify it works (I'll use plur_recall_hybrid)
2588
- 3. Rating the recall quality (I'll use plur_feedback)` : `I have ${status.engram_count} engrams stored. Try asking me something related to your project \u2014 I'll check my memory first.`}`
2589
- }
2590
- }]
2591
- };
2592
- }
2593
- if (name === "plur-session-start") {
2594
- const task = request.params.arguments?.task ?? "general work";
2595
- const scope = request.params.arguments?.scope;
2596
- return {
2597
- description: "Load relevant context for this session",
2598
- messages: [{
2599
- role: "user",
2600
- content: {
2601
- type: "text",
2602
- text: `Starting a new session. Task: ${task}${scope ? ` (scope: ${scope})` : ""}
2603
-
2604
- Please:
2605
- 1. Call plur_recall_hybrid with query "${task}"${scope ? ` and scope "${scope}"` : ""} to load relevant memories
2606
- 2. Review the recalled engrams and apply any relevant conventions or preferences
2607
- 3. If any recalled engrams are helpful, call plur_feedback with "positive"
2608
- 4. If any are irrelevant, call plur_feedback with "negative"`
2609
- }
2610
- }]
2611
- };
2612
- }
2613
- throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown prompt: ${name}`);
2614
- });
2615
- return server;
2616
- }
2617
- async function runStdio() {
2618
- const envProfile = process.env.PLUR_TOOL_PROFILE;
2619
- const profile = envProfile === "full" ? "full" : envProfile === "cursor" ? "cursor" : "lean";
2620
- const server = await createServer(void 0, { profile });
2621
- registerFlushOnExit({});
2622
- try {
2623
- writeFileSync(serverPidPath(), String(process.pid));
2624
- } catch {
2625
- }
2626
- if (process.platform !== "win32") {
2627
- process.on("SIGUSR1", () => {
2628
- _pendingReload = true;
2629
- });
2630
- }
2631
- const transport = new StdioServerTransport();
2632
- await server.connect(transport);
2633
- }
2634
2309
  export {
2635
- INSTRUCTIONS,
2636
- clearPendingReload,
2637
- createServer,
2638
- isPendingReload,
2639
- readEnterpriseToken,
2640
- runStdio,
2641
- serverPidPath
2310
+ registerFlushOnExit,
2311
+ VERSION,
2312
+ validateToolArgs,
2313
+ mcpCanary,
2314
+ CURSOR_CORE_TOOL_NAMES,
2315
+ getToolDefinitions
2642
2316
  };