@echomem/mcp 1.4.35 → 1.4.36

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.
@@ -132,7 +132,7 @@ function summarizeToolArgs(toolName, args) {
132
132
  tag_count: Array.isArray(args.tags) ? args.tags.length : undefined,
133
133
  };
134
134
  }
135
- if (toolName === "get_memories_by_time_range" || toolName === "search_memories_by_time_range") {
135
+ if (toolName === "get_memories_by_time_range") {
136
136
  return {
137
137
  has_start_date: !!asString(args.startDate),
138
138
  has_end_date: !!asString(args.endDate),
package/dist/index.js CHANGED
@@ -192,6 +192,56 @@ function readNumber(record, key) {
192
192
  const value = record[key];
193
193
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
194
194
  }
195
+ function normalizeQueryAlias(args) {
196
+ if (!isRecord(args) || !("conversation" in args))
197
+ return args;
198
+ const query = readString(args, "query") ?? readString(args, "conversation");
199
+ const { conversation: _ignored, ...rest } = args;
200
+ return query ? { ...rest, query } : rest;
201
+ }
202
+ const PERSONAL_RECALL_TOOL_NAMES = new Set([
203
+ canonicalToolNames.search,
204
+ canonicalToolNames.timeRange,
205
+ canonicalToolNames.keywords,
206
+ ]);
207
+ function hasKeywordInput(args) {
208
+ const value = args.keywords;
209
+ if (Array.isArray(value)) {
210
+ return value.some((item) => typeof item === "string" && item.trim().length > 0);
211
+ }
212
+ return typeof value === "string" && value.trim().length > 0;
213
+ }
214
+ function routePersonalRecallInvocation(canonicalName, args) {
215
+ if (!PERSONAL_RECALL_TOOL_NAMES.has(canonicalName)) {
216
+ return { canonicalName, args };
217
+ }
218
+ const normalizedArgs = normalizeQueryAlias(args);
219
+ if (!isRecord(normalizedArgs)) {
220
+ return { canonicalName, args: normalizedArgs };
221
+ }
222
+ const hasSemanticInput = Boolean(readString(normalizedArgs, "query")
223
+ || readNumber(normalizedArgs, "timeFrameDays") !== undefined);
224
+ const hasKeywords = hasKeywordInput(normalizedArgs);
225
+ const hasDateRangeInput = Boolean(readString(normalizedArgs, "startDate") || readString(normalizedArgs, "endDate"));
226
+ const intentCount = Number(hasSemanticInput) + Number(hasKeywords) + Number(hasDateRangeInput);
227
+ if (intentCount > 1) {
228
+ return {
229
+ canonicalName,
230
+ args: normalizedArgs,
231
+ error: "Ambiguous memory search arguments. Use exactly one input shape: {query, optional timeFrameDays} for topic search, {keywords} for exact-key search, or {startDate, endDate} for a date range.",
232
+ };
233
+ }
234
+ if (hasSemanticInput) {
235
+ return { canonicalName: canonicalToolNames.search, args: normalizedArgs };
236
+ }
237
+ if (hasKeywords) {
238
+ return { canonicalName: canonicalToolNames.keywords, args: normalizedArgs };
239
+ }
240
+ if (hasDateRangeInput) {
241
+ return { canonicalName: canonicalToolNames.timeRange, args: normalizedArgs };
242
+ }
243
+ return { canonicalName, args: normalizedArgs };
244
+ }
195
245
  function errorCodeFrom(value) {
196
246
  if (!isRecord(value))
197
247
  return undefined;
@@ -783,12 +833,12 @@ class EchoMemApiClient {
783
833
  };
784
834
  }
785
835
  async searchMemories(args, trace) {
786
- const parsed = searchMemoriesSchema.parse(args ?? {});
836
+ const parsed = searchMemoriesSchema.parse(normalizeQueryAlias(args) ?? {});
787
837
  const query = parsed.query?.trim();
788
838
  const limit = parsed.limit ?? parsed.k ?? 10;
789
839
  const threshold = parsed.threshold ?? 0.1;
790
840
  if (!query && !parsed.timeFrameDays) {
791
- throw new McpError(ErrorCode.InvalidParams, "Provide query, or provide timeFrameDays for chronological fetch.");
841
+ throw new McpError(ErrorCode.InvalidParams, "search_memories requires a non-empty query. For example: {\"query\":\"research article hero image\",\"timeFrameDays\":14}. For an explicit date range, call get_memories_by_time_range with both startDate and endDate.");
792
842
  }
793
843
  const enc = await this.encState(); // throws LockedError for an encrypted account without a key
794
844
  if (!query) {
@@ -1220,7 +1270,12 @@ class EchoMemMCPServer {
1220
1270
  return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
1221
1271
  });
1222
1272
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
1223
- const canonicalName = resolveCanonicalToolName(request.params.name);
1273
+ const resolvedCanonicalName = resolveCanonicalToolName(request.params.name);
1274
+ const recallRoute = routePersonalRecallInvocation(resolvedCanonicalName, request.params.arguments);
1275
+ const canonicalName = recallRoute.canonicalName;
1276
+ const toolArgs = canonicalName === canonicalToolNames.others
1277
+ ? normalizeQueryAlias(recallRoute.args)
1278
+ : recallRoute.args;
1224
1279
  const clientVersion = this.server.getClientVersion();
1225
1280
  this.mcpClientName = clientVersion?.name ?? this.mcpClientName;
1226
1281
  this.mcpClientVersion = clientVersion?.version ?? this.mcpClientVersion;
@@ -1235,8 +1290,8 @@ class EchoMemMCPServer {
1235
1290
  conversation_id: this.client.getSessionId(),
1236
1291
  tool_name: request.params.name,
1237
1292
  canonical_tool_name: canonicalName,
1238
- ...triggerAnalyticsForTool(canonicalName, request.params.arguments),
1239
- ...inputAnalyticsForTool(canonicalName, request.params.arguments),
1293
+ ...triggerAnalyticsForTool(canonicalName, toolArgs),
1294
+ ...inputAnalyticsForTool(canonicalName, toolArgs),
1240
1295
  };
1241
1296
  const analyticsCallId = randomUUID();
1242
1297
  // One event per call. Handlers enrich `rec` with tool-specific detail; we finalize + log in `finally`.
@@ -1247,6 +1302,9 @@ class EchoMemMCPServer {
1247
1302
  group_map_injected: this.groupMapInjected,
1248
1303
  };
1249
1304
  try {
1305
+ if (recallRoute.error) {
1306
+ throw new McpError(ErrorCode.InvalidParams, recallRoute.error);
1307
+ }
1250
1308
  // The usage report is a local, $0 audit — works with no login (value before signup).
1251
1309
  if (canonicalName === canonicalToolNames.report) {
1252
1310
  return { content: [{ type: "text", text: await buildReportText(false) }] };
@@ -1315,17 +1373,17 @@ class EchoMemMCPServer {
1315
1373
  }
1316
1374
  switch (canonicalName) {
1317
1375
  case canonicalToolNames.search:
1318
- return await this.handleSearch(request.params.arguments, rec);
1376
+ return await this.handleSearch(toolArgs, rec);
1319
1377
  case canonicalToolNames.save:
1320
1378
  return await this.handleSave(request.params.arguments, rec);
1321
1379
  case canonicalToolNames.timeRange:
1322
- return await this.handleTimeRange(request.params.arguments);
1380
+ return await this.handleTimeRange(toolArgs);
1323
1381
  case canonicalToolNames.getByContext:
1324
1382
  return await this.handleGetByContext(request.params.arguments);
1325
1383
  case canonicalToolNames.checkpointByContext:
1326
1384
  return await this.handleGetCheckpointByContext(request.params.arguments);
1327
1385
  case canonicalToolNames.keywords:
1328
- return await this.handleKeywords(request.params.arguments);
1386
+ return await this.handleKeywords(toolArgs);
1329
1387
  case canonicalToolNames.friends:
1330
1388
  return await this.handleFriends(request.params.arguments);
1331
1389
  case canonicalToolNames.searchUsers:
@@ -1333,7 +1391,7 @@ class EchoMemMCPServer {
1333
1391
  case canonicalToolNames.sendFriendRequest:
1334
1392
  return await this.handleSendFriendRequest(request.params.arguments);
1335
1393
  case canonicalToolNames.others:
1336
- return await this.handleOthers(request.params.arguments);
1394
+ return await this.handleOthers(toolArgs);
1337
1395
  case canonicalToolNames.publicMemory:
1338
1396
  return await this.handlePublicMemory(request.params.arguments);
1339
1397
  case canonicalToolNames.recordCitations:
@@ -34,7 +34,6 @@ export const canonicalToolNames = {
34
34
  };
35
35
  export const legacyAliasToCanonical = {
36
36
  search_memories_by_description_semantic: canonicalToolNames.search,
37
- search_memories_by_time_range: canonicalToolNames.timeRange,
38
37
  };
39
38
  export function resolveCanonicalToolName(toolName) {
40
39
  return legacyAliasToCanonical[toolName] ?? toolName;
@@ -44,7 +43,6 @@ const READ_ONLY_TOOL_NAMES = new Set([
44
43
  canonicalToolNames.search,
45
44
  "search_memories_by_description_semantic",
46
45
  canonicalToolNames.timeRange,
47
- "search_memories_by_time_range",
48
46
  canonicalToolNames.keywords,
49
47
  canonicalToolNames.friends,
50
48
  canonicalToolNames.searchUsers,
@@ -166,11 +164,11 @@ const triggerMetadataSchema = {
166
164
  };
167
165
  export const searchMemoriesSchema = z.object({
168
166
  ...triggerMetadataSchema,
169
- query: z.string().optional(),
170
- k: z.number().optional(),
171
- limit: z.number().optional(),
172
- threshold: z.number().optional().default(0.1),
173
- timeFrameDays: z.number().optional(),
167
+ query: z.string().trim().min(1).optional(),
168
+ k: z.number().int().min(1).max(50).optional(),
169
+ limit: z.number().int().min(1).max(50).optional(),
170
+ threshold: z.number().min(0).max(1).optional().default(0.1),
171
+ timeFrameDays: z.number().int().min(1).max(3650).optional(),
174
172
  includeAnswer: z.boolean().optional().default(false),
175
173
  });
176
174
  export const saveConversationSchema = z.object({
@@ -189,11 +187,20 @@ export const saveConversationSchema = z.object({
189
187
  }))
190
188
  .optional(),
191
189
  });
190
+ const dateBoundarySchema = z.string().trim().min(1).refine((value) => Number.isFinite(Date.parse(value)), "Expected an ISO-8601 date or date-time string");
192
191
  export const timeRangeSchema = z.object({
193
192
  ...triggerMetadataSchema,
194
- startDate: z.string(),
195
- endDate: z.string(),
196
- limit: z.number().optional().default(50),
193
+ startDate: dateBoundarySchema,
194
+ endDate: dateBoundarySchema,
195
+ limit: z.number().int().min(1).max(100).optional().default(50),
196
+ }).superRefine((value, ctx) => {
197
+ if (Date.parse(value.startDate) > Date.parse(value.endDate)) {
198
+ ctx.addIssue({
199
+ code: z.ZodIssueCode.custom,
200
+ path: ["startDate"],
201
+ message: "startDate must be earlier than or equal to endDate",
202
+ });
203
+ }
197
204
  });
198
205
  const keywordListSchema = z.preprocess((value) => {
199
206
  if (typeof value !== "string")
@@ -206,15 +213,15 @@ const keywordListSchema = z.preprocess((value) => {
206
213
  export const keywordsSchema = z.object({
207
214
  ...triggerMetadataSchema,
208
215
  keywords: keywordListSchema,
209
- limit: z.number().optional().default(10),
216
+ limit: z.number().int().min(1).max(50).optional().default(10),
210
217
  });
211
218
  export const listFriendsSchema = z.object({
212
219
  ...triggerMetadataSchema,
213
220
  });
214
221
  export const searchUsersSchema = z.object({
215
222
  ...triggerMetadataSchema,
216
- query: z.string().min(1),
217
- limit: z.number().optional().default(10),
223
+ query: z.string().trim().min(1),
224
+ limit: z.number().int().min(1).max(50).optional().default(10),
218
225
  });
219
226
  export const sendFriendRequestSchema = z.object({
220
227
  ...triggerMetadataSchema,
@@ -222,17 +229,17 @@ export const sendFriendRequestSchema = z.object({
222
229
  });
223
230
  export const othersSchema = z.object({
224
231
  ...triggerMetadataSchema,
225
- query: z.string().optional().default(""),
226
- limit: z.number().optional().default(10),
232
+ query: z.string().trim().optional().default(""),
233
+ limit: z.number().int().min(1).max(50).optional().default(10),
227
234
  target: z.string().optional(),
228
235
  ownerUserId: z.string().optional(),
229
236
  ownerName: z.string().optional(),
230
237
  targetFriendIds: z.array(z.string()).optional(),
231
238
  targetFriendNames: z.array(z.string()).optional(),
232
239
  recordAccess: z.boolean().optional(),
233
- kPerUser: z.number().optional(),
234
- similarityThreshold: z.number().optional(),
235
- timeFrameDays: z.number().optional(),
240
+ kPerUser: z.number().int().min(1).max(50).optional(),
241
+ similarityThreshold: z.number().min(0).max(1).optional(),
242
+ timeFrameDays: z.number().int().min(1).max(3650).optional(),
236
243
  });
237
244
  export const publicMemorySchema = z.object({
238
245
  ...triggerMetadataSchema,
@@ -329,8 +336,8 @@ export const deleteMemorySchema = z.object({
329
336
  });
330
337
  export const getByContextSchema = z.object({
331
338
  ...triggerMetadataSchema,
332
- contextId: z.string().min(1),
333
- limit: z.number().optional().default(50),
339
+ contextId: z.string().trim().min(1),
340
+ limit: z.number().int().min(1).max(100).optional().default(50),
334
341
  });
335
342
  export function listToolSpecs(opts = {}) {
336
343
  const currentTime = new Date().toISOString();
@@ -349,42 +356,64 @@ export function listToolSpecs(opts = {}) {
349
356
  const groupMapSection = groupMap
350
357
  ? `\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${compactGroupRoster(groupMap)}\n${groupMap}\n`
351
358
  : "";
359
+ const semanticSearchInputSchema = {
360
+ type: "object",
361
+ properties: {
362
+ query: {
363
+ type: "string",
364
+ minLength: 1,
365
+ description: "Required topic or question to search for. Never omit this field or send it as conversation.",
366
+ },
367
+ limit: { type: "integer", minimum: 1, maximum: 50, default: 10 },
368
+ threshold: { type: "number", minimum: 0, maximum: 1, default: 0.1 },
369
+ timeFrameDays: {
370
+ type: "integer",
371
+ minimum: 1,
372
+ maximum: 3650,
373
+ description: "Optional recency filter for this query, such as 14 for the last two weeks.",
374
+ },
375
+ triggerMessage: {
376
+ type: "string",
377
+ description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
378
+ },
379
+ triggerMessageRole: { type: "string", default: "user" },
380
+ },
381
+ required: ["query"],
382
+ };
383
+ const timeRangeInputSchema = {
384
+ type: "object",
385
+ properties: {
386
+ startDate: {
387
+ type: "string",
388
+ minLength: 1,
389
+ description: "Required ISO-8601 start date or date-time. Must not be later than endDate.",
390
+ },
391
+ endDate: {
392
+ type: "string",
393
+ minLength: 1,
394
+ description: "Required ISO-8601 end date or date-time. Must not be earlier than startDate.",
395
+ },
396
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
397
+ triggerMessage: {
398
+ type: "string",
399
+ description: "Optional: the user's message that caused this lookup. EchoMem stores only a redacted analytics preview and hash.",
400
+ },
401
+ triggerMessageRole: { type: "string", default: "user" },
402
+ },
403
+ required: ["startDate", "endDate"],
404
+ };
352
405
  const tools = [
353
406
  {
354
407
  name: canonicalToolNames.search,
355
- description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
356
- inputSchema: {
357
- type: "object",
358
- properties: {
359
- query: { type: "string" },
360
- limit: { type: "number", default: 10 },
361
- threshold: { type: "number", default: 0.1 },
362
- timeFrameDays: { type: "number" },
363
- triggerMessage: {
364
- type: "string",
365
- description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
366
- },
367
- triggerMessageRole: { type: "string", default: "user" },
368
- },
369
- },
408
+ title: "Search your memories by topic",
409
+ description: withMcpVersion(`TOPIC SEARCH for the user's own EchoMem memories across all AI tools. Always pass a non-empty query; optionally add timeFrameDays to filter that topic to recent memories. Do not pass keywords, startDate, or endDate. For exact memory-key terms use search_memories_by_keywords. For an explicit calendar range use get_memories_by_time_range. Use this tool instead of re-deriving or re-asking what the user already settled. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
410
+ inputSchema: semanticSearchInputSchema,
370
411
  },
371
412
  {
372
413
  name: "search_memories_by_description_semantic",
373
- description: `Legacy alias for search_memories. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}`,
374
- inputSchema: {
375
- type: "object",
376
- properties: {
377
- query: { type: "string" },
378
- limit: { type: "number", default: 10 },
379
- threshold: { type: "number", default: 0.1 },
380
- timeFrameDays: { type: "number" },
381
- triggerMessage: {
382
- type: "string",
383
- description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
384
- },
385
- triggerMessageRole: { type: "string", default: "user" },
386
- },
387
- },
414
+ title: "Legacy topic search (compatibility)",
415
+ description: `LEGACY TOPIC SEARCH alias for search_memories. Prefer search_memories. Always pass a non-empty query; timeFrameDays is only an optional recency filter. Do not pass keywords, startDate, or endDate. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}`,
416
+ inputSchema: semanticSearchInputSchema,
388
417
  },
389
418
  {
390
419
  name: canonicalToolNames.save,
@@ -426,25 +455,14 @@ export function listToolSpecs(opts = {}) {
426
455
  },
427
456
  {
428
457
  name: canonicalToolNames.timeRange,
429
- description: `Retrieve memories within a specific date range. ${recallPlanNote} ${memoryCitationInstruction} Current time: ${currentTime}.`,
430
- inputSchema: {
431
- type: "object",
432
- properties: {
433
- startDate: { type: "string" },
434
- endDate: { type: "string" },
435
- limit: { type: "number", default: 50 },
436
- triggerMessage: {
437
- type: "string",
438
- description: "Optional: the user's message that caused this lookup. EchoMem stores only a redacted analytics preview and hash.",
439
- },
440
- triggerMessageRole: { type: "string", default: "user" },
441
- },
442
- required: ["startDate", "endDate"],
443
- },
458
+ title: "Get your memories by date range",
459
+ description: `DATE-RANGE FETCH for the user's own memories. Always pass both startDate and endDate as ISO-8601 strings. Do not pass query, keywords, or timeFrameDays. For a topic with a recency filter use search_memories instead. ${recallPlanNote} ${memoryCitationInstruction} Current time: ${currentTime}.`,
460
+ inputSchema: timeRangeInputSchema,
444
461
  },
445
462
  {
446
463
  name: canonicalToolNames.keywords,
447
- description: `Search memories based on keywords in keys field. Pass keywords as valid JSON: preferably an array of quoted strings, for example {"keywords":["flow-lab","flow.html","Rive"],"limit":8}. A comma-separated JSON string is also accepted as a compatibility fallback. Never emit bare comma-separated tokens. ${recallPlanNote} ${memoryCitationInstruction}`,
464
+ title: "Search your memory keys exactly",
465
+ description: `EXACT-KEYWORD SEARCH over the keys field of the user's own memories. Pass keywords as valid JSON: preferably an array of quoted strings, for example {"keywords":["flow-lab","flow.html","Rive"],"limit":8}. Do not pass query, timeFrameDays, startDate, or endDate. For natural-language topic search use search_memories. A comma-separated JSON string is accepted only as a compatibility fallback. Never emit bare comma-separated tokens. ${recallPlanNote} ${memoryCitationInstruction}`,
448
466
  inputSchema: {
449
467
  type: "object",
450
468
  properties: {
@@ -477,6 +495,7 @@ export function listToolSpecs(opts = {}) {
477
495
  },
478
496
  {
479
497
  name: canonicalToolNames.friends,
498
+ title: "List accepted EchoMem friends",
480
499
  description: "Friends: list accepted EchoMem friends with each friend's public memory count. Use this before asking a specific friend by name.",
481
500
  inputSchema: {
482
501
  type: "object",
@@ -491,7 +510,8 @@ export function listToolSpecs(opts = {}) {
491
510
  },
492
511
  {
493
512
  name: canonicalToolNames.searchUsers,
494
- description: "Search EchoMem users by display name/username before sending a friend request. Results include total memories, public memories, and current relationship state.",
513
+ title: "Search the EchoMem user directory",
514
+ description: "USER-DIRECTORY SEARCH by display name or username before sending a friend request. This searches people/accounts, not memory content. Results include total memories, public memories, and current relationship state.",
495
515
  inputSchema: {
496
516
  type: "object",
497
517
  properties: {
@@ -530,12 +550,16 @@ export function listToolSpecs(opts = {}) {
530
550
  },
531
551
  {
532
552
  name: canonicalToolNames.others,
533
- description: `Search public memories from accepted friends or people who share your company group. EchoMem identifies the caller from the EchoMem credential and has already excluded only that authenticated user's own memories. Present every returned owner; never filter again using a Claude account, host profile, git identity, or inferred identity. 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}`,
553
+ title: "Search teammates' and friends' memories",
554
+ description: `PEER-MEMORY SEARCH for public memories owned by accepted friends or company-group members—not the user's own memories. Pass query for a topic; omit it only when intentionally browsing peer memories, and optionally use target to scope a person. Do not use this tool for the user's private memories or the EchoMem user directory. EchoMem identifies the caller from the EchoMem credential and has already excluded only that authenticated user's own memories. Present every returned owner; never filter again using a Claude account, host profile, git identity, or inference. For onboarding and division-of-work questions, call get_group_context first. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}${groupMapSection}`,
534
555
  inputSchema: {
535
556
  type: "object",
536
557
  properties: {
537
- query: { type: "string" },
538
- limit: { type: "number", default: 10 },
558
+ query: {
559
+ type: "string",
560
+ description: "Optional peer-memory topic. Omit only for an intentional broad browse; never send this field as conversation.",
561
+ },
562
+ limit: { type: "integer", minimum: 1, maximum: 50, default: 10 },
539
563
  target: {
540
564
  type: "string",
541
565
  description: "Accessible friend or group-member user id or exact display name. Prefer this for @Name asks.",
@@ -562,9 +586,9 @@ export function listToolSpecs(opts = {}) {
562
586
  type: "boolean",
563
587
  description: "Defaults true. When true, returned public memories are recorded in memory_views.",
564
588
  },
565
- kPerUser: { type: "number", default: 5 },
566
- similarityThreshold: { type: "number", default: 0.1 },
567
- timeFrameDays: { type: "number" },
589
+ kPerUser: { type: "integer", minimum: 1, maximum: 50, default: 5 },
590
+ similarityThreshold: { type: "number", minimum: 0, maximum: 1, default: 0.1 },
591
+ timeFrameDays: { type: "integer", minimum: 1, maximum: 3650 },
568
592
  triggerMessage: {
569
593
  type: "string",
570
594
  description: "Optional: the user's message that caused this public-memory search. EchoMem stores only a redacted analytics preview and hash.",
@@ -575,6 +599,7 @@ export function listToolSpecs(opts = {}) {
575
599
  },
576
600
  {
577
601
  name: canonicalToolNames.publicMemory,
602
+ title: "Get one teammate or friend memory",
578
603
  description: `Fetch one public memory by id when its owner is an accepted friend or shares your company group. If the caller is not the owner, EchoMem records the access in memory_views. ${memoryCitationInstruction}`,
579
604
  inputSchema: {
580
605
  type: "object",
@@ -892,6 +917,7 @@ export function listToolSpecs(opts = {}) {
892
917
  },
893
918
  {
894
919
  name: canonicalToolNames.getByContext,
920
+ title: "Get the exact memories from a saved context",
895
921
  description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. ${recallPlanNote} save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. ${memoryCitationInstruction} Current time: ${currentTime}.`),
896
922
  inputSchema: {
897
923
  type: "object",
@@ -909,6 +935,7 @@ export function listToolSpecs(opts = {}) {
909
935
  },
910
936
  {
911
937
  name: canonicalToolNames.checkpointByContext,
938
+ title: "Rebuild a checkpoint from a saved context",
912
939
  description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${recallPlanNote} Use this when the user or EchoMem returns a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. ${memoryCitationInstruction} Current time: ${currentTime}.`),
913
940
  inputSchema: {
914
941
  type: "object",
@@ -971,19 +998,6 @@ export function listToolSpecs(opts = {}) {
971
998
  },
972
999
  },
973
1000
  },
974
- {
975
- name: "search_memories_by_time_range",
976
- description: "Legacy alias for get_memories_by_time_range.",
977
- inputSchema: {
978
- type: "object",
979
- properties: {
980
- startDate: { type: "string" },
981
- endDate: { type: "string" },
982
- limit: { type: "number", default: 50 },
983
- },
984
- required: ["startDate", "endDate"],
985
- },
986
- },
987
1001
  ];
988
1002
  return tools.map(decorateLocalToolSpec);
989
1003
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.35",
3
+ "version": "1.4.36",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",