@xultrax-web/agent-memory-mcp 0.11.6 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +161 -37
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
25
25
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26
|
-
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
+
import { CallToolRequestSchema, CreateMessageResultSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
27
27
|
import Fuse from "fuse.js";
|
|
28
28
|
import matter from "gray-matter";
|
|
29
29
|
import { spawnSync } from "node:child_process";
|
|
@@ -615,27 +615,27 @@ export function toolDeleteMemory(args) {
|
|
|
615
615
|
const fp = memoryFilePath(name);
|
|
616
616
|
if (!existsSync(fp))
|
|
617
617
|
return `Memory "${name}" not found.`;
|
|
618
|
-
// v0.
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
// will require receipts unconditionally for destructive ops.
|
|
618
|
+
// v0.12.0 · receipt REQUIRED for delete_memory. The v0.11.x back-compat
|
|
619
|
+
// path (delete without receipt) is removed. Callers MUST first call
|
|
620
|
+
// check_action({action_type: 'deletions'}) to obtain a fresh receipt,
|
|
621
|
+
// then pass it to delete_memory as the `receipt` argument.
|
|
623
622
|
const receipt = parseReceiptArg(args.receipt);
|
|
624
|
-
if (receipt) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
logEvent("
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
623
|
+
if (!receipt) {
|
|
624
|
+
logEvent("delete_refused_no_receipt", { name });
|
|
625
|
+
throw new Error(`delete_memory refused · receipt required (v0.12.0+). ` +
|
|
626
|
+
`Call check_action({action: 'delete memory ${name}', action_type: 'deletions'}) ` +
|
|
627
|
+
`first, then pass the issued receipt as the 'receipt' argument to delete_memory.`);
|
|
628
|
+
}
|
|
629
|
+
const v = validateReceipt(receipt, {
|
|
630
|
+
required_caveats: [{ type: "action_type", value: "deletions" }],
|
|
631
|
+
});
|
|
632
|
+
if (!v.valid) {
|
|
633
|
+
logEvent("delete_denied", { name, reason: v.reason, receipt_id: receipt.id });
|
|
634
|
+
throw new Error(`delete_memory refused · receipt invalid (${v.reason}). ` +
|
|
635
|
+
`Call check_action({action: 'delete memory ${name}', action_type: 'deletions'}) ` +
|
|
636
|
+
`to get a fresh receipt.`);
|
|
638
637
|
}
|
|
638
|
+
logEvent("delete_approved_via_receipt", { name, receipt_id: receipt.id });
|
|
639
639
|
return withLock(() => {
|
|
640
640
|
ensureTrash();
|
|
641
641
|
// Trash filename: <unix-ms>-<name>.md so restore can pick the
|
|
@@ -644,12 +644,9 @@ export function toolDeleteMemory(args) {
|
|
|
644
644
|
const trashPath = join(TRASH_DIR, `${ts}-${name}.md`);
|
|
645
645
|
renameSync(fp, trashPath);
|
|
646
646
|
removeIndexEntryUnlocked(name);
|
|
647
|
-
logEvent("delete", { name, trash: `${ts}-${name}.md`, gated:
|
|
647
|
+
logEvent("delete", { name, trash: `${ts}-${name}.md`, gated: true });
|
|
648
648
|
log("debug", "delete_memory", { name });
|
|
649
|
-
|
|
650
|
-
? ` (gated by receipt ${receipt.id})`
|
|
651
|
-
: " (no receipt · v0.11.3 back-compat path)";
|
|
652
|
-
return `Moved "${name}" to trash${gateMsg}. Restore with: agent-memory restore ${name}`;
|
|
649
|
+
return `Moved "${name}" to trash (gated by receipt ${receipt.id}). Restore with: agent-memory restore ${name}`;
|
|
653
650
|
});
|
|
654
651
|
}
|
|
655
652
|
function toolRestoreMemory(args) {
|
|
@@ -1504,15 +1501,116 @@ export function checkActionAgainstRules(action, actionType) {
|
|
|
1504
1501
|
}
|
|
1505
1502
|
return { hard, soft, rules_evaluated: rules.length };
|
|
1506
1503
|
}
|
|
1507
|
-
|
|
1504
|
+
/**
|
|
1505
|
+
* Tier-2 Sampling enrichment · runs ONE rule's natural-language
|
|
1506
|
+
* applies_when conditions past an LLM via MCP sampling/createMessage.
|
|
1507
|
+
* The server makes the request; the client decides (per MCP spec)
|
|
1508
|
+
* whether to forward to its LLM, prompt the user, or refuse.
|
|
1509
|
+
*
|
|
1510
|
+
* On any error (client lacks sampling, user refused, unparseable
|
|
1511
|
+
* response), returns null — Tier-2 silently degrades to "no extra
|
|
1512
|
+
* violations found" and we ship the Tier-1 result.
|
|
1513
|
+
*/
|
|
1514
|
+
function clientSupportsSampling() {
|
|
1515
|
+
// server.getClientCapabilities() is undefined before the MCP initialize
|
|
1516
|
+
// handshake; once initialized, returns the capabilities the client
|
|
1517
|
+
// declared. We only call Sampling if `sampling` is in there — saves a
|
|
1518
|
+
// round-trip and prevents test harnesses (which don't respond to
|
|
1519
|
+
// sampling/createMessage) from hanging.
|
|
1520
|
+
try {
|
|
1521
|
+
const caps = server.getClientCapabilities();
|
|
1522
|
+
return !!caps?.sampling;
|
|
1523
|
+
}
|
|
1524
|
+
catch {
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
async function runTier2Sampling(rule, action, actionType) {
|
|
1529
|
+
if (!rule.applies_when || rule.applies_when.length === 0)
|
|
1530
|
+
return null;
|
|
1531
|
+
if (!clientSupportsSampling())
|
|
1532
|
+
return null;
|
|
1533
|
+
const prompt = `You are evaluating whether a proposed action violates an operator rule.\n\n` +
|
|
1534
|
+
`RULE:\n` +
|
|
1535
|
+
` name: ${rule.name}\n` +
|
|
1536
|
+
` description: ${rule.description}\n` +
|
|
1537
|
+
` severity: ${rule.severity ?? "soft"}\n` +
|
|
1538
|
+
` applies_when:\n` +
|
|
1539
|
+
rule.applies_when.map((s) => ` - ${s}`).join("\n") +
|
|
1540
|
+
`\n\nPROPOSED ACTION:\n` +
|
|
1541
|
+
` ${action}\n` +
|
|
1542
|
+
` (category: ${actionType})\n\n` +
|
|
1543
|
+
`Does the proposed action match any of the "applies_when" conditions?\n` +
|
|
1544
|
+
`Respond with strict JSON only, no commentary: {"violates": true|false, "reason": "..."}.\n` +
|
|
1545
|
+
`If the action is ambiguous, answer false.`;
|
|
1546
|
+
try {
|
|
1547
|
+
const result = await server.request({
|
|
1548
|
+
method: "sampling/createMessage",
|
|
1549
|
+
params: {
|
|
1550
|
+
messages: [{ role: "user", content: { type: "text", text: prompt } }],
|
|
1551
|
+
systemPrompt: "You are a strict policy evaluator. Reply with JSON only.",
|
|
1552
|
+
maxTokens: 200,
|
|
1553
|
+
modelPreferences: { intelligencePriority: 0.8, speedPriority: 0.4 },
|
|
1554
|
+
},
|
|
1555
|
+
}, CreateMessageResultSchema);
|
|
1556
|
+
const text = result.content.type === "text" ? result.content.text : "";
|
|
1557
|
+
// Tolerate a stray code-fence around the JSON.
|
|
1558
|
+
const cleaned = text.trim().replace(/^```(?:json)?\s*|\s*```$/g, "");
|
|
1559
|
+
const parsed = JSON.parse(cleaned);
|
|
1560
|
+
if (parsed.violates === true) {
|
|
1561
|
+
return {
|
|
1562
|
+
rule: rule.name,
|
|
1563
|
+
severity: rule.severity ?? "soft",
|
|
1564
|
+
reason: `Sampling judgment: ${parsed.reason ?? "applies_when matched"}`,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
catch (err) {
|
|
1570
|
+
// Sampling unsupported on this client, user refused, response
|
|
1571
|
+
// unparseable, or any other transport-level failure. Degrade
|
|
1572
|
+
// silently to Tier-1 only · we never block a check_action call
|
|
1573
|
+
// because Tier-2 couldn't run.
|
|
1574
|
+
log("debug", "tier2_sampling_skipped", {
|
|
1575
|
+
rule: rule.name,
|
|
1576
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1577
|
+
});
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async function toolCheckAction(args) {
|
|
1508
1582
|
const action = String(args.action ?? "").trim();
|
|
1509
1583
|
const actionType = String(args.action_type ?? "").trim();
|
|
1510
1584
|
const sessionId = typeof args.session_id === "string" ? args.session_id.trim() : "";
|
|
1585
|
+
// Tier-2 Sampling is opt-out: defaults to true on clients that support
|
|
1586
|
+
// it; gracefully degrades on clients that don't. Set to false to skip
|
|
1587
|
+
// the LLM round-trip entirely (e.g. for batched/script use).
|
|
1588
|
+
const tier2Enabled = args.use_sampling !== false;
|
|
1511
1589
|
if (!action)
|
|
1512
1590
|
throw new Error("action is required (the proposed action description)");
|
|
1513
1591
|
if (!actionType)
|
|
1514
1592
|
throw new Error("action_type is required (e.g. 'deletions', 'commits', 'file_writes', 'chat_responses')");
|
|
1515
1593
|
const { hard, soft, rules_evaluated } = checkActionAgainstRules(action, actionType);
|
|
1594
|
+
// Tier-2: run Sampling for any rule with applies_when that DIDN'T
|
|
1595
|
+
// already match deterministically. Rules already flagged in Tier-1
|
|
1596
|
+
// don't need a Sampling round-trip (we know they violate).
|
|
1597
|
+
if (tier2Enabled) {
|
|
1598
|
+
const tier1HitRules = new Set([...hard.map((v) => v.rule), ...soft.map((v) => v.rule)]);
|
|
1599
|
+
const rules = loadAllRules();
|
|
1600
|
+
const tier2Candidates = rules.filter((r) => r.applies_when &&
|
|
1601
|
+
r.applies_when.length > 0 &&
|
|
1602
|
+
!tier1HitRules.has(r.name) &&
|
|
1603
|
+
(!r.enforce_on || r.enforce_on.length === 0 || r.enforce_on.includes(actionType)));
|
|
1604
|
+
for (const rule of tier2Candidates) {
|
|
1605
|
+
const violation = await runTier2Sampling(rule, action, actionType);
|
|
1606
|
+
if (violation) {
|
|
1607
|
+
if (violation.severity === "hard")
|
|
1608
|
+
hard.push(violation);
|
|
1609
|
+
else
|
|
1610
|
+
soft.push(violation);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1516
1614
|
if (hard.length > 0) {
|
|
1517
1615
|
const result = {
|
|
1518
1616
|
approved: false,
|
|
@@ -1681,7 +1779,14 @@ function recentDenials() {
|
|
|
1681
1779
|
}));
|
|
1682
1780
|
}
|
|
1683
1781
|
function recentUnreceiptedDeletes() {
|
|
1684
|
-
|
|
1782
|
+
// v0.11.x emitted "delete_without_receipt" when an unreceipted delete
|
|
1783
|
+
// succeeded. v0.12.0 emits "delete_refused_no_receipt" when refusing.
|
|
1784
|
+
// We surface BOTH event types so an audit run against pre-v0.12 logs
|
|
1785
|
+
// still reports historical unreceipted deletes correctly.
|
|
1786
|
+
const records = [
|
|
1787
|
+
...readEventLog({ tail: AUDIT_EVENT_TAIL, action: "delete_without_receipt" }),
|
|
1788
|
+
...readEventLog({ tail: AUDIT_EVENT_TAIL, action: "delete_refused_no_receipt" }),
|
|
1789
|
+
];
|
|
1685
1790
|
return records.map((r) => ({
|
|
1686
1791
|
ts: String(r.ts),
|
|
1687
1792
|
name: String(r.name ?? ""),
|
|
@@ -2087,7 +2192,7 @@ function actionColor(action) {
|
|
|
2087
2192
|
// -------------------------------------------------------------
|
|
2088
2193
|
// Server wiring
|
|
2089
2194
|
// -------------------------------------------------------------
|
|
2090
|
-
const server = new Server({ name: "agent-memory", version: "0.
|
|
2195
|
+
const server = new Server({ name: "agent-memory", version: "0.12.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
2091
2196
|
// -------------------------------------------------------------
|
|
2092
2197
|
// Resource URI scheme
|
|
2093
2198
|
// -------------------------------------------------------------
|
|
@@ -2411,18 +2516,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2411
2516
|
{
|
|
2412
2517
|
name: "delete_memory",
|
|
2413
2518
|
description: "Move a memory to .trash/ (soft delete). The file is removed from the index but recoverable via restore_memory until you manually empty .trash/. " +
|
|
2414
|
-
"v0.
|
|
2519
|
+
"v0.12.0+ · receipt REQUIRED. Caller MUST first call check_action({action: 'delete memory <name>', action_type: 'deletions'}) to obtain a fresh Compliance Receipt, then pass it to this tool as `receipt`. " +
|
|
2415
2520
|
"Receipts must carry the caveat {type: 'action_type', value: 'deletions'} or the delete refuses. " +
|
|
2416
|
-
"
|
|
2521
|
+
"Migration from v0.11.x: previously unreceipted deletes were accepted with a warning · now they throw. Add a check_action call before each delete_memory call.",
|
|
2417
2522
|
inputSchema: {
|
|
2418
2523
|
type: "object",
|
|
2419
2524
|
properties: {
|
|
2420
2525
|
name: { type: "string", description: "The memory's name slug" },
|
|
2421
2526
|
receipt: {
|
|
2422
|
-
description: "
|
|
2527
|
+
description: "REQUIRED · Compliance Receipt (object or JSON string) from check_action with action_type=deletions. Without this, the delete is refused.",
|
|
2423
2528
|
},
|
|
2424
2529
|
},
|
|
2425
|
-
required: ["name"],
|
|
2530
|
+
required: ["name", "receipt"],
|
|
2426
2531
|
},
|
|
2427
2532
|
},
|
|
2428
2533
|
{
|
|
@@ -2611,7 +2716,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2611
2716
|
" - APPROVES: returns a short-lived Compliance Receipt (HMAC-signed, 60s default) the agent can pass to destructive tools (e.g. delete_memory) as proof of compliance.\n" +
|
|
2612
2717
|
" - DENIES: returns structured hard_violations (severity:hard rules that block) and/or soft_warnings (severity:soft rules that warn but allow).\n\n" +
|
|
2613
2718
|
"Tier 1 (deterministic) matches the action against rule.matches regexes + rule.enforce_on category filter. Works on every MCP client.\n" +
|
|
2614
|
-
"Tier 2 (
|
|
2719
|
+
"Tier 2 (v0.11.7+) calls back to the client via MCP sampling/createMessage to judge rule.applies_when natural-language conditions. Auto-enabled on clients that declared the sampling capability; silently skipped on clients that didn't.",
|
|
2615
2720
|
inputSchema: {
|
|
2616
2721
|
type: "object",
|
|
2617
2722
|
properties: {
|
|
@@ -2627,6 +2732,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2627
2732
|
type: "string",
|
|
2628
2733
|
description: "Optional session identifier · binds the issued receipt to this session via a caveat.",
|
|
2629
2734
|
},
|
|
2735
|
+
use_sampling: {
|
|
2736
|
+
type: "boolean",
|
|
2737
|
+
description: "Opt out of Tier-2 Sampling enrichment (default true). Set false for batched/scripted use where the Sampling round-trip would add latency. CLI invocations default this to false automatically.",
|
|
2738
|
+
},
|
|
2630
2739
|
},
|
|
2631
2740
|
required: ["action", "action_type"],
|
|
2632
2741
|
},
|
|
@@ -2721,7 +2830,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2721
2830
|
result = toolEmitCompanions(args);
|
|
2722
2831
|
break;
|
|
2723
2832
|
case "check_action":
|
|
2724
|
-
result = toolCheckAction(args);
|
|
2833
|
+
result = await toolCheckAction(args);
|
|
2725
2834
|
break;
|
|
2726
2835
|
case "audit":
|
|
2727
2836
|
result = toolAudit(args);
|
|
@@ -2881,7 +2990,19 @@ async function cliMain(command, rest) {
|
|
|
2881
2990
|
const name = positional[0];
|
|
2882
2991
|
if (!name)
|
|
2883
2992
|
throw new Error("Usage: agent-memory delete <name>");
|
|
2884
|
-
|
|
2993
|
+
// v0.12.0+ · delete_memory requires a Compliance Receipt. The CLI
|
|
2994
|
+
// is the trusted operator path (a human is running the command,
|
|
2995
|
+
// not an AI agent), so we auto-issue a CLI-scoped receipt rather
|
|
2996
|
+
// than make the operator chain `check-action` then paste JSON.
|
|
2997
|
+
// MCP callers (AI agents) still must go through check_action
|
|
2998
|
+
// explicitly — this short-circuit only fires from the CLI binary.
|
|
2999
|
+
const receipt = issueReceipt({
|
|
3000
|
+
caveats: [
|
|
3001
|
+
{ type: "action_type", value: "deletions" },
|
|
3002
|
+
{ type: "issued_by", value: "cli" },
|
|
3003
|
+
],
|
|
3004
|
+
});
|
|
3005
|
+
process.stdout.write(toolDeleteMemory({ name, receipt: JSON.stringify(receipt) }) + "\n");
|
|
2885
3006
|
return 0;
|
|
2886
3007
|
}
|
|
2887
3008
|
case "restore": {
|
|
@@ -3018,11 +3139,14 @@ async function cliMain(command, rest) {
|
|
|
3018
3139
|
if (!action || !actionType) {
|
|
3019
3140
|
throw new Error("Usage: agent-memory check-action '<action description>' --type <action_type> [--session <id>]");
|
|
3020
3141
|
}
|
|
3021
|
-
process.stdout.write(toolCheckAction({
|
|
3142
|
+
process.stdout.write((await toolCheckAction({
|
|
3022
3143
|
action,
|
|
3023
3144
|
action_type: actionType,
|
|
3024
3145
|
session_id: flags.session ? String(flags.session) : undefined,
|
|
3025
|
-
|
|
3146
|
+
// CLI invocations don't have a Sampling-capable client attached,
|
|
3147
|
+
// so skip Tier 2 to avoid a timeout · keeps the CLI fast.
|
|
3148
|
+
use_sampling: false,
|
|
3149
|
+
})) + "\n");
|
|
3026
3150
|
return 0;
|
|
3027
3151
|
}
|
|
3028
3152
|
case "audit": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xultrax-web/agent-memory-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"mcpName": "io.github.xultrax-web/agent-memory-mcp",
|
|
5
5
|
"description": "Codify how you work. Every AI tool obeys. Markdown rules + cross-tool companion files (AGENTS.md/CLAUDE.md/.cursor/rules/.gemini) + Compliance Receipts for protocol-level enforcement of destructive ops. Reference implementation of CRP 1.0. Works on every MCP client (no Sampling required).",
|
|
6
6
|
"type": "module",
|