@echomem/mcp 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,13 +4,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import axios from "axios";
6
6
  import { ZodError } from "zod";
7
- import { canonicalToolNames, deleteMemorySchema, keywordsSchema, listToolSpecs, othersSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, timeRangeSchema, } from "./v1-contract.js";
7
+ import { canonicalToolNames, deleteMemorySchema, getByContextSchema, keywordsSchema, listToolSpecs, othersSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, timeRangeSchema, } from "./v1-contract.js";
8
8
  import { KeyStore } from "./keystore.js";
9
9
  import { EventLogger, hashText } from "./events.js";
10
10
  import { buildReportText } from "./report.js";
11
+ import { contextHealthMarkdown, recomposeCapsuleMarkdown } from "./hud/api.js";
11
12
  import { createHash, randomUUID } from "node:crypto";
12
13
  import { fetchEncryptionConfig, decryptMemoryFields } from "./encryption.js";
13
14
  import { runCli } from "./setup.js";
15
+ import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS } from "./package-metadata.js";
14
16
  const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
15
17
  const MEMORY_FEED_API_URL = process.env.MEMORY_FEED_API_URL || "https://memory-feed.vercel.app";
16
18
  /** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
@@ -690,6 +692,7 @@ class EchoMemApiClient {
690
692
  title: parsed.title,
691
693
  // Stable per-session id so multiple saves in this coding session group under one context.
692
694
  conversationKey: this.sessionId,
695
+ passthrough: parsed.passthrough || false,
693
696
  triggerMessage: parsed.triggerMessage ||
694
697
  lastUserMessageFromMessages(parsed.messages) ||
695
698
  lastUserMessageFromConversationText(parsed.conversation),
@@ -751,6 +754,15 @@ class EchoMemApiClient {
751
754
  });
752
755
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
753
756
  }
757
+ async getMemoriesByContext(args) {
758
+ const parsed = getByContextSchema.parse(args);
759
+ const enc = await this.encState();
760
+ const response = await this.axios.post("/api/extension/memories/by-context", {
761
+ contextId: parsed.contextId,
762
+ limit: parsed.limit,
763
+ });
764
+ return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
765
+ }
754
766
  async searchMemoriesByKeywords(args) {
755
767
  const parsed = keywordsSchema.parse(args);
756
768
  const enc = await this.encState();
@@ -778,7 +790,7 @@ class EchoMemApiClient {
778
790
  }
779
791
  }
780
792
  }
781
- const SERVER_VERSION = "1.1.0";
793
+ const SERVER_VERSION = MCP_PACKAGE_VERSION;
782
794
  class EchoMemMCPServer {
783
795
  server;
784
796
  client;
@@ -793,6 +805,7 @@ class EchoMemMCPServer {
793
805
  name: "echomem-mcp",
794
806
  version: SERVER_VERSION,
795
807
  }, {
808
+ instructions: MCP_SERVER_INSTRUCTIONS,
796
809
  capabilities: {
797
810
  tools: {},
798
811
  },
@@ -870,6 +883,45 @@ class EchoMemMCPServer {
870
883
  if (canonicalName === canonicalToolNames.report) {
871
884
  return { content: [{ type: "text", text: await buildReportText(false) }] };
872
885
  }
886
+ if (canonicalName === canonicalToolNames.contextHealth) {
887
+ const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
888
+ ? request.params.arguments.client
889
+ : "auto";
890
+ const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
891
+ ? client
892
+ : "auto";
893
+ return { content: [{ type: "text", text: await contextHealthMarkdown(mode) }] };
894
+ }
895
+ if (canonicalName === canonicalToolNames.recompose) {
896
+ const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
897
+ ? request.params.arguments.client
898
+ : "auto";
899
+ const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
900
+ ? client
901
+ : "auto";
902
+ const capsuleText = await recomposeCapsuleMarkdown(mode);
903
+ // If logged in, persist the capsule via passthrough so it's retrievable by contextId.
904
+ if (this.client.hasToken()) {
905
+ try {
906
+ const saveResult = await this.client.saveConversation({
907
+ conversation: capsuleText,
908
+ title: "Session capsule (recompose)",
909
+ source: "mcp_recompose",
910
+ passthrough: true,
911
+ });
912
+ const ctxId = saveResult?.contextId;
913
+ const capId = saveResult?.capsuleId;
914
+ const persistLine = ctxId
915
+ ? `\n\n---\nCapsule persisted${capId ? ` (${capId})` : ""}. To reload in a fresh session:\nget_memories_by_context({ contextId: "${ctxId}" })`
916
+ : "";
917
+ return { content: [{ type: "text", text: capsuleText + persistLine }] };
918
+ }
919
+ catch {
920
+ // Persistence is best-effort; return the capsule text regardless.
921
+ }
922
+ }
923
+ return { content: [{ type: "text", text: capsuleText }] };
924
+ }
873
925
  if (!this.client.hasToken())
874
926
  throw new NoTokenError();
875
927
  if (canonicalName !== canonicalToolNames.save) {
@@ -886,6 +938,8 @@ class EchoMemMCPServer {
886
938
  return await this.handleSave(request.params.arguments, rec);
887
939
  case canonicalToolNames.timeRange:
888
940
  return await this.handleTimeRange(request.params.arguments);
941
+ case canonicalToolNames.getByContext:
942
+ return await this.handleGetByContext(request.params.arguments);
889
943
  case canonicalToolNames.keywords:
890
944
  return await this.handleKeywords(request.params.arguments);
891
945
  case canonicalToolNames.others:
@@ -940,6 +994,8 @@ class EchoMemMCPServer {
940
994
  rec.latency_ms = Date.now() - t0;
941
995
  this.events.record(rec);
942
996
  if (canonicalName !== canonicalToolNames.report &&
997
+ canonicalName !== canonicalToolNames.contextHealth &&
998
+ canonicalName !== canonicalToolNames.recompose &&
943
999
  canonicalName !== canonicalToolNames.save &&
944
1000
  this.client.hasToken()) {
945
1001
  const finalAnalytics = {
@@ -1061,14 +1117,46 @@ Details: ${m.details || "N/A"}`)
1061
1117
  rec.conversation_chars = text.length;
1062
1118
  rec.save_source = typeof a?.source === "string" ? a.source : sourceFallback;
1063
1119
  }
1064
- const { success, memoriesExtracted, error } = await this.client.saveConversation(enrichedArgs);
1120
+ const { success, memoriesExtracted, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
1065
1121
  if (!success)
1066
1122
  throw new Error(`EchoMem API Error: ${error}`);
1067
1123
  if (rec)
1068
1124
  rec.memories_extracted = typeof memoriesExtracted === "number" ? memoriesExtracted : undefined;
1069
- return {
1070
- content: [{ type: "text", text: `Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.` }],
1071
- };
1125
+ // Passthrough saves store the capsule verbatim — no extraction, no embeddings.
1126
+ if (isPassthrough) {
1127
+ const text = [
1128
+ `Session capsule saved (passthrough, no extraction).`,
1129
+ typeof capsuleId === "string" && capsuleId ? `Capsule ID: ${capsuleId}` : "",
1130
+ typeof contextId === "string" && contextId ? `Context: ${contextId}` : "",
1131
+ "",
1132
+ `To reload this capsule in a fresh session: get_memories_by_context({ contextId: "${contextId}" })`,
1133
+ ].filter(Boolean).join("\n");
1134
+ return { content: [{ type: "text", text }] };
1135
+ }
1136
+ // Surface WHAT was captured (not just the count) so the agent can verify the key facts survived
1137
+ // extraction, and so it holds the ids to deterministically re-fetch this batch later (warm-up).
1138
+ const saved = Array.isArray(extractedMemories) ? extractedMemories.filter(isRecord) : [];
1139
+ const list = saved
1140
+ .map((m, idx) => {
1141
+ const keys = readString(m, "keys") ?? "(no key)";
1142
+ const description = readString(m, "description") ?? "";
1143
+ const details = readString(m, "details") ?? "";
1144
+ const id = readString(m, "id");
1145
+ return [
1146
+ `[${idx + 1}] ${keys}${id ? ` · id ${id}` : ""}`,
1147
+ description ? `Description: ${description}` : "",
1148
+ details ? `Details: ${details}` : "",
1149
+ ].filter(Boolean).join("\n");
1150
+ })
1151
+ .join("\n\n");
1152
+ const text = [
1153
+ `Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
1154
+ list,
1155
+ saved.length
1156
+ ? `Verify these captured the key facts. To re-fetch this exact batch later, search by the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
1157
+ : "",
1158
+ ].filter(Boolean).join("\n\n");
1159
+ return { content: [{ type: "text", text }] };
1072
1160
  }
1073
1161
  async handleTimeRange(args) {
1074
1162
  const parsed = timeRangeSchema.parse(args);
@@ -1095,6 +1183,31 @@ Details: ${m.details || "N/A"}`)
1095
1183
  ],
1096
1184
  };
1097
1185
  }
1186
+ async handleGetByContext(args) {
1187
+ const parsed = getByContextSchema.parse(args);
1188
+ const { success, memories, error } = await this.client.getMemoriesByContext(args);
1189
+ if (!success)
1190
+ throw new Error(`EchoMem API Error: ${error}`);
1191
+ if (!memories?.length) {
1192
+ return {
1193
+ content: [{ type: "text", text: `No memories found for context ${parsed.contextId}.` }],
1194
+ };
1195
+ }
1196
+ const formattedResults = memories
1197
+ .map((m, idx) => `[${idx + 1}] ${m.keys || "Saved memory"}${m.id ? ` · id ${m.id}` : ""}
1198
+ Time: ${m.time} | Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
1199
+ Description: ${m.description}
1200
+ Details: ${m.details || "N/A"}`)
1201
+ .join("\n\n");
1202
+ return {
1203
+ content: [
1204
+ {
1205
+ type: "text",
1206
+ text: `Recalled ${memories.length} memories from context ${parsed.contextId} (deterministic full batch):\n\n${formattedResults}`,
1207
+ },
1208
+ ],
1209
+ };
1210
+ }
1098
1211
  async handleKeywords(args) {
1099
1212
  const parsed = keywordsSchema.parse(args);
1100
1213
  const { success, memories, error } = await this.client.searchMemoriesByKeywords(args);
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+ const FALLBACK_PACKAGE = {
3
+ name: "@echomem/mcp",
4
+ version: "0.0.0",
5
+ description: "EchoMem Cloud-First MCP Server",
6
+ };
7
+ function loadPackageJson() {
8
+ try {
9
+ const packageUrl = new URL("../package.json", import.meta.url);
10
+ return JSON.parse(fs.readFileSync(packageUrl, "utf8"));
11
+ }
12
+ catch {
13
+ return {};
14
+ }
15
+ }
16
+ const packageJson = loadPackageJson();
17
+ function stringOrFallback(value, fallback) {
18
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
19
+ }
20
+ export const MCP_PACKAGE_NAME = stringOrFallback(packageJson.name, FALLBACK_PACKAGE.name);
21
+ export const MCP_PACKAGE_VERSION = stringOrFallback(packageJson.version, FALLBACK_PACKAGE.version);
22
+ export const MCP_PACKAGE_DESCRIPTION = stringOrFallback(packageJson.description, FALLBACK_PACKAGE.description);
23
+ export const MCP_PACKAGE_LABEL = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
24
+ export const MCP_UPDATE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest update`;
25
+ export const MCP_SERVER_INSTRUCTIONS = [
26
+ `${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
27
+ `If the user or local config expects a newer EchoMem MCP version than ${MCP_PACKAGE_VERSION}, update once with \`${MCP_UPDATE_COMMAND}\` and start a new MCP session.`,
28
+ "Do not auto-update on every MCP startup; this bridge is intentionally stable between explicit updates.",
29
+ ].join(" ");
30
+ export function withMcpVersion(description) {
31
+ return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, update once with \`${MCP_UPDATE_COMMAND}\` (add \`--client cursor|windsurf|claude-desktop|codex\` when needed), then start a new MCP session. Do not run updates repeatedly or on every startup.`;
32
+ }
package/dist/report.js CHANGED
@@ -623,7 +623,7 @@ function renderText(a, memCount, useColor) {
623
623
  L(c.green(`✓ You're connected — every new session now builds on your ${memCount} memories.`));
624
624
  }
625
625
  else {
626
- L(c.green("→ Start now (no signup wall): ") + c.bold("npx -y @echomem/mcp setup"));
626
+ L(c.green("→ Start now (no signup wall): ") + c.bold("npm i -g @echomem/mcp@latest && echomem-mcp setup"));
627
627
  L(c.dim(` ~1 minute. Your next coding session recalls instead of re-reading.`));
628
628
  }
629
629
  NL();