recallx 1.0.8 → 1.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/README.md CHANGED
@@ -204,10 +204,29 @@ Use the returned endpoint list and request examples to search nodes and activiti
204
204
  Reuse the existing local service instead of starting a new one.
205
205
  ```
206
206
 
207
+ Recommended instruction for MCP-connected coding agents:
208
+
209
+ ```text
210
+ Use my RecallX MCP server as an active local memory layer during this task, not just for final write-back.
211
+ Treat the current workspace as the default scope and do not switch workspaces unless I explicitly ask.
212
+ Before making assumptions or starting meaningful work, read context first: confirm the current workspace, use recallx_search_workspace when the target is still unclear, search for an existing project node when the task is clearly project-shaped, and build a compact recallx_context_bundle once the relevant node or project is known.
213
+ Prefer compact context over repeated broad browsing.
214
+ Once a project is known, append routine work logs to that project instead of writing untargeted workspace captures.
215
+ At the end of meaningful work, write back a concise summary of what changed, what was verified, and any follow-up.
216
+ ```
217
+
218
+ MCP result rendering note:
219
+
220
+ - RecallX MCP keeps `structuredContent` as the authoritative machine-readable payload.
221
+ - The mirrored text content may be a compact deterministic summary rather than a pretty-printed JSON dump.
222
+ - For the current rendering policy and compatibility guidance, see `docs/mcp.md`.
223
+
207
224
  ## MCP Bridge
208
225
 
209
226
  RecallX also ships a stdio MCP adapter for agent clients that prefer tool discovery over raw HTTP calls.
210
227
 
228
+ MCP tool results keep `structuredContent` as the authoritative machine-readable payload, while `content.text` is rendered as a compact deterministic summary instead of a pretty-printed JSON mirror when the payload shape is known.
229
+
211
230
  ```bash
212
231
  npm run mcp
213
232
  node dist/server/app/mcp/index.js --api http://127.0.0.1:8787/api/v1
@@ -206,8 +206,18 @@ export function renderTelemetrySummary(data) {
206
206
  `since: ${data?.since || ""}`,
207
207
  `logs: ${data?.logsPath || ""}`,
208
208
  `events: ${data?.totalEvents ?? 0}`,
209
+ `slow threshold: ${data?.slowRequestThresholdMs ?? ""}ms`,
209
210
  ];
210
211
 
212
+ const hot = Array.isArray(data?.hotOperations) ? data.hotOperations : [];
213
+ if (hot.length > 0) {
214
+ lines.push("");
215
+ lines.push("Hot operations:");
216
+ for (const item of hot.slice(0, 5)) {
217
+ lines.push(`- [${item.surface}] ${item.operation} p95=${item.p95DurationMs ?? ""}ms errors=${item.errorCount}/${item.count}`);
218
+ }
219
+ }
220
+
211
221
  const slow = Array.isArray(data?.slowOperations) ? data.slowOperations : [];
212
222
  if (slow.length > 0) {
213
223
  lines.push("");
package/app/mcp/index.js CHANGED
@@ -45,7 +45,7 @@ async function resolveObservabilityState() {
45
45
  workspaceRoot: typeof workspace.rootPath === "string" ? workspace.rootPath : process.cwd(),
46
46
  workspaceName: typeof workspace.workspaceName === "string" ? workspace.workspaceName : "RecallX MCP",
47
47
  retentionDays: typeof values["observability.retentionDays"] === "number" ? values["observability.retentionDays"] : 14,
48
- slowRequestMs: typeof values["observability.slowRequestMs"] === "number" ? values["observability.slowRequestMs"] : 250,
48
+ slowRequestMs: typeof values["observability.slowRequestMs"] === "number" ? values["observability.slowRequestMs"] : 50,
49
49
  capturePayloadShape: values["observability.capturePayloadShape"] !== false
50
50
  };
51
51
  }
@@ -55,7 +55,7 @@ async function resolveObservabilityState() {
55
55
  workspaceRoot: process.cwd(),
56
56
  workspaceName: "RecallX MCP",
57
57
  retentionDays: 14,
58
- slowRequestMs: 250,
58
+ slowRequestMs: 50,
59
59
  capturePayloadShape: true
60
60
  };
61
61
  }
package/app/mcp/server.js CHANGED
@@ -64,8 +64,264 @@ function coerceBooleanSchema(defaultValue) {
64
64
  return value;
65
65
  }, z.boolean().default(defaultValue));
66
66
  }
67
+ const textSummaryItemLimit = 5;
68
+ const textSummaryLength = 140;
69
+ const textTitleLength = 80;
70
+ function isRecord(value) {
71
+ return typeof value === "object" && value !== null && !Array.isArray(value);
72
+ }
73
+ function cleanInlineText(value, maxLength = textSummaryLength) {
74
+ const compact = value.replace(/\s+/g, " ").trim();
75
+ if (compact.length <= maxLength) {
76
+ return compact;
77
+ }
78
+ return `${compact.slice(0, maxLength - 3).trimEnd()}...`;
79
+ }
80
+ function formatKeyLabel(key) {
81
+ return key
82
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
83
+ .replace(/[_-]+/g, " ")
84
+ .trim();
85
+ }
86
+ function capitalize(value) {
87
+ return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
88
+ }
89
+ function pickString(record, keys) {
90
+ for (const key of keys) {
91
+ const candidate = record[key];
92
+ if (typeof candidate === "string" && candidate.trim()) {
93
+ return candidate;
94
+ }
95
+ }
96
+ return undefined;
97
+ }
98
+ function summarizeValue(value) {
99
+ if (typeof value === "string" && value.trim()) {
100
+ return cleanInlineText(value);
101
+ }
102
+ if (typeof value === "number" || typeof value === "boolean") {
103
+ return String(value);
104
+ }
105
+ if (Array.isArray(value)) {
106
+ return `${value.length} item(s)`;
107
+ }
108
+ if (isRecord(value)) {
109
+ const title = pickString(value, ["title", "name", "id", "nodeId"]);
110
+ if (title) {
111
+ return cleanInlineText(title);
112
+ }
113
+ return `${Object.keys(value).length} field(s)`;
114
+ }
115
+ return undefined;
116
+ }
117
+ function unwrapSummaryRecord(record) {
118
+ const resultType = typeof record.resultType === "string" ? record.resultType : undefined;
119
+ if (resultType && isRecord(record[resultType])) {
120
+ const nested = record[resultType];
121
+ return {
122
+ kind: (resultType === "activity" ? pickString(nested, ["activityType"]) : undefined) ??
123
+ (resultType === "node" ? pickString(nested, ["type"]) : undefined) ??
124
+ resultType,
125
+ value: nested
126
+ };
127
+ }
128
+ if (isRecord(record.node)) {
129
+ return {
130
+ kind: resultType ?? "node",
131
+ value: record.node
132
+ };
133
+ }
134
+ if (isRecord(record.activity)) {
135
+ return {
136
+ kind: resultType ?? "activity",
137
+ value: record.activity
138
+ };
139
+ }
140
+ return {
141
+ kind: pickString(record, ["type", "activityType", "status"]) ??
142
+ (typeof record.resultType === "string" ? record.resultType : undefined),
143
+ value: record
144
+ };
145
+ }
146
+ function summarizeRecordLine(record, index) {
147
+ const { kind, value } = unwrapSummaryRecord(record);
148
+ const title = pickString(value, ["title", "targetNodeTitle", "name", "workspaceName", "rootPath", "id", "nodeId"]);
149
+ const identifier = pickString(value, ["id", "nodeId", "targetNodeId"]);
150
+ const detail = pickString(value, ["summary", "reason", "body", "message", "staleReason", "workspaceRoot"]);
151
+ const parts = [`${index}.`];
152
+ if (kind) {
153
+ parts.push(`[${kind}]`);
154
+ }
155
+ if (title) {
156
+ parts.push(cleanInlineText(title, textTitleLength));
157
+ }
158
+ if (identifier && identifier !== title) {
159
+ parts.push(`(id: ${cleanInlineText(identifier, textTitleLength)})`);
160
+ }
161
+ let line = parts.join(" ");
162
+ if (detail) {
163
+ line += ` - ${cleanInlineText(detail)}`;
164
+ }
165
+ return line;
166
+ }
167
+ function formatItemsSummary(payload) {
168
+ const rawItems = payload.items;
169
+ if (!Array.isArray(rawItems)) {
170
+ return null;
171
+ }
172
+ const items = rawItems.filter(isRecord);
173
+ const shown = Math.min(items.length, textSummaryItemLimit);
174
+ const total = typeof payload.total === "number" ? payload.total : items.length;
175
+ const lines = [`Results: ${shown} shown of ${total} total.`];
176
+ if (!items.length) {
177
+ lines.push("No results.");
178
+ }
179
+ else {
180
+ for (const [index, item] of items.slice(0, textSummaryItemLimit).entries()) {
181
+ lines.push(summarizeRecordLine(item, index + 1));
182
+ }
183
+ }
184
+ if (total > shown) {
185
+ lines.push(`More available: ${total - shown} additional result(s).`);
186
+ }
187
+ else if (items.length > shown) {
188
+ lines.push(`More available: ${items.length - shown} additional item(s).`);
189
+ }
190
+ if (typeof payload.nextCursor === "string" && payload.nextCursor.trim()) {
191
+ lines.push(`Next cursor: ${cleanInlineText(payload.nextCursor, textTitleLength)}`);
192
+ }
193
+ return lines.join("\n");
194
+ }
195
+ function formatBundleSummary(payload) {
196
+ const bundle = isRecord(payload.bundle) ? payload.bundle : payload;
197
+ if (!isRecord(payload.bundle) && !isRecord(bundle.target)) {
198
+ return null;
199
+ }
200
+ const target = isRecord(bundle.target) ? bundle.target : null;
201
+ const type = target ? pickString(target, ["type"]) : undefined;
202
+ const mode = pickString(bundle, ["mode"]);
203
+ const preset = pickString(bundle, ["preset"]);
204
+ const summary = pickString(bundle, ["summary"]);
205
+ const items = Array.isArray(bundle.items) ? bundle.items.filter(isRecord) : [];
206
+ const decisions = Array.isArray(bundle.decisions) ? bundle.decisions : [];
207
+ const openQuestions = Array.isArray(bundle.openQuestions) ? bundle.openQuestions : [];
208
+ const activityDigest = Array.isArray(bundle.activityDigest) ? bundle.activityDigest : [];
209
+ const lines = [`Context bundle: Target${type ? ` [${type}]` : ""}.`];
210
+ if (mode || preset) {
211
+ lines.push(`Mode: ${[mode, preset].filter(Boolean).join(", ")}.`);
212
+ }
213
+ if (summary) {
214
+ lines.push(`Summary: ${cleanInlineText(summary)}`);
215
+ }
216
+ lines.push(`Items: ${items.length}.`);
217
+ for (const [index, item] of items.slice(0, textSummaryItemLimit).entries()) {
218
+ lines.push(summarizeRecordLine(item, index + 1));
219
+ }
220
+ if (items.length > textSummaryItemLimit) {
221
+ lines.push(`More bundle items: ${items.length - textSummaryItemLimit}.`);
222
+ }
223
+ if (activityDigest.length) {
224
+ lines.push(`Activity digest: ${activityDigest.length} entr${activityDigest.length === 1 ? "y" : "ies"}.`);
225
+ }
226
+ if (decisions.length) {
227
+ lines.push(`Decisions: ${decisions.length}.`);
228
+ }
229
+ if (openQuestions.length) {
230
+ lines.push(`Open questions: ${openQuestions.length}.`);
231
+ }
232
+ return lines.join("\n");
233
+ }
234
+ function formatWriteSummary(payload) {
235
+ const primaryKey = ["node", "activity", "relation"].find((key) => isRecord(payload[key]));
236
+ if (!primaryKey && !isRecord(payload.landing)) {
237
+ return null;
238
+ }
239
+ const lines = [];
240
+ if (primaryKey && isRecord(payload[primaryKey])) {
241
+ lines.push(`${capitalize(primaryKey)} stored: ${summarizeRecordLine(payload[primaryKey], 1).replace(/^1\.\s*/, "")}`);
242
+ }
243
+ if (isRecord(payload.landing)) {
244
+ const landing = payload.landing;
245
+ const parts = [
246
+ typeof landing.storedAs === "string" ? `storedAs=${landing.storedAs}` : null,
247
+ typeof landing.canonicality === "string" ? `canonicality=${landing.canonicality}` : null,
248
+ typeof landing.status === "string" ? `status=${landing.status}` : null,
249
+ typeof landing.governanceState === "string" ? `governance=${landing.governanceState}` : null
250
+ ].filter((value) => Boolean(value));
251
+ if (parts.length) {
252
+ lines.push(`Landing: ${parts.join(", ")}.`);
253
+ }
254
+ if (typeof landing.reason === "string" && landing.reason.trim()) {
255
+ lines.push(`Reason: ${cleanInlineText(landing.reason)}`);
256
+ }
257
+ }
258
+ return lines.length ? lines.join("\n") : null;
259
+ }
260
+ function formatObjectSummary(payload) {
261
+ const preferredKeys = [
262
+ "status",
263
+ "message",
264
+ "workspaceName",
265
+ "workspaceRoot",
266
+ "rootPath",
267
+ "schemaVersion",
268
+ "authMode",
269
+ "bindAddress",
270
+ "queued",
271
+ "queuedCount",
272
+ "nodeId",
273
+ "id",
274
+ "title",
275
+ "type",
276
+ "summary",
277
+ "reason",
278
+ "nextCursor"
279
+ ];
280
+ const orderedKeys = Array.from(new Set([...preferredKeys.filter((key) => key in payload), ...Object.keys(payload).filter((key) => !preferredKeys.includes(key))]));
281
+ const lines = [];
282
+ for (const key of orderedKeys) {
283
+ const summary = summarizeValue(payload[key]);
284
+ if (!summary) {
285
+ continue;
286
+ }
287
+ lines.push(`${formatKeyLabel(key)}: ${summary}`);
288
+ if (lines.length >= 8) {
289
+ break;
290
+ }
291
+ }
292
+ return lines.length ? lines.join("\n") : "Structured response returned.";
293
+ }
294
+ function formatArraySummary(items) {
295
+ const recordItems = items.filter(isRecord);
296
+ if (!recordItems.length) {
297
+ return `Items: ${items.length}.`;
298
+ }
299
+ const lines = [`Items: ${recordItems.length}.`];
300
+ for (const [index, item] of recordItems.slice(0, textSummaryItemLimit).entries()) {
301
+ lines.push(summarizeRecordLine(item, index + 1));
302
+ }
303
+ if (recordItems.length > textSummaryItemLimit) {
304
+ lines.push(`More available: ${recordItems.length - textSummaryItemLimit} additional item(s).`);
305
+ }
306
+ return lines.join("\n");
307
+ }
67
308
  function formatStructuredContent(content) {
68
- return JSON.stringify(content, null, 2);
309
+ if (Array.isArray(content)) {
310
+ return formatArraySummary(content);
311
+ }
312
+ if (isRecord(content)) {
313
+ return formatBundleSummary(content) ?? formatItemsSummary(content) ?? formatWriteSummary(content) ?? formatObjectSummary(content);
314
+ }
315
+ if (typeof content === "string" && content.trim()) {
316
+ return cleanInlineText(content);
317
+ }
318
+ if (typeof content === "number" || typeof content === "boolean") {
319
+ return String(content);
320
+ }
321
+ return "Structured response returned.";
322
+ }
323
+ function renderToolText(_toolName, structuredContent) {
324
+ return formatStructuredContent(structuredContent);
69
325
  }
70
326
  function formatInvalidBundleModeMessage(input) {
71
327
  const quotedInput = typeof input === "string" && input.trim() ? `'${input}'` : "that value";
@@ -85,12 +341,12 @@ function bundlePresetSchema(defaultValue) {
85
341
  error: (issue) => formatInvalidBundlePresetMessage(issue.input)
86
342
  })).default(defaultValue);
87
343
  }
88
- function toolResult(structuredContent) {
344
+ function toolResult(toolName, structuredContent) {
89
345
  return {
90
346
  content: [
91
347
  {
92
348
  type: "text",
93
- text: formatStructuredContent(structuredContent)
349
+ text: renderToolText(toolName, structuredContent)
94
350
  }
95
351
  ],
96
352
  structuredContent
@@ -133,14 +389,14 @@ const readOnlyToolAnnotations = {
133
389
  readOnlyHint: true,
134
390
  idempotentHint: true
135
391
  };
136
- function createGetToolHandler(apiClient, path) {
137
- return async () => toolResult(await apiClient.get(path));
392
+ function createGetToolHandler(toolName, apiClient, path) {
393
+ return async () => toolResult(toolName, await apiClient.get(path));
138
394
  }
139
- function createPostToolHandler(apiClient, path) {
140
- return async (input) => toolResult(await apiClient.post(path, input));
395
+ function createPostToolHandler(toolName, apiClient, path) {
396
+ return async (input) => toolResult(toolName, await apiClient.post(path, input));
141
397
  }
142
- function createNormalizedPostToolHandler(apiClient, path, normalize) {
143
- return async (input) => toolResult(await apiClient.post(path, normalize(input)));
398
+ function createNormalizedPostToolHandler(toolName, apiClient, path, normalize) {
399
+ return async (input) => toolResult(toolName, await apiClient.post(path, normalize(input)));
144
400
  }
145
401
  function withReadOnlyAnnotations(config) {
146
402
  return {
@@ -312,7 +568,7 @@ export function createRecallXMcpServer(params) {
312
568
  workspaceRoot: process.cwd(),
313
569
  workspaceName: "RecallX MCP",
314
570
  retentionDays: 14,
315
- slowRequestMs: 250,
571
+ slowRequestMs: 50,
316
572
  capturePayloadShape: true
317
573
  };
318
574
  let currentObservabilityState = params?.observabilityState ?? defaultObservabilityState;
@@ -335,7 +591,7 @@ export function createRecallXMcpServer(params) {
335
591
  name: "recallx-mcp",
336
592
  version: params?.serverVersion ?? RECALLX_VERSION
337
593
  }, {
338
- instructions: "Use RecallX as a local knowledge backend. Treat the current workspace as the default scope, and do not create or open another workspace unless the user explicitly asks. When the work is clearly project-shaped, search for an existing project inside the current workspace first: prefer recallx_search_nodes with type=project, broaden with recallx_search_workspace when needed, create a project node only if no suitable one exists, and then anchor follow-up context with recallx_context_bundle targetId. Once a project is known, do not keep writing untargeted workspace captures for routine work logs: append activity to that project or pass targetNodeId on capture writes. Reserve workspace-scope inbox activity for genuinely untargeted, cross-project, or not-yet-classified short logs. If the conversation is not project-specific, keep memory at workspace scope. Prefer read tools first, and include source details on durable writes when you want caller-specific provenance.",
594
+ instructions: "Use RecallX as the default local memory layer for meaningful work, not as an afterthought. Treat the current workspace as the default scope, and do not create or open another workspace unless the user explicitly asks. Before making assumptions or starting a meaningful task, read context first: confirm the active workspace, use recallx_search_workspace as the broad default when the target is still unclear, narrow with recallx_search_nodes or recallx_search_activities when needed, and build a compact recallx_context_bundle before deep execution when a node or project is known. When the work is clearly project-shaped, search for an existing project inside the current workspace first, create one only if no suitable project exists, and then anchor follow-up context and routine logs to that project. Once a project is known, do not keep writing untargeted workspace captures for routine work logs: append activity to that project or pass targetNodeId on capture writes. Reserve workspace-scope inbox activity for genuinely untargeted, cross-project, or not-yet-classified short logs. Prefer read tools before durable writes, prefer compact context over repeated broad browsing, and write back concise summaries, decisions, or feedback when RecallX materially helped the task. Include source details on durable writes when you want caller-specific provenance.",
339
595
  capabilities: {
340
596
  logging: {}
341
597
  }
@@ -388,12 +644,12 @@ export function createRecallXMcpServer(params) {
388
644
  includeDetails: z.boolean().optional().default(true)
389
645
  }),
390
646
  outputSchema: healthOutputSchema
391
- }, async () => toolResult(await apiClient.get("/health")));
647
+ }, async () => toolResult("recallx_health", await apiClient.get("/health")));
392
648
  registerReadOnlyTool(server, "recallx_workspace_current", {
393
649
  title: "Current Workspace",
394
650
  description: "Read the currently active RecallX workspace and auth mode. Use this to confirm the default workspace scope before deciding whether an explicit user request justifies switching workspaces.",
395
651
  outputSchema: workspaceInfoSchema
396
- }, createGetToolHandler(apiClient, "/workspace"));
652
+ }, createGetToolHandler("recallx_workspace_current", apiClient, "/workspace"));
397
653
  registerReadOnlyTool(server, "recallx_workspace_list", {
398
654
  title: "List Workspaces",
399
655
  description: "List known RecallX workspaces and identify the currently active one.",
@@ -401,7 +657,7 @@ export function createRecallXMcpServer(params) {
401
657
  current: workspaceInfoSchema,
402
658
  items: z.array(workspaceInfoSchema.extend({ isCurrent: z.boolean(), lastOpenedAt: z.string() }))
403
659
  })
404
- }, createGetToolHandler(apiClient, "/workspaces"));
660
+ }, createGetToolHandler("recallx_workspace_list", apiClient, "/workspaces"));
405
661
  registerTool(server, "recallx_workspace_create", {
406
662
  title: "Create Workspace",
407
663
  description: "Create a RecallX workspace on disk and switch the running service to it without restarting. Only use this when the user explicitly requests creating or switching to a new workspace.",
@@ -409,14 +665,14 @@ export function createRecallXMcpServer(params) {
409
665
  rootPath: z.string().min(1).describe("Absolute or user-resolved path for the new workspace root."),
410
666
  workspaceName: z.string().min(1).optional().describe("Human-friendly workspace name.")
411
667
  }
412
- }, createPostToolHandler(apiClient, "/workspaces"));
668
+ }, createPostToolHandler("recallx_workspace_create", apiClient, "/workspaces"));
413
669
  registerTool(server, "recallx_workspace_open", {
414
670
  title: "Open Workspace",
415
671
  description: "Switch the running RecallX service to another existing workspace. Only use this when the user explicitly requests opening or switching workspaces.",
416
672
  inputSchema: {
417
673
  rootPath: z.string().min(1).describe("Existing workspace root path to open.")
418
674
  }
419
- }, createPostToolHandler(apiClient, "/workspaces/open"));
675
+ }, createPostToolHandler("recallx_workspace_open", apiClient, "/workspaces/open"));
420
676
  registerReadOnlyTool(server, "recallx_semantic_status", {
421
677
  title: "Semantic Index Status",
422
678
  description: "Read the current semantic indexing status, provider configuration, and queued item counts.",
@@ -434,7 +690,7 @@ export function createRecallXMcpServer(params) {
434
690
  failed: z.number()
435
691
  })
436
692
  })
437
- }, createGetToolHandler(apiClient, "/semantic/status"));
693
+ }, createGetToolHandler("recallx_semantic_status", apiClient, "/semantic/status"));
438
694
  registerReadOnlyTool(server, "recallx_semantic_issues", {
439
695
  title: "Semantic Index Issues",
440
696
  description: "Read semantic indexing issues with optional status filters and cursor pagination.",
@@ -462,7 +718,7 @@ export function createRecallXMcpServer(params) {
462
718
  if (statuses?.length) {
463
719
  params.set("statuses", statuses.join(","));
464
720
  }
465
- return toolResult(await apiClient.get(`/semantic/issues?${params.toString()}`));
721
+ return toolResult("recallx_semantic_issues", await apiClient.get(`/semantic/issues?${params.toString()}`));
466
722
  });
467
723
  registerReadOnlyTool(server, "recallx_search_nodes", {
468
724
  title: "Search Nodes",
@@ -486,7 +742,7 @@ export function createRecallXMcpServer(params) {
486
742
  offset: coerceIntegerSchema(0, 0, 10_000),
487
743
  sort: z.enum(["relevance", "updated_at"]).default("relevance")
488
744
  }
489
- }, createNormalizedPostToolHandler(apiClient, "/nodes/search", normalizeNodeSearchInput));
745
+ }, createNormalizedPostToolHandler("recallx_search_nodes", apiClient, "/nodes/search", normalizeNodeSearchInput));
490
746
  registerReadOnlyTool(server, "recallx_search_activities", {
491
747
  title: "Search Activities",
492
748
  description: "Search operational activity timelines by keyword and optional filters. Prefer this for recent logs, change history, and 'what happened recently' questions. Accepts `activityType` and `targetNodeId` aliases and normalizes single strings into arrays.",
@@ -508,7 +764,7 @@ export function createRecallXMcpServer(params) {
508
764
  offset: coerceIntegerSchema(0, 0, 10_000),
509
765
  sort: z.enum(["relevance", "updated_at"]).default("relevance")
510
766
  }
511
- }, createNormalizedPostToolHandler(apiClient, "/activities/search", normalizeActivitySearchInput));
767
+ }, createNormalizedPostToolHandler("recallx_search_activities", apiClient, "/activities/search", normalizeActivitySearchInput));
512
768
  registerReadOnlyTool(server, "recallx_search_workspace", {
513
769
  title: "Search Workspace",
514
770
  description: "Search nodes, activities, or both through one workspace-wide endpoint. This is the preferred broad entry point when the target node or request shape is still unclear, or when you need both node and activity recall in the current workspace. Use `scopes` as an array such as `[\"nodes\", \"activities\"]`, or use `scope: \"activities\"` for a single scope. Do not pass a comma-separated string like `\"nodes,activities\"`.",
@@ -538,14 +794,14 @@ export function createRecallXMcpServer(params) {
538
794
  offset: coerceIntegerSchema(0, 0, 10_000),
539
795
  sort: z.enum(["relevance", "updated_at", "smart"]).default("relevance")
540
796
  }
541
- }, createNormalizedPostToolHandler(apiClient, "/search", normalizeWorkspaceSearchInput));
797
+ }, createNormalizedPostToolHandler("recallx_search_workspace", apiClient, "/search", normalizeWorkspaceSearchInput));
542
798
  registerReadOnlyTool(server, "recallx_get_node", {
543
799
  title: "Get Node",
544
800
  description: "Fetch a node together with its related nodes, activities, artifacts, and provenance.",
545
801
  inputSchema: {
546
802
  nodeId: z.string().min(1).describe("Target node id.")
547
803
  }
548
- }, async ({ nodeId }) => toolResult(await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}`)));
804
+ }, async ({ nodeId }) => toolResult("recallx_get_node", await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}`)));
549
805
  registerReadOnlyTool(server, "recallx_get_related", {
550
806
  title: "Get Node Neighborhood",
551
807
  description: "Fetch the canonical RecallX node neighborhood with optional inferred relations.",
@@ -565,7 +821,7 @@ export function createRecallXMcpServer(params) {
565
821
  if (relationTypeFilter.length) {
566
822
  query.set("types", relationTypeFilter.join(","));
567
823
  }
568
- return toolResult(await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}/neighborhood?${query.toString()}`));
824
+ return toolResult("recallx_get_related", await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}/neighborhood?${query.toString()}`));
569
825
  });
570
826
  registerTool(server, "recallx_upsert_inferred_relation", {
571
827
  title: "Upsert Inferred Relation",
@@ -583,7 +839,7 @@ export function createRecallXMcpServer(params) {
583
839
  expiresAt: z.string().optional(),
584
840
  metadata: jsonRecordSchema
585
841
  }
586
- }, createPostToolHandler(apiClient, "/inferred-relations"));
842
+ }, createPostToolHandler("recallx_upsert_inferred_relation", apiClient, "/inferred-relations"));
587
843
  registerTool(server, "recallx_append_relation_usage_event", {
588
844
  title: "Append Relation Usage Event",
589
845
  description: "Append a lightweight usage signal after a relation actually helped retrieval or final output.",
@@ -597,7 +853,7 @@ export function createRecallXMcpServer(params) {
597
853
  delta: z.number(),
598
854
  metadata: jsonRecordSchema
599
855
  }
600
- }, createPostToolHandler(apiClient, "/relation-usage-events"));
856
+ }, createPostToolHandler("recallx_append_relation_usage_event", apiClient, "/relation-usage-events"));
601
857
  registerTool(server, "recallx_append_search_feedback", {
602
858
  title: "Append Search Feedback",
603
859
  description: "Append a usefulness signal for a node or activity search result after it helped or failed a task.",
@@ -612,7 +868,7 @@ export function createRecallXMcpServer(params) {
612
868
  confidence: z.number().min(0).max(1).default(1),
613
869
  metadata: jsonRecordSchema
614
870
  }
615
- }, createPostToolHandler(apiClient, "/search-feedback-events"));
871
+ }, createPostToolHandler("recallx_append_search_feedback", apiClient, "/search-feedback-events"));
616
872
  registerTool(server, "recallx_recompute_inferred_relations", {
617
873
  title: "Recompute Inferred Relations",
618
874
  description: "Run an explicit maintenance pass that refreshes inferred relation usage_score and final_score from usage events.",
@@ -621,7 +877,7 @@ export function createRecallXMcpServer(params) {
621
877
  generator: z.string().min(1).optional(),
622
878
  limit: z.number().int().min(1).max(500).default(100)
623
879
  }
624
- }, createPostToolHandler(apiClient, "/inferred-relations/recompute"));
880
+ }, createPostToolHandler("recallx_recompute_inferred_relations", apiClient, "/inferred-relations/recompute"));
625
881
  registerTool(server, "recallx_append_activity", {
626
882
  title: "Append Activity",
627
883
  description: "Append an activity entry to a specific RecallX node or project timeline with provenance. Use this when you already know the target node or project; otherwise prefer recallx_capture_memory for general workspace-scope updates.",
@@ -632,7 +888,7 @@ export function createRecallXMcpServer(params) {
632
888
  source: sourceSchema,
633
889
  metadata: jsonRecordSchema
634
890
  }
635
- }, createPostToolHandler(apiClient, "/activities"));
891
+ }, createPostToolHandler("recallx_append_activity", apiClient, "/activities"));
636
892
  registerTool(server, "recallx_capture_memory", {
637
893
  title: "Capture Memory",
638
894
  description: "Safely capture a memory item without choosing low-level storage first. Prefer this as the default write only when the conversation is not yet tied to a specific project or node. Once a project or target node is known, include targetNodeId or switch to recallx_append_activity for routine work logs. General short logs can stay at workspace scope and be auto-routed into activities, while reusable content can still land as durable memory.",
@@ -646,7 +902,7 @@ export function createRecallXMcpServer(params) {
646
902
  source: sourceSchema,
647
903
  metadata: jsonRecordSchema
648
904
  }
649
- }, createPostToolHandler(apiClient, "/capture"));
905
+ }, createPostToolHandler("recallx_capture_memory", apiClient, "/capture"));
650
906
  registerTool(server, "recallx_create_node", {
651
907
  title: "Create Node",
652
908
  description: "Create a durable RecallX node with provenance. Use this for reusable knowledge; when creating a project node in the current workspace, search first and only create one if no suitable project already exists. Short work-log updates are usually better captured with `recallx_capture_memory` or `recallx_append_activity`.",
@@ -663,7 +919,7 @@ export function createRecallXMcpServer(params) {
663
919
  }
664
920
  }, async (input) => {
665
921
  try {
666
- return toolResult(await apiClient.post("/nodes", input));
922
+ return toolResult("recallx_create_node", await apiClient.post("/nodes", input));
667
923
  }
668
924
  catch (error) {
669
925
  if (error instanceof RecallXApiError &&
@@ -695,7 +951,7 @@ export function createRecallXMcpServer(params) {
695
951
  .min(1)
696
952
  .max(100)
697
953
  }
698
- }, async (input) => toolResult(await apiClient.post("/nodes/batch", input)));
954
+ }, async (input) => toolResult("recallx_create_nodes", await apiClient.post("/nodes/batch", input)));
699
955
  registerTool(server, "recallx_create_relation", {
700
956
  title: "Create Relation",
701
957
  description: "Create a relation between two nodes. Agent-created relations typically start suggested and are promoted automatically when confidence improves.",
@@ -707,7 +963,7 @@ export function createRecallXMcpServer(params) {
707
963
  source: sourceSchema,
708
964
  metadata: jsonRecordSchema
709
965
  }
710
- }, createPostToolHandler(apiClient, "/relations"));
966
+ }, createPostToolHandler("recallx_create_relation", apiClient, "/relations"));
711
967
  registerReadOnlyTool(server, "recallx_list_governance_issues", {
712
968
  title: "List Governance Issues",
713
969
  description: "List contested or low-confidence governance items that may need inspection.",
@@ -720,7 +976,7 @@ export function createRecallXMcpServer(params) {
720
976
  states: states.join(","),
721
977
  limit: String(limit)
722
978
  });
723
- return toolResult(await apiClient.get(`/governance/issues?${query.toString()}`));
979
+ return toolResult("recallx_list_governance_issues", await apiClient.get(`/governance/issues?${query.toString()}`));
724
980
  });
725
981
  registerReadOnlyTool(server, "recallx_get_governance_state", {
726
982
  title: "Get Governance State",
@@ -729,7 +985,7 @@ export function createRecallXMcpServer(params) {
729
985
  entityType: z.enum(["node", "relation"]),
730
986
  entityId: z.string().min(1)
731
987
  }
732
- }, async ({ entityType, entityId }) => toolResult(await apiClient.get(`/governance/state/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`)));
988
+ }, async ({ entityType, entityId }) => toolResult("recallx_get_governance_state", await apiClient.get(`/governance/state/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`)));
733
989
  registerTool(server, "recallx_recompute_governance", {
734
990
  title: "Recompute Governance",
735
991
  description: "Run a bounded automatic governance recompute pass for nodes, relations, or both.",
@@ -738,7 +994,7 @@ export function createRecallXMcpServer(params) {
738
994
  entityIds: z.array(z.string().min(1)).max(200).optional(),
739
995
  limit: z.number().int().min(1).max(500).default(100)
740
996
  }
741
- }, createPostToolHandler(apiClient, "/governance/recompute"));
997
+ }, createPostToolHandler("recallx_recompute_governance", apiClient, "/governance/recompute"));
742
998
  registerReadOnlyTool(server, "recallx_context_bundle", {
743
999
  title: "Build Context Bundle",
744
1000
  description: "Build a compact RecallX context bundle for coding, research, writing, or decision support. Omit targetId to get a workspace-entry bundle when the work is not yet tied to a specific project or node, and add targetId only after you know which project or node should anchor the context.",
@@ -766,7 +1022,7 @@ export function createRecallXMcpServer(params) {
766
1022
  maxItems: 10
767
1023
  })
768
1024
  }
769
- }, async ({ targetId, ...input }) => toolResult(await apiClient.post("/context/bundles", {
1025
+ }, async ({ targetId, ...input }) => toolResult("recallx_context_bundle", await apiClient.post("/context/bundles", {
770
1026
  ...input,
771
1027
  ...(targetId
772
1028
  ? {
@@ -782,14 +1038,14 @@ export function createRecallXMcpServer(params) {
782
1038
  inputSchema: {
783
1039
  limit: coerceIntegerSchema(250, 1, 1000)
784
1040
  }
785
- }, createPostToolHandler(apiClient, "/semantic/reindex"));
1041
+ }, createPostToolHandler("recallx_semantic_reindex", apiClient, "/semantic/reindex"));
786
1042
  registerTool(server, "recallx_semantic_reindex_node", {
787
1043
  title: "Queue Node Semantic Reindex",
788
1044
  description: "Queue semantic reindexing for a specific node id.",
789
1045
  inputSchema: {
790
1046
  nodeId: z.string().min(1)
791
1047
  }
792
- }, async ({ nodeId }) => toolResult(await apiClient.post(`/semantic/reindex/${encodeURIComponent(nodeId)}`, {})));
1048
+ }, async ({ nodeId }) => toolResult("recallx_semantic_reindex_node", await apiClient.post(`/semantic/reindex/${encodeURIComponent(nodeId)}`, {})));
793
1049
  registerReadOnlyTool(server, "recallx_rank_candidates", {
794
1050
  title: "Rank Candidate Nodes",
795
1051
  description: "Rank a bounded set of candidate node ids for a target using RecallX request-time retrieval scoring.",
@@ -799,6 +1055,6 @@ export function createRecallXMcpServer(params) {
799
1055
  preset: bundlePresetSchema("for-assistant"),
800
1056
  targetNodeId: z.string().optional()
801
1057
  }
802
- }, createPostToolHandler(apiClient, "/retrieval/rank-candidates"));
1058
+ }, createPostToolHandler("recallx_rank_candidates", apiClient, "/retrieval/rank-candidates"));
803
1059
  return server;
804
1060
  }
package/app/server/app.js CHANGED
@@ -546,7 +546,7 @@ export function createRecallXApp(params) {
546
546
  workspaceRoot: currentWorkspaceRoot(),
547
547
  workspaceName: currentWorkspaceInfo().workspaceName,
548
548
  retentionDays: Math.max(1, parseNumberSetting(settings["observability.retentionDays"], 14)),
549
- slowRequestMs: Math.max(1, parseNumberSetting(settings["observability.slowRequestMs"], 250)),
549
+ slowRequestMs: Math.max(1, parseNumberSetting(settings["observability.slowRequestMs"], 50)),
550
550
  capturePayloadShape: parseBooleanSetting(settings["observability.capturePayloadShape"], true)
551
551
  };
552
552
  };
@@ -747,7 +747,9 @@ export class ObservabilityWriter {
747
747
  generatedAt: nowIso(),
748
748
  logsPath,
749
749
  totalEvents: events.length,
750
+ slowRequestThresholdMs: state.slowRequestMs,
750
751
  operationSummaries,
752
+ hotOperations: operationSummaries.slice(0, 10),
751
753
  slowOperations: operationSummaries
752
754
  .filter((item) => (item.p95DurationMs ?? 0) >= state.slowRequestMs)
753
755
  .slice(0, 10),