@plur-ai/mcp
Advanced tools
Sorry, the diff of this file is too big to display
| import { | ||
| CURSOR_CORE_TOOL_NAMES, | ||
| VERSION, | ||
| getToolDefinitions, | ||
| mcpCanary, | ||
| registerFlushOnExit, | ||
| validateToolArgs | ||
| } from "./chunk-27IMBG4G.js"; | ||
| // src/server.ts | ||
| import { Server, ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; | ||
| import { existsSync, readFileSync, writeFileSync } from "fs"; | ||
| import { join } from "path"; | ||
| import { homedir } from "os"; | ||
| import { Plur, checkForUpdate } from "@plur-ai/core"; | ||
| function serverPidPath(baseDir) { | ||
| return join(baseDir ?? join(homedir(), ".plur"), "server.pid"); | ||
| } | ||
| function readEnterpriseToken(baseDir) { | ||
| const configPath = join(baseDir ?? join(homedir(), ".plur"), "config.json"); | ||
| if (!existsSync(configPath)) return void 0; | ||
| try { | ||
| const cfg = JSON.parse(readFileSync(configPath, "utf8")); | ||
| const ent = cfg?.enterprise; | ||
| if (!ent || typeof ent.url !== "string" || typeof ent.token !== "string") return void 0; | ||
| return { url: ent.url, token: ent.token, username: ent.username }; | ||
| } catch { | ||
| return void 0; | ||
| } | ||
| } | ||
| var _pendingReload = false; | ||
| function isPendingReload() { | ||
| return _pendingReload; | ||
| } | ||
| function clearPendingReload() { | ||
| _pendingReload = false; | ||
| } | ||
| var INSTRUCTIONS = `PLUR is your persistent memory. Corrections, preferences, and conventions persist across sessions as engrams. | ||
| 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. | ||
| 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. | ||
| SESSION LIFECYCLE: | ||
| - 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. | ||
| - Without hooks: call plur_session_start at the start, plur_session_end at the end. | ||
| DURING the session: | ||
| - When user corrects you ("no, use X not Y") \u2192 call plur_learn immediately | ||
| - When user states a preference ("always X", "never Y") \u2192 call plur_learn immediately | ||
| - When you discover a codebase convention or pattern \u2192 call plur_learn | ||
| SCOPE SELECTION (set scope PER engram, by content \u2014 not once per session): | ||
| A single session produces engrams that belong in different stores. Choose the | ||
| "scope" on EACH plur_learn call from what the engram is about: | ||
| - Team / engineering / architecture / shared-convention knowledge \u2192 the matching | ||
| team store scope (e.g. "group:<org>/<team>"). plur_session_start lists the | ||
| scopes this install can write to \u2014 use them. | ||
| - Personal preferences, your workflow, or details specific to one project \u2192 the | ||
| default/local scope (or "project:<name>"). | ||
| - "global" is for genuinely cross-project facts only (language gotchas, tool | ||
| quirks). Do NOT let team-relevant knowledge fall back to "global" by omitting | ||
| scope \u2014 with a team store configured it never reaches the team store. If no | ||
| scope fits, prefer the project/local default over global. | ||
| OPTIONAL but improves quality: | ||
| - Call plur_feedback to rate which injected engrams helped (positive/negative) | ||
| - Call plur_recall before answering factual questions \u2014 the answer may be in memory | ||
| Do not ask permission to use these tools \u2014 they are your memory system. | ||
| Setup: If this is a fresh install, suggest the user run: npx @plur-ai/mcp init | ||
| This installs hooks for automatic injection + session management. One-time global setup.`; | ||
| var GUIDE_RESOURCE = `# PLUR \u2014 Agent Guide | ||
| ## What is PLUR? | ||
| 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. | ||
| ## Quick Start | ||
| 1. \`plur_session_start\` \u2014 start a session, inject relevant context | ||
| 2. \`plur_learn\` \u2014 store a new learning | ||
| 3. \`plur_feedback\` \u2014 rate injected engrams | ||
| 4. \`plur_session_end\` \u2014 capture summary and new learnings | ||
| ## When to Call Each Tool | ||
| | Trigger | Tool | | ||
| |---------|------| | ||
| | Session starts | \`plur_session_start\` with task description | | ||
| | User corrects you | \`plur_learn\` with the correction | | ||
| | User states preference ("always X", "never Y") | \`plur_learn\` with scope and type | | ||
| | You used a recalled engram successfully | \`plur_feedback\` with "positive" | | ||
| | A recalled engram was wrong or irrelevant | \`plur_feedback\` with "negative" | | ||
| | User says "forget X" or a memory is outdated | \`plur_forget\` | | ||
| | You need to check what's stored | \`plur_status\` or \`plur_packs_list\` | | ||
| | User asks what memory did for them / is memory working | \`plur_receipt\` (relay its \`summary\`; activation_rate is coverage, not quality) | | ||
| | End of session | \`plur_session_end\` with summary and suggestions | | ||
| ## Tool Categories | ||
| ### Session Management | ||
| - **plur_session_start** \u2014 start a session, inject relevant context | ||
| - **plur_session_end** \u2014 end a session, capture summary and new learnings | ||
| ### Core Memory | ||
| - **plur_learn** \u2014 store a correction, preference, or convention | ||
| - **plur_recall** \u2014 hybrid search by default (BM25 + embeddings); pass mode:"keyword" for BM25-only | ||
| - **plur_feedback** \u2014 rate an engram (trains relevance) | ||
| - **plur_forget** \u2014 retire an outdated engram | ||
| - **plur_promote** \u2014 activate a candidate engram | ||
| ### Context Injection | ||
| - **plur_inject** \u2014 select engrams for a task (BM25) | ||
| - **plur_inject_hybrid** \u2014 select engrams for a task (BM25 + embeddings, recommended) | ||
| ### Episodic Timeline | ||
| - **plur_capture** \u2014 record what happened in a session | ||
| - **plur_timeline** \u2014 query past episodes | ||
| ### Knowledge Management | ||
| - **plur_ingest** \u2014 extract engrams from text content | ||
| - **plur_packs_install** \u2014 install curated engram packs | ||
| - **plur_packs_list** \u2014 list installed packs | ||
| - **plur_packs_export** \u2014 export engrams as a shareable pack | ||
| ### Multi-Store | ||
| - **plur_stores_add** \u2014 register an additional engram store | ||
| - **plur_stores_list** \u2014 list all configured stores | ||
| **Note:** Multi-store is currently config-only. Recall and inject search the primary store. Cross-store search coming in a future release. | ||
| ### Sync & Status | ||
| - **plur_sync** \u2014 sync engrams across devices via git | ||
| - **plur_sync_status** \u2014 check sync state | ||
| - **plur_status** \u2014 system health | ||
| - **plur_receipt** \u2014 counted report of what memory retrieved for the user (local, read-only) | ||
| ## Scoping | ||
| Use \`scope\` to namespace engrams per project: | ||
| - \`scope: "global"\` \u2014 applies everywhere (default) | ||
| - \`scope: "project:my-app"\` \u2014 applies only to my-app | ||
| - Scoped recall automatically includes global engrams | ||
| ## Storage | ||
| \`\`\` | ||
| ~/.plur/ | ||
| \u251C\u2500\u2500 engrams.yaml # learned knowledge | ||
| \u251C\u2500\u2500 episodes.yaml # session timeline | ||
| \u2514\u2500\u2500 config.yaml # settings | ||
| \`\`\` | ||
| Override with \`PLUR_PATH\` environment variable. | ||
| `; | ||
| async function createServer(plur, options) { | ||
| const instance = plur ?? new Plur(); | ||
| const tools = getToolDefinitions(options?.profile ?? "lean"); | ||
| checkForUpdate("@plur-ai/mcp", VERSION, (r) => { | ||
| if (r.updateAvailable) { | ||
| console.error(`[plur] Update available: ${r.current} \u2192 ${r.latest}. Run: npx @plur-ai/mcp@latest`); | ||
| } | ||
| }); | ||
| const server = new Server( | ||
| { name: "plur-mcp", version: VERSION }, | ||
| { | ||
| capabilities: { | ||
| tools: {}, | ||
| resources: {}, | ||
| prompts: {}, | ||
| logging: {} | ||
| }, | ||
| instructions: INSTRUCTIONS | ||
| } | ||
| ); | ||
| server.setRequestHandler("tools/list", async () => ({ | ||
| tools: tools.map((t) => ({ | ||
| name: t.name, | ||
| description: t.description, | ||
| inputSchema: t.inputSchema, | ||
| ...t.annotations && { annotations: t.annotations } | ||
| })) | ||
| })); | ||
| server.setRequestHandler("tools/call", async (request) => { | ||
| const tool = tools.find((t) => t.name === request.params.name); | ||
| if (!tool) { | ||
| const hidden = getToolDefinitions("full").find((t) => t.name === request.params.name); | ||
| if (hidden) { | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify({ | ||
| error: `Tool "${request.params.name}" exists but is not directly callable under the current tool profile.`, | ||
| success: false, | ||
| 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.` | ||
| }) }], | ||
| isError: true | ||
| }; | ||
| } | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${request.params.name}`, success: false }) }], | ||
| isError: true | ||
| }; | ||
| } | ||
| mcpCanary.tick(); | ||
| try { | ||
| let args = request.params.arguments ?? {}; | ||
| const validated = validateToolArgs(tool, args); | ||
| if (!validated.ok) { | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(validated.errorPayload) }], | ||
| isError: true | ||
| }; | ||
| } | ||
| args = validated.data; | ||
| const result = await tool.handler(args, instance); | ||
| let payload = result; | ||
| let resultIsError = false; | ||
| if (result && typeof result === "object" && result._isError === true) { | ||
| resultIsError = true; | ||
| const { _isError, ...rest } = result; | ||
| payload = rest; | ||
| } | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], | ||
| ...resultIsError ? { isError: true } : {} | ||
| }; | ||
| } catch (err) { | ||
| const message = err?.message ?? String(err); | ||
| server.sendLoggingMessage({ level: "error", data: `Tool ${request.params.name} failed: ${message}` }); | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify({ error: message, success: false }) }], | ||
| isError: true | ||
| }; | ||
| } | ||
| }); | ||
| server.setRequestHandler("resources/list", async () => ({ | ||
| resources: [ | ||
| { | ||
| uri: "plur://guide", | ||
| name: "PLUR Agent Guide", | ||
| description: "Complete reference for all PLUR tools, when to use them, scoping, and storage", | ||
| mimeType: "text/markdown" | ||
| }, | ||
| { | ||
| uri: "plur://status", | ||
| name: "PLUR Status", | ||
| description: "Live system health \u2014 engram count, episode count, pack count, storage path", | ||
| mimeType: "application/json" | ||
| } | ||
| ] | ||
| })); | ||
| server.setRequestHandler("resources/read", async (request) => { | ||
| const uri = request.params.uri; | ||
| if (uri === "plur://guide") { | ||
| const cursorNote = options?.profile === "cursor" || options?.profile === "lean" || options?.profile == null ? ` | ||
| ## Lean tool profile (default) | ||
| 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.` : ""; | ||
| return { | ||
| contents: [{ | ||
| uri: "plur://guide", | ||
| mimeType: "text/markdown", | ||
| text: GUIDE_RESOURCE + cursorNote | ||
| }] | ||
| }; | ||
| } | ||
| if (uri === "plur://status") { | ||
| const status = await instance.status(); | ||
| return { | ||
| contents: [{ | ||
| uri: "plur://status", | ||
| mimeType: "application/json", | ||
| text: JSON.stringify({ | ||
| engram_count: status.engram_count, | ||
| episode_count: status.episode_count, | ||
| pack_count: status.pack_count, | ||
| storage_root: status.storage_root, | ||
| version: VERSION | ||
| }, null, 2) | ||
| }] | ||
| }; | ||
| } | ||
| throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown resource: ${uri}`); | ||
| }); | ||
| server.setRequestHandler("prompts/list", async () => ({ | ||
| prompts: [ | ||
| { | ||
| name: "plur-getting-started", | ||
| description: "Step-by-step guide to set up and start using PLUR memory" | ||
| }, | ||
| { | ||
| name: "plur-session-start", | ||
| description: "Load relevant context for a task \u2014 call at the start of each session", | ||
| arguments: [ | ||
| { name: "task", description: "Brief description of the task or goal", required: true }, | ||
| { name: "scope", description: "Project scope (e.g. project:my-app)", required: false } | ||
| ] | ||
| } | ||
| ] | ||
| })); | ||
| server.setRequestHandler("prompts/get", async (request) => { | ||
| const name = request.params.name; | ||
| if (name === "plur-getting-started") { | ||
| const status = await instance.status(); | ||
| return { | ||
| description: "Get started with PLUR memory", | ||
| messages: [{ | ||
| role: "user", | ||
| content: { | ||
| type: "text", | ||
| text: `I just set up PLUR. Here's my current status: | ||
| - Engrams stored: ${status.engram_count} | ||
| - Episodes recorded: ${status.episode_count} | ||
| - Packs installed: ${status.pack_count} | ||
| - Storage: ${status.storage_root} | ||
| ${status.engram_count === 0 ? `I have no memories yet. Help me get started by: | ||
| 1. Teaching me a coding preference or convention (I'll use plur_learn) | ||
| 2. Then recalling it to verify it works (I'll use plur_recall) | ||
| 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.`}` | ||
| } | ||
| }] | ||
| }; | ||
| } | ||
| if (name === "plur-session-start") { | ||
| const task = request.params.arguments?.task ?? "general work"; | ||
| const scope = request.params.arguments?.scope; | ||
| return { | ||
| description: "Load relevant context for this session", | ||
| messages: [{ | ||
| role: "user", | ||
| content: { | ||
| type: "text", | ||
| text: `Starting a new session. Task: ${task}${scope ? ` (scope: ${scope})` : ""} | ||
| Please: | ||
| 1. Call plur_recall with query "${task}"${scope ? ` and scope "${scope}"` : ""} to load relevant memories | ||
| 2. Review the recalled engrams and apply any relevant conventions or preferences | ||
| 3. If any recalled engrams are helpful, call plur_feedback with "positive" | ||
| 4. If any are irrelevant, call plur_feedback with "negative"` | ||
| } | ||
| }] | ||
| }; | ||
| } | ||
| throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown prompt: ${name}`); | ||
| }); | ||
| return server; | ||
| } | ||
| async function runStdio() { | ||
| const envProfile = process.env.PLUR_TOOL_PROFILE; | ||
| const profile = envProfile === "full" ? "full" : envProfile === "cursor" ? "cursor" : "lean"; | ||
| const server = await createServer(void 0, { profile }); | ||
| registerFlushOnExit({}); | ||
| try { | ||
| writeFileSync(serverPidPath(), String(process.pid)); | ||
| } catch { | ||
| } | ||
| if (process.platform !== "win32") { | ||
| process.on("SIGUSR1", () => { | ||
| _pendingReload = true; | ||
| }); | ||
| } | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| } | ||
| export { | ||
| INSTRUCTIONS, | ||
| clearPendingReload, | ||
| createServer, | ||
| isPendingReload, | ||
| readEnterpriseToken, | ||
| runStdio, | ||
| serverPidPath | ||
| }; |
+2
-2
@@ -8,3 +8,3 @@ #!/usr/bin/env node | ||
| import { homedir, platform } from "os"; | ||
| var VERSION = "0.16.0"; | ||
| var VERSION = "0.16.1"; | ||
| var HELP = `plur-mcp v${VERSION} \u2014 persistent memory for AI agents | ||
@@ -355,3 +355,3 @@ | ||
| if (arg === "serve" || arg === void 0) { | ||
| const { runStdio } = await import("./server-VHSO4XMV.js"); | ||
| const { runStdio } = await import("./server-GMXICHML.js"); | ||
| runStdio().catch((err) => { | ||
@@ -358,0 +358,0 @@ console.error("Failed to start PLUR MCP server:", err); |
@@ -5,3 +5,3 @@ import { | ||
| validateToolArgs | ||
| } from "./chunk-TPL7QPPO.js"; | ||
| } from "./chunk-27IMBG4G.js"; | ||
@@ -8,0 +8,0 @@ // src/tools-export.ts |
+2
-2
| { | ||
| "name": "@plur-ai/mcp", | ||
| "mcpName": "io.github.plur-ai/plur", | ||
| "version": "0.16.0", | ||
| "version": "0.16.1", | ||
| "type": "module", | ||
@@ -19,3 +19,3 @@ "bin": { | ||
| "zod": "^3.23.0", | ||
| "@plur-ai/core": "0.16.0" | ||
| "@plur-ai/core": "0.16.1" | ||
| }, | ||
@@ -22,0 +22,0 @@ "devDependencies": { |
Sorry, the diff of this file is too big to display
| import { | ||
| CURSOR_CORE_TOOL_NAMES, | ||
| VERSION, | ||
| getToolDefinitions, | ||
| mcpCanary, | ||
| registerFlushOnExit, | ||
| validateToolArgs | ||
| } from "./chunk-TPL7QPPO.js"; | ||
| // src/server.ts | ||
| import { Server, ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; | ||
| import { existsSync, readFileSync, writeFileSync } from "fs"; | ||
| import { join } from "path"; | ||
| import { homedir } from "os"; | ||
| import { Plur, checkForUpdate } from "@plur-ai/core"; | ||
| function serverPidPath(baseDir) { | ||
| return join(baseDir ?? join(homedir(), ".plur"), "server.pid"); | ||
| } | ||
| function readEnterpriseToken(baseDir) { | ||
| const configPath = join(baseDir ?? join(homedir(), ".plur"), "config.json"); | ||
| if (!existsSync(configPath)) return void 0; | ||
| try { | ||
| const cfg = JSON.parse(readFileSync(configPath, "utf8")); | ||
| const ent = cfg?.enterprise; | ||
| if (!ent || typeof ent.url !== "string" || typeof ent.token !== "string") return void 0; | ||
| return { url: ent.url, token: ent.token, username: ent.username }; | ||
| } catch { | ||
| return void 0; | ||
| } | ||
| } | ||
| var _pendingReload = false; | ||
| function isPendingReload() { | ||
| return _pendingReload; | ||
| } | ||
| function clearPendingReload() { | ||
| _pendingReload = false; | ||
| } | ||
| var INSTRUCTIONS = `PLUR is your persistent memory. Corrections, preferences, and conventions persist across sessions as engrams. | ||
| 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. | ||
| 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. | ||
| SESSION LIFECYCLE: | ||
| - 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. | ||
| - Without hooks: call plur_session_start at the start, plur_session_end at the end. | ||
| DURING the session: | ||
| - When user corrects you ("no, use X not Y") \u2192 call plur_learn immediately | ||
| - When user states a preference ("always X", "never Y") \u2192 call plur_learn immediately | ||
| - When you discover a codebase convention or pattern \u2192 call plur_learn | ||
| SCOPE SELECTION (set scope PER engram, by content \u2014 not once per session): | ||
| A single session produces engrams that belong in different stores. Choose the | ||
| "scope" on EACH plur_learn call from what the engram is about: | ||
| - Team / engineering / architecture / shared-convention knowledge \u2192 the matching | ||
| team store scope (e.g. "group:<org>/<team>"). plur_session_start lists the | ||
| scopes this install can write to \u2014 use them. | ||
| - Personal preferences, your workflow, or details specific to one project \u2192 the | ||
| default/local scope (or "project:<name>"). | ||
| - "global" is for genuinely cross-project facts only (language gotchas, tool | ||
| quirks). Do NOT let team-relevant knowledge fall back to "global" by omitting | ||
| scope \u2014 with a team store configured it never reaches the team store. If no | ||
| scope fits, prefer the project/local default over global. | ||
| OPTIONAL but improves quality: | ||
| - Call plur_feedback to rate which injected engrams helped (positive/negative) | ||
| - Call plur_recall before answering factual questions \u2014 the answer may be in memory | ||
| Do not ask permission to use these tools \u2014 they are your memory system. | ||
| Setup: If this is a fresh install, suggest the user run: npx @plur-ai/mcp init | ||
| This installs hooks for automatic injection + session management. One-time global setup.`; | ||
| var GUIDE_RESOURCE = `# PLUR \u2014 Agent Guide | ||
| ## What is PLUR? | ||
| 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. | ||
| ## Quick Start | ||
| 1. \`plur_session_start\` \u2014 start a session, inject relevant context | ||
| 2. \`plur_learn\` \u2014 store a new learning | ||
| 3. \`plur_feedback\` \u2014 rate injected engrams | ||
| 4. \`plur_session_end\` \u2014 capture summary and new learnings | ||
| ## When to Call Each Tool | ||
| | Trigger | Tool | | ||
| |---------|------| | ||
| | Session starts | \`plur_session_start\` with task description | | ||
| | User corrects you | \`plur_learn\` with the correction | | ||
| | User states preference ("always X", "never Y") | \`plur_learn\` with scope and type | | ||
| | You used a recalled engram successfully | \`plur_feedback\` with "positive" | | ||
| | A recalled engram was wrong or irrelevant | \`plur_feedback\` with "negative" | | ||
| | User says "forget X" or a memory is outdated | \`plur_forget\` | | ||
| | You need to check what's stored | \`plur_status\` or \`plur_packs_list\` | | ||
| | User asks what memory did for them / is memory working | \`plur_receipt\` (relay its \`summary\`; activation_rate is coverage, not quality) | | ||
| | End of session | \`plur_session_end\` with summary and suggestions | | ||
| ## Tool Categories | ||
| ### Session Management | ||
| - **plur_session_start** \u2014 start a session, inject relevant context | ||
| - **plur_session_end** \u2014 end a session, capture summary and new learnings | ||
| ### Core Memory | ||
| - **plur_learn** \u2014 store a correction, preference, or convention | ||
| - **plur_recall** \u2014 hybrid search by default (BM25 + embeddings); pass mode:"keyword" for BM25-only | ||
| - **plur_feedback** \u2014 rate an engram (trains relevance) | ||
| - **plur_forget** \u2014 retire an outdated engram | ||
| - **plur_promote** \u2014 activate a candidate engram | ||
| ### Context Injection | ||
| - **plur_inject** \u2014 select engrams for a task (BM25) | ||
| - **plur_inject_hybrid** \u2014 select engrams for a task (BM25 + embeddings, recommended) | ||
| ### Episodic Timeline | ||
| - **plur_capture** \u2014 record what happened in a session | ||
| - **plur_timeline** \u2014 query past episodes | ||
| ### Knowledge Management | ||
| - **plur_ingest** \u2014 extract engrams from text content | ||
| - **plur_packs_install** \u2014 install curated engram packs | ||
| - **plur_packs_list** \u2014 list installed packs | ||
| - **plur_packs_export** \u2014 export engrams as a shareable pack | ||
| ### Multi-Store | ||
| - **plur_stores_add** \u2014 register an additional engram store | ||
| - **plur_stores_list** \u2014 list all configured stores | ||
| **Note:** Multi-store is currently config-only. Recall and inject search the primary store. Cross-store search coming in a future release. | ||
| ### Sync & Status | ||
| - **plur_sync** \u2014 sync engrams across devices via git | ||
| - **plur_sync_status** \u2014 check sync state | ||
| - **plur_status** \u2014 system health | ||
| - **plur_receipt** \u2014 counted report of what memory retrieved for the user (local, read-only) | ||
| ## Scoping | ||
| Use \`scope\` to namespace engrams per project: | ||
| - \`scope: "global"\` \u2014 applies everywhere (default) | ||
| - \`scope: "project:my-app"\` \u2014 applies only to my-app | ||
| - Scoped recall automatically includes global engrams | ||
| ## Storage | ||
| \`\`\` | ||
| ~/.plur/ | ||
| \u251C\u2500\u2500 engrams.yaml # learned knowledge | ||
| \u251C\u2500\u2500 episodes.yaml # session timeline | ||
| \u2514\u2500\u2500 config.yaml # settings | ||
| \`\`\` | ||
| Override with \`PLUR_PATH\` environment variable. | ||
| `; | ||
| async function createServer(plur, options) { | ||
| const instance = plur ?? new Plur(); | ||
| const tools = getToolDefinitions(options?.profile ?? "lean"); | ||
| checkForUpdate("@plur-ai/mcp", VERSION, (r) => { | ||
| if (r.updateAvailable) { | ||
| console.error(`[plur] Update available: ${r.current} \u2192 ${r.latest}. Run: npx @plur-ai/mcp@latest`); | ||
| } | ||
| }); | ||
| const server = new Server( | ||
| { name: "plur-mcp", version: VERSION }, | ||
| { | ||
| capabilities: { | ||
| tools: {}, | ||
| resources: {}, | ||
| prompts: {}, | ||
| logging: {} | ||
| }, | ||
| instructions: INSTRUCTIONS | ||
| } | ||
| ); | ||
| server.setRequestHandler("tools/list", async () => ({ | ||
| tools: tools.map((t) => ({ | ||
| name: t.name, | ||
| description: t.description, | ||
| inputSchema: t.inputSchema, | ||
| ...t.annotations && { annotations: t.annotations } | ||
| })) | ||
| })); | ||
| server.setRequestHandler("tools/call", async (request) => { | ||
| const tool = tools.find((t) => t.name === request.params.name); | ||
| if (!tool) { | ||
| const hidden = getToolDefinitions("full").find((t) => t.name === request.params.name); | ||
| if (hidden) { | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify({ | ||
| error: `Tool "${request.params.name}" exists but is not directly callable under the current tool profile.`, | ||
| success: false, | ||
| 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.` | ||
| }) }], | ||
| isError: true | ||
| }; | ||
| } | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${request.params.name}`, success: false }) }], | ||
| isError: true | ||
| }; | ||
| } | ||
| mcpCanary.tick(); | ||
| try { | ||
| let args = request.params.arguments ?? {}; | ||
| const validated = validateToolArgs(tool, args); | ||
| if (!validated.ok) { | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(validated.errorPayload) }], | ||
| isError: true | ||
| }; | ||
| } | ||
| args = validated.data; | ||
| const result = await tool.handler(args, instance); | ||
| let payload = result; | ||
| let resultIsError = false; | ||
| if (result && typeof result === "object" && result._isError === true) { | ||
| resultIsError = true; | ||
| const { _isError, ...rest } = result; | ||
| payload = rest; | ||
| } | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], | ||
| ...resultIsError ? { isError: true } : {} | ||
| }; | ||
| } catch (err) { | ||
| const message = err?.message ?? String(err); | ||
| server.sendLoggingMessage({ level: "error", data: `Tool ${request.params.name} failed: ${message}` }); | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify({ error: message, success: false }) }], | ||
| isError: true | ||
| }; | ||
| } | ||
| }); | ||
| server.setRequestHandler("resources/list", async () => ({ | ||
| resources: [ | ||
| { | ||
| uri: "plur://guide", | ||
| name: "PLUR Agent Guide", | ||
| description: "Complete reference for all PLUR tools, when to use them, scoping, and storage", | ||
| mimeType: "text/markdown" | ||
| }, | ||
| { | ||
| uri: "plur://status", | ||
| name: "PLUR Status", | ||
| description: "Live system health \u2014 engram count, episode count, pack count, storage path", | ||
| mimeType: "application/json" | ||
| } | ||
| ] | ||
| })); | ||
| server.setRequestHandler("resources/read", async (request) => { | ||
| const uri = request.params.uri; | ||
| if (uri === "plur://guide") { | ||
| const cursorNote = options?.profile === "cursor" || options?.profile === "lean" || options?.profile == null ? ` | ||
| ## Lean tool profile (default) | ||
| 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.` : ""; | ||
| return { | ||
| contents: [{ | ||
| uri: "plur://guide", | ||
| mimeType: "text/markdown", | ||
| text: GUIDE_RESOURCE + cursorNote | ||
| }] | ||
| }; | ||
| } | ||
| if (uri === "plur://status") { | ||
| const status = await instance.status(); | ||
| return { | ||
| contents: [{ | ||
| uri: "plur://status", | ||
| mimeType: "application/json", | ||
| text: JSON.stringify({ | ||
| engram_count: status.engram_count, | ||
| episode_count: status.episode_count, | ||
| pack_count: status.pack_count, | ||
| storage_root: status.storage_root, | ||
| version: VERSION | ||
| }, null, 2) | ||
| }] | ||
| }; | ||
| } | ||
| throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown resource: ${uri}`); | ||
| }); | ||
| server.setRequestHandler("prompts/list", async () => ({ | ||
| prompts: [ | ||
| { | ||
| name: "plur-getting-started", | ||
| description: "Step-by-step guide to set up and start using PLUR memory" | ||
| }, | ||
| { | ||
| name: "plur-session-start", | ||
| description: "Load relevant context for a task \u2014 call at the start of each session", | ||
| arguments: [ | ||
| { name: "task", description: "Brief description of the task or goal", required: true }, | ||
| { name: "scope", description: "Project scope (e.g. project:my-app)", required: false } | ||
| ] | ||
| } | ||
| ] | ||
| })); | ||
| server.setRequestHandler("prompts/get", async (request) => { | ||
| const name = request.params.name; | ||
| if (name === "plur-getting-started") { | ||
| const status = await instance.status(); | ||
| return { | ||
| description: "Get started with PLUR memory", | ||
| messages: [{ | ||
| role: "user", | ||
| content: { | ||
| type: "text", | ||
| text: `I just set up PLUR. Here's my current status: | ||
| - Engrams stored: ${status.engram_count} | ||
| - Episodes recorded: ${status.episode_count} | ||
| - Packs installed: ${status.pack_count} | ||
| - Storage: ${status.storage_root} | ||
| ${status.engram_count === 0 ? `I have no memories yet. Help me get started by: | ||
| 1. Teaching me a coding preference or convention (I'll use plur_learn) | ||
| 2. Then recalling it to verify it works (I'll use plur_recall) | ||
| 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.`}` | ||
| } | ||
| }] | ||
| }; | ||
| } | ||
| if (name === "plur-session-start") { | ||
| const task = request.params.arguments?.task ?? "general work"; | ||
| const scope = request.params.arguments?.scope; | ||
| return { | ||
| description: "Load relevant context for this session", | ||
| messages: [{ | ||
| role: "user", | ||
| content: { | ||
| type: "text", | ||
| text: `Starting a new session. Task: ${task}${scope ? ` (scope: ${scope})` : ""} | ||
| Please: | ||
| 1. Call plur_recall with query "${task}"${scope ? ` and scope "${scope}"` : ""} to load relevant memories | ||
| 2. Review the recalled engrams and apply any relevant conventions or preferences | ||
| 3. If any recalled engrams are helpful, call plur_feedback with "positive" | ||
| 4. If any are irrelevant, call plur_feedback with "negative"` | ||
| } | ||
| }] | ||
| }; | ||
| } | ||
| throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown prompt: ${name}`); | ||
| }); | ||
| return server; | ||
| } | ||
| async function runStdio() { | ||
| const envProfile = process.env.PLUR_TOOL_PROFILE; | ||
| const profile = envProfile === "full" ? "full" : envProfile === "cursor" ? "cursor" : "lean"; | ||
| const server = await createServer(void 0, { profile }); | ||
| registerFlushOnExit({}); | ||
| try { | ||
| writeFileSync(serverPidPath(), String(process.pid)); | ||
| } catch { | ||
| } | ||
| if (process.platform !== "win32") { | ||
| process.on("SIGUSR1", () => { | ||
| _pendingReload = true; | ||
| }); | ||
| } | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| } | ||
| export { | ||
| INSTRUCTIONS, | ||
| clearPendingReload, | ||
| createServer, | ||
| isPendingReload, | ||
| readEnterpriseToken, | ||
| runStdio, | ||
| serverPidPath | ||
| }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
15
7.14%+ Added
- Removed
Updated