memoryai-mcp 1.5.0 → 2.1.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 +432 -13
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11,6 +11,10 @@ import { z } from "zod";
|
|
|
11
11
|
const API_URL = process.env.HM_ENDPOINT || "http://localhost:8420";
|
|
12
12
|
const API_KEY = process.env.HM_API_KEY || "";
|
|
13
13
|
const REQUEST_TIMEOUT_MS = 30_000; // P2 #6: 30s default timeout for API requests
|
|
14
|
+
// Context Guard — per-IDE settings via env vars
|
|
15
|
+
const CG_CONTEXT_CAP = parseInt(process.env.HM_CONTEXT_CAP || "0", 10); // IDE's context window (0 = let server detect)
|
|
16
|
+
const CG_COMPACT_PCT = parseInt(process.env.HM_COMPACT_AT || "0", 10); // % to warn (e.g. 30 = 30%)
|
|
17
|
+
const CG_CRITICAL_PCT = parseInt(process.env.HM_CRITICAL_AT || "0", 10); // % to force compact (e.g. 50 = 50%)
|
|
14
18
|
// --- HTTP helper ---
|
|
15
19
|
async function api(method, path, body) {
|
|
16
20
|
const resp = await fetch(`${API_URL}${path}`, {
|
|
@@ -38,17 +42,19 @@ function err(e) {
|
|
|
38
42
|
// --- MCP Server ---
|
|
39
43
|
const server = new McpServer({ name: "memoryai", version: "0.9.0" }, { capabilities: { tools: {} } });
|
|
40
44
|
// 1. memory_store
|
|
41
|
-
server.tool("memory_store", "Store information in persistent memory. Use when you learn something important — project context, user preferences, architectural decisions, patterns, or
|
|
45
|
+
server.tool("memory_store", "Store information in persistent memory. Use when you learn something important — project context, user preferences, architectural decisions, patterns, bugs, pricing/cost discussions, business plans, financial calculations, credit/billing info, revenue models, partnership details, or ANY information the user might ask about later. When in doubt, STORE — MemoryAI handles dedup automatically, so storing too much is always better than forgetting.", {
|
|
42
46
|
content: z.string().describe("What to remember"),
|
|
43
47
|
source: z.string().optional().describe("Source context (e.g. file path, conversation)"),
|
|
44
48
|
tags: z.array(z.string()).optional().describe("Categories: preferences, architecture, bugs, patterns, decisions"),
|
|
45
49
|
priority: z.enum(["hot", "warm", "cold"]).optional().describe("Memory priority (default: warm)"),
|
|
46
|
-
memory_type: z.enum(["fact", "decision", "preference", "error", "goal", "episodic", "identity"]).optional().describe("Memory type. 'preference', 'decision', 'identity' are DNA-protected — never decay, 1.5x recall boost. Default: fact"),
|
|
50
|
+
memory_type: z.enum(["fact", "decision", "preference", "error", "goal", "episodic", "identity", "pitfall", "life_event", "procedure"]).optional().describe("Memory type. 'preference', 'decision', 'identity', 'procedure' are DNA-protected — never decay, 1.5x recall boost. 'pitfall' for failure memories. 'procedure' for learned workflows/steps. Default: fact"),
|
|
47
51
|
retention: z.enum(["auto", "forever", "6m", "1y"]).optional().describe("Retention policy. 'forever' = never deleted. Default: auto"),
|
|
48
52
|
content_type: z.enum(["conversation", "code", "decision", "preference", "architecture", "lesson_learned", "todo", "entity", "pattern", "environment", "bug_fix", "action_log"]).optional().describe("Content type — helps with filtering and recall accuracy"),
|
|
49
53
|
metadata: z.record(z.string(), z.unknown()).optional().describe("Additional metadata (JSONB)"),
|
|
50
54
|
zone: z.enum(["critical", "important", "standard", "ephemeral"]).optional().describe("Memory zone (default: standard). critical=never evict, ephemeral=auto-expire"),
|
|
51
55
|
importance: z.number().min(0).max(1).optional().describe("Importance score 0.0-1.0 (default: 0.5). Higher = slower decay, prioritized in recall"),
|
|
56
|
+
project_id: z.string().optional().describe("Scope memory to a project/workspace. DNA memories (preference/decision/identity/pitfall) are always cross-project visible."),
|
|
57
|
+
thread_id: z.string().optional().describe("Scope memory to a conversation thread. Memories without thread_id are visible in all threads. Use for parallel topics (e.g. 'relationship', 'career')."),
|
|
52
58
|
}, async (args) => {
|
|
53
59
|
try {
|
|
54
60
|
const r = (await api("POST", "/v1/store", {
|
|
@@ -62,6 +68,8 @@ server.tool("memory_store", "Store information in persistent memory. Use when yo
|
|
|
62
68
|
metadata: args.metadata,
|
|
63
69
|
zone: args.zone || "standard",
|
|
64
70
|
importance: args.importance ?? 0.5,
|
|
71
|
+
project_id: args.project_id,
|
|
72
|
+
thread_id: args.thread_id,
|
|
65
73
|
}));
|
|
66
74
|
let msg = `Stored (id=${r.id}, type=${args.memory_type || "fact"})`;
|
|
67
75
|
if (r.deduplicated) {
|
|
@@ -81,12 +89,14 @@ server.tool("memory_store", "Store information in persistent memory. Use when yo
|
|
|
81
89
|
// 2. memory_recall
|
|
82
90
|
server.tool("memory_recall", "Search persistent memory for relevant context. Use before starting work to check what you already know about the project or task.", {
|
|
83
91
|
query: z.string().describe("What to search for"),
|
|
84
|
-
depth: z.enum(["fast", "deep", "exhaustive"]).optional().describe("Search depth (default
|
|
92
|
+
depth: z.enum(["fast", "instant", "deep", "exhaustive"]).optional().describe("Search depth. 'instant'=vector only (~50ms), 'fast'=FTS only, 'deep'=full fusion (default), 'exhaustive'=deep+more results"),
|
|
85
93
|
limit: z.number().optional().describe("Max results (default: 5)"),
|
|
86
94
|
min_score: z.number().optional().describe("Minimum relevance score 0-1 (default: 0)"),
|
|
87
95
|
tags: z.array(z.string()).optional().describe("Filter by tags"),
|
|
88
96
|
max_tokens: z.number().optional().describe("Token budget limit — results truncated to fit within this budget"),
|
|
89
97
|
priority_min: z.enum(["critical", "important", "standard", "ephemeral"]).optional().describe("Minimum zone priority filter (default: all zones)"),
|
|
98
|
+
project_id: z.string().optional().describe("Scope recall to a project/workspace. DNA memories are always visible cross-project."),
|
|
99
|
+
thread_id: z.string().optional().describe("Scope recall to a conversation thread. Memories without thread_id are always visible."),
|
|
90
100
|
}, async (args) => {
|
|
91
101
|
try {
|
|
92
102
|
const body = {
|
|
@@ -100,6 +110,10 @@ server.tool("memory_recall", "Search persistent memory for relevant context. Use
|
|
|
100
110
|
body.max_tokens = args.max_tokens;
|
|
101
111
|
if (args.priority_min)
|
|
102
112
|
body.priority_min = args.priority_min;
|
|
113
|
+
if (args.project_id)
|
|
114
|
+
body.project_id = args.project_id;
|
|
115
|
+
if (args.thread_id)
|
|
116
|
+
body.thread_id = args.thread_id;
|
|
103
117
|
const r = (await api("POST", "/v1/recall", body));
|
|
104
118
|
if (!r.results?.length)
|
|
105
119
|
return ok("No relevant memories found.");
|
|
@@ -804,15 +818,23 @@ server.tool("session_handoff_status", "Check current session handoff status —
|
|
|
804
818
|
// context_guard_check — universal guard check with DNA count
|
|
805
819
|
server.tool("context_guard_check", "Check context window health using Context Guard v6 — dynamic thresholds, DNA memory count, bootstrap readiness. Replaces memory_health with richer data.", {
|
|
806
820
|
estimated_tokens: z.number().describe("Current token count in context window"),
|
|
807
|
-
max_tokens: z.number().optional().describe("Max context window size (
|
|
821
|
+
max_tokens: z.number().optional().describe("Max context window size (uses HM_CONTEXT_CAP env if omitted)"),
|
|
808
822
|
model: z.string().optional().describe("Model name for auto-detecting context window size (e.g. claude-sonnet-4-6)"),
|
|
809
823
|
}, async (args) => {
|
|
810
824
|
try {
|
|
811
|
-
|
|
825
|
+
// Use env var HM_CONTEXT_CAP as default if max_tokens not provided
|
|
826
|
+
const maxTokens = args.max_tokens || CG_CONTEXT_CAP || 0;
|
|
827
|
+
const payload = {
|
|
812
828
|
estimated_tokens: args.estimated_tokens,
|
|
813
|
-
max_tokens:
|
|
829
|
+
max_tokens: maxTokens,
|
|
814
830
|
model: args.model || null,
|
|
815
|
-
}
|
|
831
|
+
};
|
|
832
|
+
// Send per-IDE threshold overrides if configured via env vars
|
|
833
|
+
if (CG_COMPACT_PCT > 0)
|
|
834
|
+
payload.compact_pct = CG_COMPACT_PCT / 100;
|
|
835
|
+
if (CG_CRITICAL_PCT > 0)
|
|
836
|
+
payload.critical_pct = CG_CRITICAL_PCT / 100;
|
|
837
|
+
const r = (await api("POST", "/v1/context/guard/check", payload));
|
|
816
838
|
const pct = r.usage_percent;
|
|
817
839
|
const barLen = 20;
|
|
818
840
|
const filled = Math.round(pct / 100 * barLen);
|
|
@@ -831,8 +853,8 @@ server.tool("context_guard_check", "Check context window health using Context Gu
|
|
|
831
853
|
}
|
|
832
854
|
});
|
|
833
855
|
// context_guard_compact — compact with DNA protection
|
|
834
|
-
server.tool("context_guard_compact", "Compact session context with DNA protection — DNA memories are never overwritten.
|
|
835
|
-
content: z.string().describe("
|
|
856
|
+
server.tool("context_guard_compact", "Compact session context with DNA protection — DNA memories are never overwritten. IMPORTANT: Send a REAL summary of the conversation (>500 chars) including topics discussed, decisions made, key numbers/facts, and current status. Do NOT send just a status string like 'context guard - 132%'. If you send useless content, the server will use its internal buffer as fallback, but a good summary from you produces better memories.", {
|
|
857
|
+
content: z.string().describe("Conversation summary — include topics, decisions, key facts, numbers. Must be >500 chars of real content."),
|
|
836
858
|
task_context: z.string().optional().describe("Task description for tagging"),
|
|
837
859
|
blocking: z.boolean().optional().describe("Wait for result (true) or return task_id (false, default)"),
|
|
838
860
|
}, async (args) => {
|
|
@@ -853,23 +875,420 @@ server.tool("context_guard_compact", "Compact session context with DNA protectio
|
|
|
853
875
|
}
|
|
854
876
|
});
|
|
855
877
|
// context_guard_bootstrap — DNA-first session bootstrap
|
|
856
|
-
server.tool("context_guard_bootstrap", "Bootstrap a new session with DNA-first context — identity/preferences first, then recent activity, then task-relevant memories.", {
|
|
878
|
+
server.tool("context_guard_bootstrap", "Bootstrap a new session with DNA-first context — identity/preferences first, then recent activity, then task-relevant memories. For BOT clients: uses 3-tier wake-up (800 tokens). For IDE: flat layout (~4000 tokens).", {
|
|
857
879
|
task: z.string().describe("Task description for the new session"),
|
|
858
880
|
limit: z.number().optional().describe("Max memories to include (default: 10)"),
|
|
881
|
+
mode: z.enum(["default", "deep"]).optional().describe("'default' = 800 token 3-tier wake-up, 'deep' = full context with L2 chunks"),
|
|
882
|
+
token_budget: z.number().optional().describe("Token budget for bootstrap (default: 800 for bot, 4000 for IDE)"),
|
|
859
883
|
}, async (args) => {
|
|
860
884
|
try {
|
|
861
|
-
const r = (await api("POST", "/v1/
|
|
885
|
+
const r = (await api("POST", "/v1/bot/guard/bootstrap", {
|
|
862
886
|
task: args.task,
|
|
863
887
|
limit: args.limit || 10,
|
|
888
|
+
mode: args.mode || "default",
|
|
889
|
+
token_budget: args.token_budget,
|
|
864
890
|
}));
|
|
865
|
-
return ok(`Bootstrap complete: ${r.
|
|
866
|
-
`Tokens used: ${r.tokens_used}\n
|
|
891
|
+
return ok(`Bootstrap complete: ${r.memories_included} memories\n` +
|
|
892
|
+
`Tokens used: ${r.tokens_used}\n` +
|
|
893
|
+
`L2 sessions: ${r.l2_sessions_included || 0}\n\n` +
|
|
867
894
|
r.context_block);
|
|
868
895
|
}
|
|
869
896
|
catch (e) {
|
|
870
897
|
return err(e);
|
|
871
898
|
}
|
|
872
899
|
});
|
|
900
|
+
// bot_session_message — Rolling 3-session tracking (60 msg raw context)
|
|
901
|
+
server.tool("bot_session_message", "Track a message in the rolling session (rolling 3: keeps 60 messages raw in LLM context). Call on EVERY message (user + assistant). Returns rotate=true when session hits 20 messages. When should_compress=true, compress the oldest session via bot_session_compress.", {
|
|
902
|
+
message: z.object({
|
|
903
|
+
role: z.enum(["user", "assistant"]).describe("Message role"),
|
|
904
|
+
content: z.string().describe("Message content"),
|
|
905
|
+
}).describe("The message to track"),
|
|
906
|
+
rotation_size: z.number().optional().describe("Messages per session before rotation (default: 20, range: 5-50)"),
|
|
907
|
+
}, async (args) => {
|
|
908
|
+
try {
|
|
909
|
+
const r = (await api("POST", "/v1/bot/session/message", {
|
|
910
|
+
message: args.message,
|
|
911
|
+
rotation_size: args.rotation_size || 20,
|
|
912
|
+
}));
|
|
913
|
+
if (r.rotate) {
|
|
914
|
+
let output = `🔄 SESSION ROTATED\n` +
|
|
915
|
+
`New session: ${r.session_id} (msg ${r.message_count})\n` +
|
|
916
|
+
`Context: ${r.context_message_count} messages raw in LLM\n`;
|
|
917
|
+
if (r.should_compress) {
|
|
918
|
+
output += `\n⚠️ COMPRESS: session ${r.compress_session_id} (${r.compress_message_count} msgs)\n` +
|
|
919
|
+
`Action: Call bot_session_compress with session_id="${r.compress_session_id}"`;
|
|
920
|
+
}
|
|
921
|
+
return ok(output);
|
|
922
|
+
}
|
|
923
|
+
return ok(`Session ${r.session_id}: ${r.message_count}/20 messages | context: ${r.context_message_count} msgs`);
|
|
924
|
+
}
|
|
925
|
+
catch (e) {
|
|
926
|
+
return err(e);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
// bot_guard_check — Bot-specific guard with spawn signal
|
|
930
|
+
server.tool("bot_guard_check", "Bot context guard — checks context pressure AND returns spawn signal. When should_spawn_new_session=true, bot should spawn a new session and compress the old one later. Use this instead of context_guard_check for bot/chatbot clients.", {
|
|
931
|
+
estimated_tokens: z.number().describe("Current token count in context window"),
|
|
932
|
+
max_tokens: z.number().optional().describe("Max context window size (default: 200000)"),
|
|
933
|
+
model: z.string().optional().describe("Model name for auto-detecting context window size"),
|
|
934
|
+
compress_threshold: z.number().optional().describe("Custom spawn threshold in tokens (default: 70% of max_tokens)"),
|
|
935
|
+
}, async (args) => {
|
|
936
|
+
try {
|
|
937
|
+
const payload = {
|
|
938
|
+
estimated_tokens: args.estimated_tokens,
|
|
939
|
+
max_tokens: args.max_tokens || CG_CONTEXT_CAP || 200000,
|
|
940
|
+
model: args.model || null,
|
|
941
|
+
};
|
|
942
|
+
if (args.compress_threshold)
|
|
943
|
+
payload.compress_threshold = args.compress_threshold;
|
|
944
|
+
const r = (await api("POST", "/v1/bot/guard/check", payload));
|
|
945
|
+
const pct = r.usage_percent;
|
|
946
|
+
const barLen = 20;
|
|
947
|
+
const filled = Math.round(pct / 100 * barLen);
|
|
948
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
949
|
+
let output = `Bot Guard:\n` +
|
|
950
|
+
`[${bar}] ${pct.toFixed(1)}%\n` +
|
|
951
|
+
`Recommendation: ${r.recommendation.toUpperCase()}${r.should_compact ? " — compact now" : ""}\n` +
|
|
952
|
+
`Urgency: ${r.urgency}\n` +
|
|
953
|
+
`Spawn threshold: ${r.compress_threshold.toLocaleString()} tokens\n` +
|
|
954
|
+
`DNA memories: ${r.dna_memories} | Bootstrap ready: ${r.bootstrap_ready ? "yes" : "no"}\n`;
|
|
955
|
+
if (r.should_spawn_new_session) {
|
|
956
|
+
output += `\n⚠️ SPAWN NEW SESSION: ${r.spawn_reason}\n`;
|
|
957
|
+
output += `Action: Start new session → when new session reaches 20K tokens → compress old session via /v1/bot/session/compress`;
|
|
958
|
+
}
|
|
959
|
+
return ok(output);
|
|
960
|
+
}
|
|
961
|
+
catch (e) {
|
|
962
|
+
return err(e);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
// ── Self-Thinking Tools ──────────────────────────────────────────────
|
|
966
|
+
// brain_thoughts — Get current active thoughts
|
|
967
|
+
server.tool("brain_thoughts", "Get the brain's current active thoughts — what it's thinking about autonomously.", {
|
|
968
|
+
limit: z.number().optional().describe("Max thoughts to return (default: 10)"),
|
|
969
|
+
}, async (args) => {
|
|
970
|
+
try {
|
|
971
|
+
const r = (await api("GET", `/v1/brain/thoughts?limit=${args.limit || 10}`));
|
|
972
|
+
if (!r.thoughts || r.thoughts.length === 0)
|
|
973
|
+
return ok("Brain has no active thoughts right now.");
|
|
974
|
+
const lines = r.thoughts.map((t) => `[${t.thought_type}] ${t.content} (confidence: ${t.confidence}, urgency: ${t.urgency})`);
|
|
975
|
+
return ok(`Active thoughts (${r.count}):\n${lines.join("\n")}`);
|
|
976
|
+
}
|
|
977
|
+
catch (e) {
|
|
978
|
+
return err(e);
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
// brain_think_about — Request brain to think about a topic
|
|
982
|
+
server.tool("brain_think_about", "Request the brain to think about a specific topic. The brain will deliberate on it in its next thinking cycle.", {
|
|
983
|
+
topic: z.string().describe("What should the brain think about?"),
|
|
984
|
+
}, async (args) => {
|
|
985
|
+
try {
|
|
986
|
+
const r = (await api("POST", "/v1/brain/think-about", { topic: args.topic }));
|
|
987
|
+
return ok(`Queued for thinking: "${args.topic}"\nQueue size: ${r.queue_size}`);
|
|
988
|
+
}
|
|
989
|
+
catch (e) {
|
|
990
|
+
return err(e);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
// brain_hypotheses — Get active hypotheses
|
|
994
|
+
server.tool("brain_hypotheses", "Get hypotheses the brain is currently testing — predictions about user behavior patterns.", {
|
|
995
|
+
limit: z.number().optional().describe("Max hypotheses to return (default: 10)"),
|
|
996
|
+
}, async (args) => {
|
|
997
|
+
try {
|
|
998
|
+
const r = (await api("GET", `/v1/brain/hypotheses?limit=${args.limit || 10}`));
|
|
999
|
+
if (!r.hypotheses || r.hypotheses.length === 0)
|
|
1000
|
+
return ok("No active hypotheses being tested.");
|
|
1001
|
+
const lines = r.hypotheses.map((h) => `[${h.status}] ${h.hypothesis} (confidence: ${h.confidence})`);
|
|
1002
|
+
return ok(`Hypotheses (${r.count}):\n${lines.join("\n")}`);
|
|
1003
|
+
}
|
|
1004
|
+
catch (e) {
|
|
1005
|
+
return err(e);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
// brain_feedback — Rate a thought
|
|
1009
|
+
server.tool("brain_feedback", "Rate a thought as useful or not — helps the brain learn what's worth thinking about.", {
|
|
1010
|
+
thought_id: z.number().describe("ID of the thought to rate"),
|
|
1011
|
+
useful: z.boolean().describe("Was this thought useful?"),
|
|
1012
|
+
}, async (args) => {
|
|
1013
|
+
try {
|
|
1014
|
+
await api("POST", "/v1/brain/thoughts/feedback", {
|
|
1015
|
+
thought_id: args.thought_id,
|
|
1016
|
+
useful: args.useful,
|
|
1017
|
+
});
|
|
1018
|
+
return ok(`Feedback recorded: thought #${args.thought_id} marked as ${args.useful ? "useful" : "not useful"}`);
|
|
1019
|
+
}
|
|
1020
|
+
catch (e) {
|
|
1021
|
+
return err(e);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
// brain_thinking_stats — Budget and efficiency
|
|
1025
|
+
server.tool("brain_thinking_stats", "Get thinking system statistics — token budget, efficiency, queue size, and meta-cognition report.", {}, async () => {
|
|
1026
|
+
try {
|
|
1027
|
+
const r = (await api("GET", "/v1/brain/thinking-stats"));
|
|
1028
|
+
return ok(`Budget: ${r.budget.remaining_tokens} tokens remaining (limit: ${r.budget.limit_per_hour}/hr)\n` +
|
|
1029
|
+
`Efficiency: ${(r.budget.efficiency * 100).toFixed(1)}%\n` +
|
|
1030
|
+
`Queue size: ${r.queue_size}\n` +
|
|
1031
|
+
`Total thoughts: ${r.meta.total_thoughts} (${r.meta.useful_thoughts} useful)\n` +
|
|
1032
|
+
`Interval: ${r.meta.recommended_interval_seconds}s\n` +
|
|
1033
|
+
`Best types: ${r.meta.best_types.join(", ") || "none yet"}\n` +
|
|
1034
|
+
`Suppressed: ${r.meta.suppressed_types.join(", ") || "none"}`);
|
|
1035
|
+
}
|
|
1036
|
+
catch (e) {
|
|
1037
|
+
return err(e);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
// ── Session Settings Tools ──────────────────────────────────────────
|
|
1041
|
+
// memory_auto_extract — LLM-based fact extraction from conversation
|
|
1042
|
+
server.tool("memory_auto_extract", "CRITICAL: Call this at the END of every conversation session to extract and store important facts automatically. Uses LLM analysis to identify pricing, decisions, plans, technical details, and anything worth remembering. This is MORE reliable than manual memory_store because it catches things you might forget to store. ALWAYS call this before the conversation ends — especially after discussions about money, pricing, plans, decisions, or business.", {
|
|
1043
|
+
conversation: z.string().describe("The conversation text to extract facts from (include both user and assistant messages)"),
|
|
1044
|
+
source: z.string().optional().describe("Source context (e.g. 'discord chat', 'slack thread')"),
|
|
1045
|
+
store: z.boolean().optional().describe("Whether to store extracted facts (default: true). Set false to preview what would be extracted."),
|
|
1046
|
+
}, async (args) => {
|
|
1047
|
+
try {
|
|
1048
|
+
const r = (await api("POST", "/v1/memory/auto-extract", {
|
|
1049
|
+
conversation: args.conversation,
|
|
1050
|
+
source: args.source || "auto-extract",
|
|
1051
|
+
store: args.store !== false,
|
|
1052
|
+
}));
|
|
1053
|
+
if (!r.facts?.length)
|
|
1054
|
+
return ok("No extractable facts found in conversation.");
|
|
1055
|
+
const factList = r.facts
|
|
1056
|
+
.map((f, i) => `${i + 1}. [${f.memory_type || 'fact'}] ${f.content}`)
|
|
1057
|
+
.join("\n");
|
|
1058
|
+
return ok(`Extracted ${r.facts.length} facts (added: ${r.added}, updated: ${r.updated}, skipped: ${r.skipped}):\n\n${factList}`);
|
|
1059
|
+
}
|
|
1060
|
+
catch (e) {
|
|
1061
|
+
return err(e);
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
// ── IDE Upgrade Tools ──────────────────────────────────────────────
|
|
1065
|
+
// memory_pitfall_check — Check pitfalls before risky actions
|
|
1066
|
+
server.tool("memory_pitfall_check", "IMPORTANT: Call this BEFORE executing risky actions (deploy, rm, git push, database changes). Returns known pitfalls (past failures + lessons) so you can avoid repeating mistakes. Pitfalls are DNA-protected and never expire.", {
|
|
1067
|
+
intent: z.string().describe("What you're about to do (e.g. 'deploy to production', 'delete user table')"),
|
|
1068
|
+
tags: z.array(z.string()).optional().describe("Filter by tags"),
|
|
1069
|
+
limit: z.number().optional().describe("Max results (default 5)"),
|
|
1070
|
+
}, async (args) => {
|
|
1071
|
+
try {
|
|
1072
|
+
const r = (await api("POST", "/v1/bot/pitfall/check", {
|
|
1073
|
+
intent: args.intent,
|
|
1074
|
+
tags: args.tags,
|
|
1075
|
+
limit: args.limit || 5,
|
|
1076
|
+
}));
|
|
1077
|
+
if (!r.has_pitfalls)
|
|
1078
|
+
return ok("No known pitfalls for this action. Proceed safely.");
|
|
1079
|
+
const list = r.pitfalls
|
|
1080
|
+
.map((p, i) => `${i + 1}. [score: ${p.score}] ${p.content}`)
|
|
1081
|
+
.join("\n");
|
|
1082
|
+
return ok(`⚠️ ${r.pitfalls.length} pitfall(s) found:\n\n${list}\n\nReview before proceeding.`);
|
|
1083
|
+
}
|
|
1084
|
+
catch (e) {
|
|
1085
|
+
return err(e);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
// memory_plan_save — Save current plan/state for session resumption
|
|
1089
|
+
server.tool("memory_plan_save", "Save your current work state (plan steps, cursor position, active goals) so you can resume exactly where you left off in the next session. Call before session ends or when switching tasks.", {
|
|
1090
|
+
session_id: z.string().optional().describe("Session identifier (default: 'default')"),
|
|
1091
|
+
state: z.record(z.string(), z.unknown()).describe("State to save: {plan: [...], cursor: 3, active_goal: '...', last_action: '...', files_read: [...]}"),
|
|
1092
|
+
}, async (args) => {
|
|
1093
|
+
try {
|
|
1094
|
+
const r = (await api("POST", "/v1/bot/state/save", {
|
|
1095
|
+
session_id: args.session_id || "default",
|
|
1096
|
+
state: args.state,
|
|
1097
|
+
}));
|
|
1098
|
+
return ok(`State saved for session '${r.session_id}'. Will be restored on next bootstrap.`);
|
|
1099
|
+
}
|
|
1100
|
+
catch (e) {
|
|
1101
|
+
return err(e);
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
// memory_plan_resume — Restore saved state from previous session
|
|
1105
|
+
server.tool("memory_plan_resume", "Restore your work state from a previous session. Returns plan steps, cursor position, active goals — everything needed to continue where you left off.", {
|
|
1106
|
+
session_id: z.string().optional().describe("Session identifier (default: 'default')"),
|
|
1107
|
+
}, async (args) => {
|
|
1108
|
+
try {
|
|
1109
|
+
const r = (await api("GET", `/v1/bot/state/restore?session_id=${args.session_id || "default"}`));
|
|
1110
|
+
if (r.status === "not_found")
|
|
1111
|
+
return ok("No saved state found for this session. Starting fresh.");
|
|
1112
|
+
return ok(`State restored (saved at ${r.saved_at}):\n\n${JSON.stringify(r.state, null, 2)}`);
|
|
1113
|
+
}
|
|
1114
|
+
catch (e) {
|
|
1115
|
+
return err(e);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
// memory_goal_track — Create/update/query goals
|
|
1119
|
+
server.tool("memory_goal_track", "Track goals across sessions. Create new goals, update progress, or query active goals. Goals with status='active' are DNA-protected (never decay).", {
|
|
1120
|
+
action: z.enum(["create", "update", "list"]).describe("Action to perform"),
|
|
1121
|
+
title: z.string().optional().describe("Goal title (for create)"),
|
|
1122
|
+
progress: z.number().optional().describe("Progress 0.0-1.0 (for update)"),
|
|
1123
|
+
goal_id: z.number().optional().describe("Goal ID (for update)"),
|
|
1124
|
+
status: z.enum(["active", "achieved", "abandoned"]).optional().describe("New status (for update)"),
|
|
1125
|
+
}, async (args) => {
|
|
1126
|
+
try {
|
|
1127
|
+
if (args.action === "create") {
|
|
1128
|
+
const r = (await api("POST", "/v1/store", {
|
|
1129
|
+
content: args.title,
|
|
1130
|
+
memory_type: "goal",
|
|
1131
|
+
zone: "important",
|
|
1132
|
+
tags: ["goal", "active"],
|
|
1133
|
+
}));
|
|
1134
|
+
return ok(`Goal created: "${args.title}" (id: ${r.id}). DNA-protected while active.`);
|
|
1135
|
+
}
|
|
1136
|
+
else if (args.action === "list") {
|
|
1137
|
+
const r = (await api("POST", "/v1/recall", {
|
|
1138
|
+
query: "active goals and objectives",
|
|
1139
|
+
memory_type: "goal",
|
|
1140
|
+
depth: "deep",
|
|
1141
|
+
limit: 10,
|
|
1142
|
+
}));
|
|
1143
|
+
if (!r.results?.length)
|
|
1144
|
+
return ok("No active goals found.");
|
|
1145
|
+
const list = r.results.map((g, i) => `${i + 1}. ${g.content}`).join("\n");
|
|
1146
|
+
return ok(`Active goals:\n\n${list}`);
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
return ok("Goal update: use memory_store with memory_type='goal' to update goal content.");
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
catch (e) {
|
|
1153
|
+
return err(e);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
// memory_thought_log — Query what the brain has been thinking about
|
|
1157
|
+
server.tool("memory_thought_log", "See what the brain has been thinking about autonomously. Returns recent thoughts, hypotheses, and insights generated during idle time.", {
|
|
1158
|
+
limit: z.number().optional().describe("Max thoughts to return (default 5)"),
|
|
1159
|
+
}, async (args) => {
|
|
1160
|
+
try {
|
|
1161
|
+
const r = (await api("GET", `/v1/brain/thoughts?limit=${args.limit || 5}`));
|
|
1162
|
+
if (!r.thoughts?.length)
|
|
1163
|
+
return ok("No recent thoughts. The brain thinks during idle periods.");
|
|
1164
|
+
const list = r.thoughts
|
|
1165
|
+
.map((t, i) => `${i + 1}. [${t.thought_type}] ${t.content} (urgency: ${t.urgency})`)
|
|
1166
|
+
.join("\n");
|
|
1167
|
+
return ok(`Recent brain thoughts:\n\n${list}`);
|
|
1168
|
+
}
|
|
1169
|
+
catch (e) {
|
|
1170
|
+
return err(e);
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
// memory_feedback — Report recall quality for self-improvement
|
|
1174
|
+
server.tool("memory_feedback", "Report whether recall results were helpful. This feeds the neuroplasticity system — over time, the brain learns what works for YOUR specific patterns and improves recall quality.", {
|
|
1175
|
+
query: z.string().describe("The recall query that was made"),
|
|
1176
|
+
chunk_ids: z.array(z.number()).describe("IDs of chunks that were returned"),
|
|
1177
|
+
helpful: z.boolean().describe("Were the results helpful for your task?"),
|
|
1178
|
+
action_succeeded: z.boolean().optional().describe("Did the action using these memories succeed? (default: true)"),
|
|
1179
|
+
}, async (args) => {
|
|
1180
|
+
try {
|
|
1181
|
+
const r = (await api("POST", "/v1/bot/feedback", {
|
|
1182
|
+
query: args.query,
|
|
1183
|
+
chunk_ids: args.chunk_ids,
|
|
1184
|
+
helpful: args.helpful,
|
|
1185
|
+
action_succeeded: args.action_succeeded !== false,
|
|
1186
|
+
}));
|
|
1187
|
+
return ok(r.message || "Feedback recorded. Brain will adapt over time.");
|
|
1188
|
+
}
|
|
1189
|
+
catch (e) {
|
|
1190
|
+
return err(e);
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
// memory_predict — Predictive recall (push intent, get predicted memories)
|
|
1194
|
+
server.tool("memory_predict", "Predictive recall — tell the brain what you're about to do and get relevant memories pre-loaded. Call this when you can anticipate what context will be needed next.", {
|
|
1195
|
+
intent: z.string().describe("What you/user are about to do"),
|
|
1196
|
+
context: z.string().optional().describe("Current conversation context (helps prediction accuracy)"),
|
|
1197
|
+
limit: z.number().optional().describe("Max predictions (default 5)"),
|
|
1198
|
+
}, async (args) => {
|
|
1199
|
+
try {
|
|
1200
|
+
const r = (await api("POST", "/v1/bot/predict", {
|
|
1201
|
+
intent: args.intent,
|
|
1202
|
+
context: args.context || "",
|
|
1203
|
+
limit: args.limit || 5,
|
|
1204
|
+
}));
|
|
1205
|
+
if (!r.predictions?.length)
|
|
1206
|
+
return ok("No relevant predictions for this intent.");
|
|
1207
|
+
const list = r.predictions
|
|
1208
|
+
.map((p, i) => `${i + 1}. [${p.memory_type || 'memory'}] ${p.content}\n (score: ${p.score}, reason: ${p.reason})`)
|
|
1209
|
+
.join("\n\n");
|
|
1210
|
+
return ok(`Predicted ${r.count} relevant memories:\n\n${list}`);
|
|
1211
|
+
}
|
|
1212
|
+
catch (e) {
|
|
1213
|
+
return err(e);
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
// memory_changelog — What changed since last session
|
|
1217
|
+
server.tool("memory_changelog", "See what changed in your memory since your last session. Shows new memories, updates, invalidations, and insights from overnight consolidation. Call at session start after bootstrap to understand what the brain learned while you were away.", {
|
|
1218
|
+
since: z.string().describe("ISO datetime — show changes after this time (e.g. '2026-05-20T10:00:00Z')"),
|
|
1219
|
+
project_id: z.string().optional().describe("Filter to specific project"),
|
|
1220
|
+
limit: z.number().optional().describe("Max changes to return (default 50)"),
|
|
1221
|
+
}, async (args) => {
|
|
1222
|
+
try {
|
|
1223
|
+
const r = (await api("POST", "/v1/memory/changelog", {
|
|
1224
|
+
since: args.since,
|
|
1225
|
+
project_id: args.project_id,
|
|
1226
|
+
limit: args.limit || 50,
|
|
1227
|
+
}));
|
|
1228
|
+
if (!r.changes?.length)
|
|
1229
|
+
return ok("No changes since last session. Memory is up to date.");
|
|
1230
|
+
const list = r.changes
|
|
1231
|
+
.map((c, i) => `${i + 1}. [${c.type}] ${c.content}${c.source ? ` (source: ${c.source})` : ""}`)
|
|
1232
|
+
.join("\n");
|
|
1233
|
+
return ok(`${r.count} changes since ${args.since}:\n\n${list}`);
|
|
1234
|
+
}
|
|
1235
|
+
catch (e) {
|
|
1236
|
+
return err(e);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
// memory_cognitive_profile — Complete self-model (metacognition)
|
|
1240
|
+
server.tool("memory_cognitive_profile", "Get the brain's complete self-model: who the user is, their mood, active goals, top entities (people/places), learned procedures, and recent topics. Use for complete context awareness. No LLM cost — pure aggregation (~50ms).", {}, async () => {
|
|
1241
|
+
try {
|
|
1242
|
+
const r = (await api("GET", "/v1/personality/cognitive-profile"));
|
|
1243
|
+
let out = `## Cognitive Profile\n\n`;
|
|
1244
|
+
if (r.persona)
|
|
1245
|
+
out += `**Persona:** ${r.persona}\n\n`;
|
|
1246
|
+
if (r.mood)
|
|
1247
|
+
out += `**Mood:** ${r.mood.current} (trend: ${r.mood.trend})\n\n`;
|
|
1248
|
+
if (r.active_goals?.length)
|
|
1249
|
+
out += `**Active Goals:**\n${r.active_goals.map((g) => `- ${g}`).join("\n")}\n\n`;
|
|
1250
|
+
if (r.top_entities?.length)
|
|
1251
|
+
out += `**Top Entities:** ${r.top_entities.map((e) => e.name || e).join(", ")}\n\n`;
|
|
1252
|
+
if (r.procedures?.length)
|
|
1253
|
+
out += `**Procedures:**\n${r.procedures.map((p) => `- ${p.slice(0, 100)}`).join("\n")}\n\n`;
|
|
1254
|
+
if (r.recent_topics?.length)
|
|
1255
|
+
out += `**Recent Topics:**\n${r.recent_topics.map((t) => `- ${t}`).join("\n")}\n`;
|
|
1256
|
+
return ok(out.trim());
|
|
1257
|
+
}
|
|
1258
|
+
catch (e) {
|
|
1259
|
+
return err(e);
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
// memory_entity_profile — Get everything known about an entity
|
|
1263
|
+
server.tool("memory_entity_profile", "Get complete profile for a specific entity (person, place, concept). Returns: frequency stats, linked memories, and relationships. Use when you need context about a specific person or topic the user has discussed.", {
|
|
1264
|
+
name: z.string().describe("Entity name to look up (e.g. 'Sarah', 'React', 'AuthService')"),
|
|
1265
|
+
}, async (args) => {
|
|
1266
|
+
try {
|
|
1267
|
+
const r = (await api("GET", `/v1/entities/${encodeURIComponent(args.name)}/profile`));
|
|
1268
|
+
if (!r.stats && !r.memories?.length)
|
|
1269
|
+
return ok(`No information found about "${args.name}".`);
|
|
1270
|
+
let out = `## Entity: ${args.name}\n\n`;
|
|
1271
|
+
if (r.stats) {
|
|
1272
|
+
out += `**Stats:** mentioned ${r.stats.frequency}x, recalled ${r.stats.recall_count}x`;
|
|
1273
|
+
if (r.stats.first_seen)
|
|
1274
|
+
out += `, first seen ${r.stats.first_seen.slice(0, 10)}`;
|
|
1275
|
+
out += `\n\n`;
|
|
1276
|
+
}
|
|
1277
|
+
if (r.memories?.length) {
|
|
1278
|
+
out += `**Linked Memories (${r.memory_count}):**\n`;
|
|
1279
|
+
out += r.memories.map((m) => `- [${m.memory_type}] ${m.content}`).join("\n");
|
|
1280
|
+
out += `\n\n`;
|
|
1281
|
+
}
|
|
1282
|
+
if (r.relationships?.length) {
|
|
1283
|
+
out += `**Relationships (${r.relationship_count}):**\n`;
|
|
1284
|
+
out += r.relationships.map((rel) => `- ${rel.source} → ${rel.relationship} → ${rel.target}`).join("\n");
|
|
1285
|
+
}
|
|
1286
|
+
return ok(out.trim());
|
|
1287
|
+
}
|
|
1288
|
+
catch (e) {
|
|
1289
|
+
return err(e);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
873
1292
|
async function main() {
|
|
874
1293
|
const transport = new StdioServerTransport();
|
|
875
1294
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoryai-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for MemoryAI
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "MCP server for MemoryAI — Long-term memory for AI agents. Works with Claude Code, Cursor, Windsurf, VS Code, Kiro.",
|
|
5
5
|
"homepage": "https://memoryai.dev",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|