@echomem/mcp 1.4.29 → 1.4.30

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,7 +4,7 @@ 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, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } from "./v1-contract.js";
7
+ import { canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, recordMemoryCitationsSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } 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";
@@ -513,6 +513,16 @@ function inputAnalyticsForTool(canonicalName, args) {
513
513
  memory_id_hash: memoryId ? hashText(memoryId) : undefined,
514
514
  };
515
515
  }
516
+ case canonicalToolNames.recordCitations: {
517
+ const memoryIds = Array.isArray(a.memoryIds)
518
+ ? a.memoryIds.filter((id) => typeof id === "string")
519
+ : [];
520
+ const receiptId = readString(a, "receiptId");
521
+ return {
522
+ memory_count: memoryIds.length,
523
+ receipt_id_hash: receiptId ? hashText(receiptId) : undefined,
524
+ };
525
+ }
516
526
  case canonicalToolNames.flagPublicationAttention: {
517
527
  const memoryIds = Array.isArray(a.memoryIds) ? a.memoryIds.filter((id) => typeof id === "string") : [];
518
528
  return {
@@ -610,6 +620,21 @@ class EchoMemApiClient {
610
620
  return undefined;
611
621
  }
612
622
  }
623
+ /**
624
+ * Compact "who covers what" guide for the user's company group, used to decorate the group tool
625
+ * descriptions. Group publication snapshots are plaintext by design, so this works even when the
626
+ * local vault is locked — unlike `fetchMemoryMap`, it never needs a decryption key.
627
+ */
628
+ async fetchGroupMemoryMap() {
629
+ try {
630
+ const response = await this.axios.get("/api/extension/social/groups/current", { timeout: 6000 });
631
+ const map = typeof response.data?.map === "string" ? response.data.map.trim() : "";
632
+ return map || undefined;
633
+ }
634
+ catch {
635
+ return undefined;
636
+ }
637
+ }
613
638
  /** Encryption config for the account, fetched once and cached. Failures are not cached. */
614
639
  async getEncryptionConfig() {
615
640
  if (!this.encConfigPromise) {
@@ -983,6 +1008,16 @@ class EchoMemApiClient {
983
1008
  throw new Error(`get_public_memory failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
984
1009
  }
985
1010
  }
1011
+ async recordMemoryCitations(args) {
1012
+ const parsed = recordMemoryCitationsSchema.parse(args ?? {});
1013
+ try {
1014
+ const response = await this.axios.post("/api/extension/social/memory-citations", { ...parsed, sessionKey: this.sessionId });
1015
+ return response.data;
1016
+ }
1017
+ catch (error) {
1018
+ throw new Error(`record_memory_citations failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
1019
+ }
1020
+ }
986
1021
  async getGroupContext(args) {
987
1022
  groupContextSchema.parse(args ?? {});
988
1023
  try {
@@ -1076,15 +1111,28 @@ class EchoMemApiClient {
1076
1111
  }
1077
1112
  }
1078
1113
  const SERVER_VERSION = MCP_PACKAGE_VERSION;
1114
+ /** Tool-description decoration must never delay the MCP handshake; give up and decorate next listing. */
1115
+ const MAP_WAIT_MS = 2500;
1116
+ function capWait(pending) {
1117
+ if (!pending)
1118
+ return Promise.resolve(undefined);
1119
+ return Promise.race([
1120
+ pending,
1121
+ new Promise((resolve) => setTimeout(() => resolve(undefined), MAP_WAIT_MS)),
1122
+ ]);
1123
+ }
1079
1124
  class EchoMemMCPServer {
1080
1125
  server;
1081
1126
  client;
1082
1127
  mapCache = null;
1128
+ groupMapCache = null;
1083
1129
  events;
1084
1130
  mcpClientName;
1085
1131
  mcpClientVersion;
1086
1132
  /** Whether the most recent ListTools response carried the memory map (per-session recall signal). */
1087
1133
  mapInjected = false;
1134
+ /** Whether the most recent ListTools response carried the group memory map. */
1135
+ groupMapInjected = false;
1088
1136
  updateStatus;
1089
1137
  constructor(store) {
1090
1138
  this.server = new Server({
@@ -1133,15 +1181,23 @@ class EchoMemMCPServer {
1133
1181
  this.mapCache.then((m) => { if (!m)
1134
1182
  this.mapCache = null; }).catch(() => { this.mapCache = null; });
1135
1183
  }
1184
+ if (this.client.hasToken() && !this.groupMapCache) {
1185
+ this.groupMapCache = this.client.fetchGroupMemoryMap();
1186
+ this.groupMapCache.then((m) => { if (!m)
1187
+ this.groupMapCache = null; }).catch(() => { this.groupMapCache = null; });
1188
+ }
1136
1189
  // NEVER block tool-listing on the network. A flaky/unreachable API would otherwise hang the MCP
1137
- // handshake and freeze the whole agent ("connection timed out after 30000ms"). The map is
1138
- // best-effort: cap the wait, and it'll be injected on the next listing once it resolves.
1139
- const map = this.mapCache
1140
- ? await Promise.race([this.mapCache, new Promise((r) => setTimeout(() => r(undefined), 2500))])
1141
- : undefined;
1190
+ // handshake and freeze the whole agent ("connection timed out after 30000ms"). The maps are
1191
+ // best-effort: cap the wait, and they'll be injected on the next listing once they resolve.
1192
+ // Both races share one wall-clock budget because they run concurrently.
1193
+ const [map, groupMap] = await Promise.all([
1194
+ capWait(this.mapCache),
1195
+ capWait(this.groupMapCache),
1196
+ ]);
1142
1197
  this.mapInjected = !!map;
1198
+ this.groupMapInjected = !!groupMap;
1143
1199
  const updateNotice = formatUpdateNotice(this.updateStatus);
1144
- return { tools: listToolSpecs({ map, updateNotice }) };
1200
+ return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
1145
1201
  });
1146
1202
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
1147
1203
  const canonicalName = resolveCanonicalToolName(request.params.name);
@@ -1168,6 +1224,7 @@ class EchoMemMCPServer {
1168
1224
  type: "tool_call",
1169
1225
  tool: canonicalName,
1170
1226
  map_injected: this.mapInjected,
1227
+ group_map_injected: this.groupMapInjected,
1171
1228
  };
1172
1229
  try {
1173
1230
  // The usage report is a local, $0 audit — works with no login (value before signup).
@@ -1251,6 +1308,8 @@ class EchoMemMCPServer {
1251
1308
  return await this.handleOthers(request.params.arguments);
1252
1309
  case canonicalToolNames.publicMemory:
1253
1310
  return await this.handlePublicMemory(request.params.arguments);
1311
+ case canonicalToolNames.recordCitations:
1312
+ return await this.handleRecordMemoryCitations(request.params.arguments);
1254
1313
  case canonicalToolNames.groupContext:
1255
1314
  return await this.handleGroupContext(request.params.arguments);
1256
1315
  case canonicalToolNames.getGroupSessionSharing:
@@ -1876,6 +1935,28 @@ Details: ${m.details || "N/A"}`;
1876
1935
  content: [{ type: "text", text: withMemoryCitationInstruction(text) }],
1877
1936
  };
1878
1937
  }
1938
+ async handleRecordMemoryCitations(args) {
1939
+ const parsed = recordMemoryCitationsSchema.parse(args ?? {});
1940
+ const payload = await this.client.recordMemoryCitations(parsed);
1941
+ const acceptedMemoryIds = Array.isArray(payload?.acceptedMemoryIds)
1942
+ ? payload.acceptedMemoryIds.filter((id) => typeof id === "string")
1943
+ : [];
1944
+ const rejectedMemoryIds = Array.isArray(payload?.rejectedMemoryIds)
1945
+ ? payload.rejectedMemoryIds.filter((id) => typeof id === "string")
1946
+ : [];
1947
+ const insertedCount = typeof payload?.insertedCount === "number" ? payload.insertedCount : 0;
1948
+ const duplicateCount = typeof payload?.duplicateCount === "number" ? payload.duplicateCount : 0;
1949
+ const text = [
1950
+ `Citation receipt accepted for ${acceptedMemoryIds.length} ${acceptedMemoryIds.length === 1 ? "memory" : "memories"}.`,
1951
+ `New citations recorded: ${insertedCount}.`,
1952
+ duplicateCount ? `Idempotent duplicates skipped: ${duplicateCount}.` : "",
1953
+ rejectedMemoryIds.length
1954
+ ? `Rejected inaccessible or self-owned Memory IDs: ${rejectedMemoryIds.join(", ")}. Do not describe these as helped.`
1955
+ : "",
1956
+ "Now send the final answer with an EchoMem sources list containing the same accepted memories actually used.",
1957
+ ].filter(Boolean).join("\n");
1958
+ return { content: [{ type: "text", text }] };
1959
+ }
1879
1960
  async handleGroupContext(args) {
1880
1961
  groupContextSchema.parse(args ?? {});
1881
1962
  const payload = await this.client.getGroupContext(args);
@@ -23,7 +23,7 @@ export const MCP_PACKAGE_DESCRIPTION = stringOrFallback(packageJson.description,
23
23
  export const MCP_PACKAGE_LABEL = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
24
24
  export const MCP_UPDATE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest update`;
25
25
  export const MCP_UPDATE_ALL_COMMAND = `${MCP_UPDATE_COMMAND} --all`;
26
- export const MEMORY_CITATION_INSTRUCTION = 'If the user-facing answer materially relies on one or more EchoMem memories, end it with a compact "EchoMem sources:" list containing only the memories actually used. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL. Do not cite memories that were merely retrieved, and omit the section when no memory informed the answer.';
26
+ export const MEMORY_CITATION_INSTRUCTION = 'If the user-facing answer materially relies on one or more EchoMem memories, end it with a compact "EchoMem sources:" list containing only the memories actually used. For memories owned by teammates or accepted friends, call record_memory_citations immediately before the final answer with those exact Memory IDs. Do not cite memories that were merely retrieved. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL. Omit the sources section and citation receipt when no memory informed the answer.';
27
27
  export const MCP_SERVER_INSTRUCTIONS = [
28
28
  `${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
29
29
  `If the user or local config expects a newer EchoMem MCP version than ${MCP_PACKAGE_VERSION}, update once with \`${MCP_UPDATE_ALL_COMMAND}\` and start a new MCP session.`,
@@ -10,6 +10,7 @@ export const canonicalToolNames = {
10
10
  sendFriendRequest: "send_friend_request",
11
11
  others: "search_others_memories",
12
12
  publicMemory: "get_public_memory",
13
+ recordCitations: "record_memory_citations",
13
14
  groupContext: "get_group_context",
14
15
  getGroupSessionSharing: "get_group_session_sharing",
15
16
  setGroupSessionSharing: "set_group_session_sharing",
@@ -114,6 +115,11 @@ export const publicMemorySchema = z.object({
114
115
  ...triggerMetadataSchema,
115
116
  memoryId: z.string().min(1),
116
117
  });
118
+ export const recordMemoryCitationsSchema = z.object({
119
+ ...triggerMetadataSchema,
120
+ memoryIds: z.array(z.string().uuid()).min(1).max(20),
121
+ receiptId: z.string().min(1).max(200),
122
+ });
117
123
  export const groupContextSchema = z.object({
118
124
  ...triggerMetadataSchema,
119
125
  });
@@ -201,6 +207,7 @@ export const getByContextSchema = z.object({
201
207
  export function listToolSpecs(opts = {}) {
202
208
  const currentTime = new Date().toISOString();
203
209
  const map = opts.map?.trim();
210
+ const groupMap = opts.groupMap?.trim();
204
211
  const updateNotice = opts.updateNotice?.trim();
205
212
  const recallPlanNote = "Available on every plan: Free includes 100 searches each week, Pro includes 500, and Power includes 2,000.";
206
213
  const searchBillingReplyInstruction = "If search returns an ACTION REQUIRED subscription message, tell the user to start their trial or subscription and include the exact URL from that result verbatim. Do not respond only with \"connect\" or \"upgrade\".";
@@ -209,6 +216,11 @@ export function listToolSpecs(opts = {}) {
209
216
  const mapSection = map
210
217
  ? `\n\nThis user's EchoMem currently covers these topics (a relevance guide — recall when the task relates to one of them):\n${map}\n`
211
218
  : "";
219
+ // Same device as the personal map, aimed at the group surface: the agent judges whether teammates
220
+ // have covered the topic before searching, instead of never calling the group tools at all.
221
+ const groupMapSection = groupMap
222
+ ? `\n\nThis user's company group currently shares work in these areas (a relevance guide — search the group when the task relates to one of these people or topics):\n${groupMap}\n`
223
+ : "";
212
224
  return [
213
225
  {
214
226
  name: canonicalToolNames.search,
@@ -385,7 +397,7 @@ export function listToolSpecs(opts = {}) {
385
397
  },
386
398
  {
387
399
  name: canonicalToolNames.others,
388
- description: `Search public memories from accepted friends or people who share your company group. For onboarding and division-of-work questions, call get_group_context first, then use this tool for current evidence. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}`,
400
+ description: `Search public memories from accepted friends or people who share your company group. For onboarding and division-of-work questions, call get_group_context first, then use this tool for current evidence. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}${groupMapSection}`,
389
401
  inputSchema: {
390
402
  type: "object",
391
403
  properties: {
@@ -447,9 +459,37 @@ export function listToolSpecs(opts = {}) {
447
459
  required: ["memoryId"],
448
460
  },
449
461
  },
462
+ {
463
+ name: canonicalToolNames.recordCitations,
464
+ description: "Call this tool immediately before the final answer when it materially uses memories owned by teammates or accepted friends. Include only the exact Memory IDs actually used. Do not cite memories that were merely retrieved. Create one opaque receiptId for the planned final answer and reuse it exactly if this tool call is retried. Do not include answer text, prompts, or secrets in receiptId.",
465
+ inputSchema: {
466
+ type: "object",
467
+ properties: {
468
+ memoryIds: {
469
+ type: "array",
470
+ minItems: 1,
471
+ maxItems: 20,
472
+ items: { type: "string", format: "uuid" },
473
+ description: "Exact IDs of teammate/friend memories materially used in the final answer.",
474
+ },
475
+ receiptId: {
476
+ type: "string",
477
+ minLength: 1,
478
+ maxLength: 200,
479
+ description: "Opaque per-answer idempotency key. Reuse the same value on retry; never include answer content.",
480
+ },
481
+ triggerMessage: {
482
+ type: "string",
483
+ description: "Optional user message that caused the answer. EchoMem stores only redacted analytics metadata.",
484
+ },
485
+ triggerMessageRole: { type: "string", default: "user" },
486
+ },
487
+ required: ["memoryIds", "receiptId"],
488
+ },
489
+ },
450
490
  {
451
491
  name: canonicalToolNames.groupContext,
452
- description: "Get your current company group, its participant directory, declared titles and responsibilities, and published-memory coverage. Use this before answering who works on what or suggesting where a new group member could contribute. Treat declared profile fields as facts and memory-derived work as evidence or inference. Session sharing is separate; call get_group_session_sharing instead of inferring it.",
492
+ description: `Get your current company group, its participant directory, declared titles and responsibilities, and published-memory coverage. Use this before answering who works on what or suggesting where a new group member could contribute. Treat declared profile fields as facts and memory-derived work as evidence or inference. Session sharing is separate; call get_group_session_sharing instead of inferring it.${groupMapSection}`,
453
493
  inputSchema: {
454
494
  type: "object",
455
495
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.29",
3
+ "version": "1.4.30",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools, local context HUD, and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "test:registry-ui": "npm run build && node test/registry-ui.test.mjs",
28
28
  "test:ui": "npm run build && node test/setup-ui.test.mjs",
29
29
  "test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
30
- "test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/hud.test.mjs && node test/save-checkpoint-hook.test.mjs",
30
+ "test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/hud.test.mjs && node test/save-checkpoint-hook.test.mjs",
31
31
  "prepack": "npm run build && node scripts/bundle-city.mjs"
32
32
  },
33
33
  "dependencies": {