recallx 1.0.8 → 1.2.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 +52 -1
- package/app/cli/src/cli.js +32 -1
- package/app/cli/src/format.js +24 -0
- package/app/mcp/index.js +2 -2
- package/app/mcp/server.js +523 -146
- package/app/server/app.js +413 -5
- package/app/server/config.js +4 -2
- package/app/server/index.js +12 -1
- package/app/server/observability.js +2 -0
- package/app/server/project-graph.js +13 -6
- package/app/server/repositories.js +178 -24
- package/app/server/sqlite-errors.js +10 -0
- package/app/server/workspace-import-helpers.js +161 -0
- package/app/server/workspace-import.js +572 -0
- package/app/server/workspace-ops.js +249 -0
- package/app/server/workspace-session.js +119 -7
- package/app/shared/contracts.js +41 -0
- package/app/shared/version.js +1 -1
- package/dist/renderer/assets/{ProjectGraphCanvas-BLmjIT0R.js → ProjectGraphCanvas-B9-L83dL.js} +1 -1
- package/dist/renderer/assets/index-CNeaY_5l.js +69 -0
- package/dist/renderer/assets/index-Dz33nPCb.css +1 -0
- package/dist/renderer/index.html +2 -2
- package/package.json +1 -1
- package/dist/renderer/assets/index-C2-KXqBO.css +0 -1
- package/dist/renderer/assets/index-CIY8bKYQ.js +0 -69
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
|
-
|
|
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:
|
|
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:
|
|
571
|
+
slowRequestMs: 50,
|
|
316
572
|
capturePayloadShape: true
|
|
317
573
|
};
|
|
318
574
|
let currentObservabilityState = params?.observabilityState ?? defaultObservabilityState;
|
|
@@ -331,11 +587,55 @@ export function createRecallXMcpServer(params) {
|
|
|
331
587
|
const observability = createObservabilityWriter({
|
|
332
588
|
getState: () => currentObservabilityState
|
|
333
589
|
});
|
|
590
|
+
// Session-level feedback tracking for automatic signal collection.
|
|
591
|
+
// Tracks which node IDs appeared in read results so that after a write we can
|
|
592
|
+
// auto-append search/relation feedback for items that were actually useful.
|
|
593
|
+
const sessionFeedback = {
|
|
594
|
+
recentSearches: [],
|
|
595
|
+
recentBundles: [],
|
|
596
|
+
runId: `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
597
|
+
trackSearch(query, resultIds, resultType) {
|
|
598
|
+
this.recentSearches.push({ query, resultIds, resultType });
|
|
599
|
+
if (this.recentSearches.length > 50)
|
|
600
|
+
this.recentSearches.shift();
|
|
601
|
+
},
|
|
602
|
+
trackBundle(targetId, itemIds) {
|
|
603
|
+
this.recentBundles.push({ targetId, itemIds });
|
|
604
|
+
if (this.recentBundles.length > 50)
|
|
605
|
+
this.recentBundles.shift();
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
/**
|
|
609
|
+
* Try to auto-append feedback for search results that led to successful reads.
|
|
610
|
+
* Called after a successful write tool (create_node, append_activity, capture_memory).
|
|
611
|
+
*/
|
|
612
|
+
async function autoAppendSearchFeedback() {
|
|
613
|
+
// Only append for the most recent search that had results. Avoid spam by
|
|
614
|
+
// checking if we already appended feedback for the same search in this session.
|
|
615
|
+
const lastSearch = sessionFeedback.recentSearches.at(-1);
|
|
616
|
+
if (!lastSearch || !lastSearch.resultIds.length)
|
|
617
|
+
return;
|
|
618
|
+
try {
|
|
619
|
+
await apiClient.post("/search-feedback-events", {
|
|
620
|
+
resultType: lastSearch.resultType,
|
|
621
|
+
resultId: lastSearch.resultIds[0],
|
|
622
|
+
verdict: "useful",
|
|
623
|
+
query: lastSearch.query,
|
|
624
|
+
sessionId: sessionFeedback.runId,
|
|
625
|
+
confidence: 0.7
|
|
626
|
+
});
|
|
627
|
+
// Remove the search so we don't append again for the same search.
|
|
628
|
+
sessionFeedback.recentSearches.pop();
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
// Feedback append is best-effort — don't break the main tool call.
|
|
632
|
+
}
|
|
633
|
+
}
|
|
334
634
|
const server = new McpServer({
|
|
335
635
|
name: "recallx-mcp",
|
|
336
636
|
version: params?.serverVersion ?? RECALLX_VERSION
|
|
337
637
|
}, {
|
|
338
|
-
instructions: "Use RecallX as
|
|
638
|
+
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. Feedback signals (search usefulness, relation usefulness) are automatically recorded on your behalf — you do NOT need to call feedback tools manually.",
|
|
339
639
|
capabilities: {
|
|
340
640
|
logging: {}
|
|
341
641
|
}
|
|
@@ -388,20 +688,26 @@ export function createRecallXMcpServer(params) {
|
|
|
388
688
|
includeDetails: z.boolean().optional().default(true)
|
|
389
689
|
}),
|
|
390
690
|
outputSchema: healthOutputSchema
|
|
391
|
-
}, async () => toolResult(await apiClient.get("/health")));
|
|
392
|
-
registerReadOnlyTool(server, "
|
|
393
|
-
title: "
|
|
394
|
-
description: "Read the
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
title: "List Workspaces",
|
|
399
|
-
description: "List known RecallX workspaces and identify the currently active one.",
|
|
691
|
+
}, async () => toolResult("recallx_health", await apiClient.get("/health")));
|
|
692
|
+
registerReadOnlyTool(server, "recallx_workspace_info", {
|
|
693
|
+
title: "Workspace Information",
|
|
694
|
+
description: "Read the active RecallX workspace and optionally list all known workspaces in one call. **When to use:** at the start of any task to confirm scope. Do not create or open another workspace unless the user explicitly asks.",
|
|
695
|
+
inputSchema: {
|
|
696
|
+
includeList: coerceBooleanSchema(false).describe("Set true to also return all known workspaces alongside the active one.")
|
|
697
|
+
},
|
|
400
698
|
outputSchema: z.object({
|
|
401
699
|
current: workspaceInfoSchema,
|
|
402
|
-
items: z.array(workspaceInfoSchema.extend({ isCurrent: z.boolean(), lastOpenedAt: z.string() }))
|
|
700
|
+
items: z.array(workspaceInfoSchema.extend({ isCurrent: z.boolean(), lastOpenedAt: z.string() })).optional()
|
|
403
701
|
})
|
|
404
|
-
},
|
|
702
|
+
}, async ({ includeList }) => {
|
|
703
|
+
const current = await apiClient.get("/workspace");
|
|
704
|
+
const result = { current: current };
|
|
705
|
+
if (includeList) {
|
|
706
|
+
const list = await apiClient.get("/workspaces");
|
|
707
|
+
result.items = (list.items ?? []);
|
|
708
|
+
}
|
|
709
|
+
return toolResult("recallx_workspace_info", result);
|
|
710
|
+
});
|
|
405
711
|
registerTool(server, "recallx_workspace_create", {
|
|
406
712
|
title: "Create Workspace",
|
|
407
713
|
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,17 +715,22 @@ export function createRecallXMcpServer(params) {
|
|
|
409
715
|
rootPath: z.string().min(1).describe("Absolute or user-resolved path for the new workspace root."),
|
|
410
716
|
workspaceName: z.string().min(1).optional().describe("Human-friendly workspace name.")
|
|
411
717
|
}
|
|
412
|
-
}, createPostToolHandler(apiClient, "/workspaces"));
|
|
718
|
+
}, createPostToolHandler("recallx_workspace_create", apiClient, "/workspaces"));
|
|
413
719
|
registerTool(server, "recallx_workspace_open", {
|
|
414
720
|
title: "Open Workspace",
|
|
415
721
|
description: "Switch the running RecallX service to another existing workspace. Only use this when the user explicitly requests opening or switching workspaces.",
|
|
416
722
|
inputSchema: {
|
|
417
723
|
rootPath: z.string().min(1).describe("Existing workspace root path to open.")
|
|
418
724
|
}
|
|
419
|
-
}, createPostToolHandler(apiClient, "/workspaces/open"));
|
|
420
|
-
registerReadOnlyTool(server, "
|
|
421
|
-
title: "Semantic
|
|
422
|
-
description: "Read
|
|
725
|
+
}, createPostToolHandler("recallx_workspace_open", apiClient, "/workspaces/open"));
|
|
726
|
+
registerReadOnlyTool(server, "recallx_semantic_overview", {
|
|
727
|
+
title: "Semantic Overview",
|
|
728
|
+
description: "Read semantic index status, counts, and optionally active issues in one call. **When to use:** during workspace health checks or when search results seem unexpectedly stale. Not needed for routine coding tasks.",
|
|
729
|
+
inputSchema: {
|
|
730
|
+
includeIssues: coerceBooleanSchema(false).describe("Set true to also return recent semantic indexing issues."),
|
|
731
|
+
issueLimit: coerceIntegerSchema(5, 1, 25).describe("Max issue items when includeIssues is true."),
|
|
732
|
+
issueStatuses: z.array(z.enum(["pending", "stale", "failed"])).max(3).optional().describe("Issue statuses to include.")
|
|
733
|
+
},
|
|
423
734
|
outputSchema: z.object({
|
|
424
735
|
enabled: z.boolean(),
|
|
425
736
|
provider: z.string().nullable(),
|
|
@@ -432,37 +743,30 @@ export function createRecallXMcpServer(params) {
|
|
|
432
743
|
stale: z.number(),
|
|
433
744
|
ready: z.number(),
|
|
434
745
|
failed: z.number()
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
}, createGetToolHandler(apiClient, "/semantic/status"));
|
|
438
|
-
registerReadOnlyTool(server, "recallx_semantic_issues", {
|
|
439
|
-
title: "Semantic Index Issues",
|
|
440
|
-
description: "Read semantic indexing issues with optional status filters and cursor pagination.",
|
|
441
|
-
inputSchema: {
|
|
442
|
-
limit: coerceIntegerSchema(5, 1, 25).describe("Maximum number of semantic issue items to return."),
|
|
443
|
-
cursor: z.string().min(1).optional().describe("Opaque cursor from a previous semantic issues call."),
|
|
444
|
-
statuses: z.array(z.enum(["pending", "stale", "failed"])).max(3).optional().describe("Optional issue statuses to include.")
|
|
445
|
-
},
|
|
446
|
-
outputSchema: z.object({
|
|
447
|
-
items: z.array(z.object({
|
|
746
|
+
}),
|
|
747
|
+
issues: z.array(z.object({
|
|
448
748
|
nodeId: z.string(),
|
|
449
749
|
title: z.string().nullable(),
|
|
450
750
|
embeddingStatus: z.enum(["pending", "processing", "stale", "ready", "failed"]),
|
|
451
751
|
staleReason: z.string().nullable(),
|
|
452
752
|
updatedAt: z.string()
|
|
453
|
-
})),
|
|
454
|
-
nextCursor: z.string().nullable()
|
|
753
|
+
})).optional(),
|
|
754
|
+
nextCursor: z.string().nullable().optional()
|
|
455
755
|
})
|
|
456
|
-
}, async ({
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
if (
|
|
460
|
-
params
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
756
|
+
}, async ({ includeIssues, issueLimit, issueStatuses }) => {
|
|
757
|
+
const status = await apiClient.get("/semantic/status");
|
|
758
|
+
const result = { ...status };
|
|
759
|
+
if (includeIssues) {
|
|
760
|
+
const params = new URLSearchParams();
|
|
761
|
+
params.set("limit", String(issueLimit));
|
|
762
|
+
if (issueStatuses?.length) {
|
|
763
|
+
params.set("statuses", issueStatuses.join(","));
|
|
764
|
+
}
|
|
765
|
+
const issuesPayload = await apiClient.get(`/semantic/issues?${params.toString()}`);
|
|
766
|
+
result.issues = (issuesPayload.items ?? []);
|
|
767
|
+
result.nextCursor = typeof issuesPayload.nextCursor === "string" ? issuesPayload.nextCursor : null;
|
|
464
768
|
}
|
|
465
|
-
return toolResult(
|
|
769
|
+
return toolResult("recallx_semantic_overview", result);
|
|
466
770
|
});
|
|
467
771
|
registerReadOnlyTool(server, "recallx_search_nodes", {
|
|
468
772
|
title: "Search Nodes",
|
|
@@ -486,7 +790,14 @@ export function createRecallXMcpServer(params) {
|
|
|
486
790
|
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
487
791
|
sort: z.enum(["relevance", "updated_at"]).default("relevance")
|
|
488
792
|
}
|
|
489
|
-
},
|
|
793
|
+
}, async (input) => {
|
|
794
|
+
const result = await apiClient.post("/nodes/search", normalizeNodeSearchInput(input));
|
|
795
|
+
const items = Array.isArray(result.items) ? result.items : [];
|
|
796
|
+
const ids = items.filter((item) => isRecord(item) && typeof item.id === "string").map((item) => item.id);
|
|
797
|
+
const query = typeof input.query === "string" ? input.query : "";
|
|
798
|
+
sessionFeedback.trackSearch(query, ids, "node");
|
|
799
|
+
return toolResult("recallx_search_nodes", result);
|
|
800
|
+
});
|
|
490
801
|
registerReadOnlyTool(server, "recallx_search_activities", {
|
|
491
802
|
title: "Search Activities",
|
|
492
803
|
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 +819,14 @@ export function createRecallXMcpServer(params) {
|
|
|
508
819
|
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
509
820
|
sort: z.enum(["relevance", "updated_at"]).default("relevance")
|
|
510
821
|
}
|
|
511
|
-
},
|
|
822
|
+
}, async (input) => {
|
|
823
|
+
const result = await apiClient.post("/activities/search", normalizeActivitySearchInput(input));
|
|
824
|
+
const items = Array.isArray(result.items) ? result.items : [];
|
|
825
|
+
const ids = items.filter((item) => isRecord(item) && typeof item.id === "string").map((item) => item.id);
|
|
826
|
+
const query = typeof input.query === "string" ? input.query : "";
|
|
827
|
+
sessionFeedback.trackSearch(query, ids, "activity");
|
|
828
|
+
return toolResult("recallx_search_activities", result);
|
|
829
|
+
});
|
|
512
830
|
registerReadOnlyTool(server, "recallx_search_workspace", {
|
|
513
831
|
title: "Search Workspace",
|
|
514
832
|
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 +856,22 @@ export function createRecallXMcpServer(params) {
|
|
|
538
856
|
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
539
857
|
sort: z.enum(["relevance", "updated_at", "smart"]).default("relevance")
|
|
540
858
|
}
|
|
541
|
-
},
|
|
859
|
+
}, async (input) => {
|
|
860
|
+
const result = await apiClient.post("/search", normalizeWorkspaceSearchInput(input));
|
|
861
|
+
const items = Array.isArray(result.items) ? result.items : [];
|
|
862
|
+
const ids = items.filter((item) => isRecord(item) && typeof (item.id ?? item.nodeId) === "string").map((item) => (item.id ?? item.nodeId));
|
|
863
|
+
const mixedTypes = [...new Set(items.filter((item) => isRecord(item) && typeof item.type === "string").map((item) => item.type))];
|
|
864
|
+
const query = typeof input.query === "string" ? input.query : "";
|
|
865
|
+
sessionFeedback.trackSearch(query, ids, `mixed(${mixedTypes.join(",") || "unknown"})`);
|
|
866
|
+
return toolResult("recallx_search_workspace", result);
|
|
867
|
+
});
|
|
542
868
|
registerReadOnlyTool(server, "recallx_get_node", {
|
|
543
869
|
title: "Get Node",
|
|
544
870
|
description: "Fetch a node together with its related nodes, activities, artifacts, and provenance.",
|
|
545
871
|
inputSchema: {
|
|
546
872
|
nodeId: z.string().min(1).describe("Target node id.")
|
|
547
873
|
}
|
|
548
|
-
}, async ({ nodeId }) => toolResult(await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}`)));
|
|
874
|
+
}, async ({ nodeId }) => toolResult("recallx_get_node", await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}`)));
|
|
549
875
|
registerReadOnlyTool(server, "recallx_get_related", {
|
|
550
876
|
title: "Get Node Neighborhood",
|
|
551
877
|
description: "Fetch the canonical RecallX node neighborhood with optional inferred relations.",
|
|
@@ -565,63 +891,104 @@ export function createRecallXMcpServer(params) {
|
|
|
565
891
|
if (relationTypeFilter.length) {
|
|
566
892
|
query.set("types", relationTypeFilter.join(","));
|
|
567
893
|
}
|
|
568
|
-
return toolResult(await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}/neighborhood?${query.toString()}`));
|
|
894
|
+
return toolResult("recallx_get_related", await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}/neighborhood?${query.toString()}`));
|
|
569
895
|
});
|
|
570
|
-
registerTool(server, "
|
|
571
|
-
title: "
|
|
572
|
-
description: "
|
|
896
|
+
registerTool(server, "recallx_manage_inferred_relations", {
|
|
897
|
+
title: "Manage Inferred Relations",
|
|
898
|
+
description: "Create or update inferred relations, or trigger a maintenance recompute pass. Use `action='upsert'` to add/update a single relation; use `action='recompute'` to refresh scores from usage events. **When to use:** only when you have strong evidence that two nodes are related and the system has not already inferred it (upsert), or during maintenance workflows (recompute). For routine tasks, prefer `recallx_get_related` to read existing inferred links.",
|
|
573
899
|
inputSchema: {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
900
|
+
action: z.enum(["upsert", "recompute"]).describe("Whether to upsert a single inferred relation or trigger a recompute pass."),
|
|
901
|
+
fromNodeId: z.string().min(1).optional().describe("Source node for upsert action."),
|
|
902
|
+
toNodeId: z.string().min(1).optional().describe("Target node for upsert action."),
|
|
903
|
+
relationType: z.enum(relationTypes).optional().describe("Relation type for upsert."),
|
|
904
|
+
baseScore: z.number().optional().describe("Base confidence score for upsert."),
|
|
905
|
+
usageScore: z.number().default(0).describe("Usage bonus for upsert."),
|
|
906
|
+
finalScore: z.number().optional().describe("Combined score for upsert."),
|
|
580
907
|
status: z.enum(inferredRelationStatuses).default("active"),
|
|
581
|
-
generator: z.string().min(1).describe("
|
|
908
|
+
generator: z.string().min(1).optional().describe("Generator label for upsert or filter for recompute."),
|
|
582
909
|
evidence: jsonRecordSchema,
|
|
583
910
|
expiresAt: z.string().optional(),
|
|
584
|
-
metadata: jsonRecordSchema
|
|
911
|
+
metadata: jsonRecordSchema,
|
|
912
|
+
relationIds: z.array(z.string().min(1)).max(200).optional().describe("Specific relation IDs to recompute."),
|
|
913
|
+
limit: z.number().int().min(1).max(500).default(100).describe("Max relations for recompute pass.")
|
|
585
914
|
}
|
|
586
|
-
},
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
915
|
+
}, async (input) => {
|
|
916
|
+
if (input.action === "upsert") {
|
|
917
|
+
if (!input.fromNodeId || !input.toNodeId || !input.relationType || input.baseScore === undefined || input.finalScore === undefined) {
|
|
918
|
+
throw new Error("Invalid arguments for tool recallx_manage_inferred_relations: action='upsert' requires fromNodeId, toNodeId, relationType, baseScore, and finalScore.");
|
|
919
|
+
}
|
|
920
|
+
const body = {
|
|
921
|
+
fromNodeId: input.fromNodeId,
|
|
922
|
+
toNodeId: input.toNodeId,
|
|
923
|
+
relationType: input.relationType,
|
|
924
|
+
baseScore: input.baseScore,
|
|
925
|
+
usageScore: input.usageScore,
|
|
926
|
+
finalScore: input.finalScore,
|
|
927
|
+
status: input.status,
|
|
928
|
+
generator: input.generator,
|
|
929
|
+
evidence: input.evidence,
|
|
930
|
+
expiresAt: input.expiresAt,
|
|
931
|
+
metadata: input.metadata
|
|
932
|
+
};
|
|
933
|
+
return toolResult("recallx_manage_inferred_relations", await apiClient.post("/inferred-relations", body));
|
|
599
934
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
935
|
+
const body = { limit: input.limit };
|
|
936
|
+
if (input.relationIds?.length)
|
|
937
|
+
body.relationIds = input.relationIds;
|
|
938
|
+
if (input.generator)
|
|
939
|
+
body.generator = input.generator;
|
|
940
|
+
return toolResult("recallx_manage_inferred_relations", await apiClient.post("/inferred-relations/recompute", body));
|
|
941
|
+
});
|
|
942
|
+
registerTool(server, "recallx_append_feedback", {
|
|
943
|
+
title: "Append Feedback",
|
|
944
|
+
description: "Append a usefulness signal for search results or relation links. **Note:** this tool is normally called automatically by the MCP bridge after your task completes. Only call it directly if you want to record ad-hoc feedback during a session.",
|
|
604
945
|
inputSchema: {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
946
|
+
feedbackType: z.enum(["search", "relation"]).describe("Whether this is search result feedback or relation usage feedback."),
|
|
947
|
+
resultType: z.enum(searchFeedbackResultTypes).optional().describe("Required when feedbackType='search': 'node' or 'activity'."),
|
|
948
|
+
resultId: z.string().min(1).optional().describe("Required when feedbackType='search': the node or activity ID."),
|
|
949
|
+
verdict: z.enum(searchFeedbackVerdicts).optional().describe("Required when feedbackType='search': 'useful', 'not_useful', or 'uncertain'."),
|
|
950
|
+
relationId: z.string().min(1).optional().describe("Required when feedbackType='relation': the relation ID."),
|
|
951
|
+
relationSource: z.enum(relationSources).optional().describe("Required when feedbackType='relation': 'canonical' or 'inferred'."),
|
|
952
|
+
relationEventType: z.enum(relationUsageEventTypes).optional().describe("Required when feedbackType='relation': e.g. 'bundle_included', 'bundle_used_in_output'."),
|
|
953
|
+
query: z.string().optional().describe("Original search query for context."),
|
|
609
954
|
sessionId: z.string().optional(),
|
|
610
955
|
runId: z.string().optional(),
|
|
611
956
|
source: sourceSchema.optional(),
|
|
612
957
|
confidence: z.number().min(0).max(1).default(1),
|
|
958
|
+
delta: z.number().default(1).describe("Score delta for relation feedback."),
|
|
613
959
|
metadata: jsonRecordSchema
|
|
614
960
|
}
|
|
615
|
-
},
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
961
|
+
}, async (input) => {
|
|
962
|
+
if (input.feedbackType === "search") {
|
|
963
|
+
if (!input.resultType || !input.resultId || !input.verdict) {
|
|
964
|
+
throw new Error("Invalid arguments for tool recallx_append_feedback: feedbackType='search' requires resultType, resultId, and verdict.");
|
|
965
|
+
}
|
|
966
|
+
return toolResult("recallx_append_feedback", await apiClient.post("/search-feedback-events", {
|
|
967
|
+
resultType: input.resultType,
|
|
968
|
+
resultId: input.resultId,
|
|
969
|
+
verdict: input.verdict,
|
|
970
|
+
query: input.query,
|
|
971
|
+
sessionId: input.sessionId,
|
|
972
|
+
runId: input.runId,
|
|
973
|
+
source: input.source,
|
|
974
|
+
confidence: input.confidence,
|
|
975
|
+
metadata: input.metadata
|
|
976
|
+
}));
|
|
623
977
|
}
|
|
624
|
-
|
|
978
|
+
if (!input.relationId || !input.relationSource || !input.relationEventType) {
|
|
979
|
+
throw new Error("Invalid arguments for tool recallx_append_feedback: feedbackType='relation' requires relationId, relationSource, and relationEventType.");
|
|
980
|
+
}
|
|
981
|
+
return toolResult("recallx_append_feedback", await apiClient.post("/relation-usage-events", {
|
|
982
|
+
relationId: input.relationId,
|
|
983
|
+
relationSource: input.relationSource,
|
|
984
|
+
eventType: input.relationEventType,
|
|
985
|
+
sessionId: input.sessionId,
|
|
986
|
+
runId: input.runId,
|
|
987
|
+
source: input.source,
|
|
988
|
+
delta: input.delta,
|
|
989
|
+
metadata: input.metadata
|
|
990
|
+
}));
|
|
991
|
+
});
|
|
625
992
|
registerTool(server, "recallx_append_activity", {
|
|
626
993
|
title: "Append Activity",
|
|
627
994
|
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 +999,7 @@ export function createRecallXMcpServer(params) {
|
|
|
632
999
|
source: sourceSchema,
|
|
633
1000
|
metadata: jsonRecordSchema
|
|
634
1001
|
}
|
|
635
|
-
}, createPostToolHandler(apiClient, "/activities"));
|
|
1002
|
+
}, createPostToolHandler("recallx_append_activity", apiClient, "/activities"));
|
|
636
1003
|
registerTool(server, "recallx_capture_memory", {
|
|
637
1004
|
title: "Capture Memory",
|
|
638
1005
|
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 +1013,7 @@ export function createRecallXMcpServer(params) {
|
|
|
646
1013
|
source: sourceSchema,
|
|
647
1014
|
metadata: jsonRecordSchema
|
|
648
1015
|
}
|
|
649
|
-
}, createPostToolHandler(apiClient, "/capture"));
|
|
1016
|
+
}, createPostToolHandler("recallx_capture_memory", apiClient, "/capture"));
|
|
650
1017
|
registerTool(server, "recallx_create_node", {
|
|
651
1018
|
title: "Create Node",
|
|
652
1019
|
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 +1030,9 @@ export function createRecallXMcpServer(params) {
|
|
|
663
1030
|
}
|
|
664
1031
|
}, async (input) => {
|
|
665
1032
|
try {
|
|
666
|
-
|
|
1033
|
+
const result = await apiClient.post("/nodes", input);
|
|
1034
|
+
await autoAppendSearchFeedback();
|
|
1035
|
+
return toolResult("recallx_create_node", result);
|
|
667
1036
|
}
|
|
668
1037
|
catch (error) {
|
|
669
1038
|
if (error instanceof RecallXApiError &&
|
|
@@ -695,7 +1064,11 @@ export function createRecallXMcpServer(params) {
|
|
|
695
1064
|
.min(1)
|
|
696
1065
|
.max(100)
|
|
697
1066
|
}
|
|
698
|
-
}, async (input) =>
|
|
1067
|
+
}, async (input) => {
|
|
1068
|
+
const result = await apiClient.post("/nodes/batch", input);
|
|
1069
|
+
await autoAppendSearchFeedback();
|
|
1070
|
+
return toolResult("recallx_create_nodes", result);
|
|
1071
|
+
});
|
|
699
1072
|
registerTool(server, "recallx_create_relation", {
|
|
700
1073
|
title: "Create Relation",
|
|
701
1074
|
description: "Create a relation between two nodes. Agent-created relations typically start suggested and are promoted automatically when confidence improves.",
|
|
@@ -707,38 +1080,37 @@ export function createRecallXMcpServer(params) {
|
|
|
707
1080
|
source: sourceSchema,
|
|
708
1081
|
metadata: jsonRecordSchema
|
|
709
1082
|
}
|
|
710
|
-
}, createPostToolHandler(apiClient, "/relations"));
|
|
711
|
-
registerReadOnlyTool(server, "
|
|
712
|
-
title: "
|
|
713
|
-
description: "
|
|
1083
|
+
}, createPostToolHandler("recallx_create_relation", apiClient, "/relations"));
|
|
1084
|
+
registerReadOnlyTool(server, "recallx_governance", {
|
|
1085
|
+
title: "Governance",
|
|
1086
|
+
description: "Read governance issues, check a specific entity's state, or trigger a recompute pass. **When to use:** after creating/editing content to verify it landed in good shape, or when reviewing items flagged as contested/low_confidence. Use action='issues' (default) to list problems, action='state' to inspect one entity, or action='recompute' to refresh state.",
|
|
714
1087
|
inputSchema: {
|
|
715
|
-
|
|
716
|
-
|
|
1088
|
+
action: z.enum(["issues", "state", "recompute"]).default("issues"),
|
|
1089
|
+
states: z.array(z.enum(governanceStates)).default(["contested", "low_confidence"]).describe("Issue states to include (for action='issues')."),
|
|
1090
|
+
limit: z.number().int().min(1).max(100).default(20).describe("Max issues (for action='issues') or recompute batch (for action='recompute')."),
|
|
1091
|
+
entityType: z.enum(["node", "relation"]).optional().describe("Required for action='state': entity type to inspect."),
|
|
1092
|
+
entityId: z.string().min(1).optional().describe("Required for action='state': entity ID to inspect."),
|
|
1093
|
+
entityIds: z.array(z.string().min(1)).max(200).optional().describe("Specific entity IDs to recompute (for action='recompute').")
|
|
1094
|
+
}
|
|
1095
|
+
}, async ({ action, states, limit, entityType, entityId, entityIds }) => {
|
|
1096
|
+
if (action === "state") {
|
|
1097
|
+
if (!entityType || !entityId) {
|
|
1098
|
+
throw new Error("Invalid arguments for tool recallx_governance: action='state' requires entityType and entityId.");
|
|
1099
|
+
}
|
|
1100
|
+
return toolResult("recallx_governance", await apiClient.get(`/governance/state/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`));
|
|
1101
|
+
}
|
|
1102
|
+
if (action === "recompute") {
|
|
1103
|
+
const body = { limit };
|
|
1104
|
+
if (entityIds?.length)
|
|
1105
|
+
body.entityIds = entityIds;
|
|
1106
|
+
return toolResult("recallx_governance", await apiClient.post("/governance/recompute", body));
|
|
717
1107
|
}
|
|
718
|
-
}, async ({ states, limit }) => {
|
|
719
1108
|
const query = new URLSearchParams({
|
|
720
1109
|
states: states.join(","),
|
|
721
1110
|
limit: String(limit)
|
|
722
1111
|
});
|
|
723
|
-
return toolResult(await apiClient.get(`/governance/issues?${query.toString()}`));
|
|
1112
|
+
return toolResult("recallx_governance", await apiClient.get(`/governance/issues?${query.toString()}`));
|
|
724
1113
|
});
|
|
725
|
-
registerReadOnlyTool(server, "recallx_get_governance_state", {
|
|
726
|
-
title: "Get Governance State",
|
|
727
|
-
description: "Read the current automatic governance state and recent events for a node or relation.",
|
|
728
|
-
inputSchema: {
|
|
729
|
-
entityType: z.enum(["node", "relation"]),
|
|
730
|
-
entityId: z.string().min(1)
|
|
731
|
-
}
|
|
732
|
-
}, async ({ entityType, entityId }) => toolResult(await apiClient.get(`/governance/state/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`)));
|
|
733
|
-
registerTool(server, "recallx_recompute_governance", {
|
|
734
|
-
title: "Recompute Governance",
|
|
735
|
-
description: "Run a bounded automatic governance recompute pass for nodes, relations, or both.",
|
|
736
|
-
inputSchema: {
|
|
737
|
-
entityType: z.enum(["node", "relation"]).optional(),
|
|
738
|
-
entityIds: z.array(z.string().min(1)).max(200).optional(),
|
|
739
|
-
limit: z.number().int().min(1).max(500).default(100)
|
|
740
|
-
}
|
|
741
|
-
}, createPostToolHandler(apiClient, "/governance/recompute"));
|
|
742
1114
|
registerReadOnlyTool(server, "recallx_context_bundle", {
|
|
743
1115
|
title: "Build Context Bundle",
|
|
744
1116
|
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,30 +1138,35 @@ export function createRecallXMcpServer(params) {
|
|
|
766
1138
|
maxItems: 10
|
|
767
1139
|
})
|
|
768
1140
|
}
|
|
769
|
-
}, async ({ targetId, ...input }) =>
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1141
|
+
}, async ({ targetId, ...input }) => {
|
|
1142
|
+
const result = await apiClient.post("/context/bundles", {
|
|
1143
|
+
...input,
|
|
1144
|
+
...(targetId
|
|
1145
|
+
? {
|
|
1146
|
+
target: {
|
|
1147
|
+
id: targetId
|
|
1148
|
+
}
|
|
775
1149
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1150
|
+
: {})
|
|
1151
|
+
});
|
|
1152
|
+
const items = Array.isArray(result.items) ? result.items : [];
|
|
1153
|
+
const ids = items.filter((item) => isRecord(item) && typeof item.id === "string").map((item) => item.id);
|
|
1154
|
+
sessionFeedback.trackBundle(targetId, ids);
|
|
1155
|
+
return toolResult("recallx_context_bundle", result);
|
|
1156
|
+
});
|
|
779
1157
|
registerTool(server, "recallx_semantic_reindex", {
|
|
780
|
-
title: "
|
|
781
|
-
description: "Queue semantic reindexing for a
|
|
1158
|
+
title: "Semantic Reindex",
|
|
1159
|
+
description: "Queue semantic reindexing for recent workspace nodes or a specific node. **When to use:** after editing node content that needs updated embeddings, or when semantic search results seem stale. Omit nodeId to reindex recent nodes.",
|
|
782
1160
|
inputSchema: {
|
|
783
|
-
|
|
1161
|
+
nodeId: z.string().min(1).optional().describe("If provided, reindex only this specific node. Otherwise, reindex recent active nodes."),
|
|
1162
|
+
limit: coerceIntegerSchema(250, 1, 1000).describe("Max nodes to reindex (ignored when nodeId is provided).")
|
|
784
1163
|
}
|
|
785
|
-
},
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
description: "Queue semantic reindexing for a specific node id.",
|
|
789
|
-
inputSchema: {
|
|
790
|
-
nodeId: z.string().min(1)
|
|
1164
|
+
}, async ({ nodeId, limit }) => {
|
|
1165
|
+
if (nodeId) {
|
|
1166
|
+
return toolResult("recallx_semantic_reindex", await apiClient.post(`/semantic/reindex/${encodeURIComponent(nodeId)}`, {}));
|
|
791
1167
|
}
|
|
792
|
-
|
|
1168
|
+
return toolResult("recallx_semantic_reindex", await apiClient.post("/semantic/reindex", { limit }));
|
|
1169
|
+
});
|
|
793
1170
|
registerReadOnlyTool(server, "recallx_rank_candidates", {
|
|
794
1171
|
title: "Rank Candidate Nodes",
|
|
795
1172
|
description: "Rank a bounded set of candidate node ids for a target using RecallX request-time retrieval scoring.",
|
|
@@ -799,6 +1176,6 @@ export function createRecallXMcpServer(params) {
|
|
|
799
1176
|
preset: bundlePresetSchema("for-assistant"),
|
|
800
1177
|
targetNodeId: z.string().optional()
|
|
801
1178
|
}
|
|
802
|
-
}, createPostToolHandler(apiClient, "/retrieval/rank-candidates"));
|
|
1179
|
+
}, createPostToolHandler("recallx_rank_candidates", apiClient, "/retrieval/rank-candidates"));
|
|
803
1180
|
return server;
|
|
804
1181
|
}
|