recallx 1.0.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 +205 -0
- package/app/cli/bin/recallx-mcp.js +2 -0
- package/app/cli/bin/recallx.js +8 -0
- package/app/cli/src/cli.js +808 -0
- package/app/cli/src/format.js +242 -0
- package/app/cli/src/http.js +35 -0
- package/app/mcp/api-client.js +101 -0
- package/app/mcp/index.js +128 -0
- package/app/mcp/server.js +786 -0
- package/app/server/app.js +2263 -0
- package/app/server/config.js +27 -0
- package/app/server/db.js +399 -0
- package/app/server/errors.js +17 -0
- package/app/server/governance.js +466 -0
- package/app/server/index.js +26 -0
- package/app/server/inferred-relations.js +247 -0
- package/app/server/observability.js +495 -0
- package/app/server/project-graph.js +199 -0
- package/app/server/relation-scoring.js +59 -0
- package/app/server/repositories.js +2992 -0
- package/app/server/retrieval.js +486 -0
- package/app/server/semantic/chunker.js +85 -0
- package/app/server/semantic/provider.js +124 -0
- package/app/server/semantic/types.js +1 -0
- package/app/server/semantic/vector-store.js +169 -0
- package/app/server/utils.js +43 -0
- package/app/server/workspace-session.js +128 -0
- package/app/server/workspace.js +79 -0
- package/app/shared/contracts.js +268 -0
- package/app/shared/request-runtime.js +30 -0
- package/app/shared/types.js +1 -0
- package/app/shared/version.js +1 -0
- package/dist/renderer/assets/ProjectGraphCanvas-BMvz9DmE.js +312 -0
- package/dist/renderer/assets/index-C2-KXqBO.css +1 -0
- package/dist/renderer/assets/index-CrDu22h7.js +76 -0
- package/dist/renderer/index.html +13 -0
- package/package.json +49 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
import { activityTypes, bundleModes, bundlePresets, canonicalities, captureModes, governanceStates, inferredRelationStatuses, nodeStatuses, nodeTypes, relationSources, relationStatuses, relationTypes, relationUsageEventTypes, searchFeedbackResultTypes, searchFeedbackVerdicts, sourceTypes } from "../shared/contracts.js";
|
|
4
|
+
import { RECALLX_VERSION } from "../shared/version.js";
|
|
5
|
+
import { createObservabilityWriter, summarizePayloadShape } from "../server/observability.js";
|
|
6
|
+
import { RecallXApiClient, RecallXApiError } from "./api-client.js";
|
|
7
|
+
const jsonRecordSchema = z.record(z.string(), z.any()).default({});
|
|
8
|
+
const stringOrStringArraySchema = z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]);
|
|
9
|
+
const workspaceSearchScopes = ["nodes", "activities"];
|
|
10
|
+
const workspaceScopeSchema = z.enum(workspaceSearchScopes);
|
|
11
|
+
const workspaceScopeInputSchema = z.preprocess((value) => {
|
|
12
|
+
if (typeof value !== "string") {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
const parts = value
|
|
16
|
+
.split(",")
|
|
17
|
+
.map((part) => part.trim())
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
if (!parts.length) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
return parts.length === 1 ? parts[0] : parts;
|
|
23
|
+
}, z.union([workspaceScopeSchema, z.array(workspaceScopeSchema).min(1)]));
|
|
24
|
+
function parseIntegerLike(value) {
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!/^[+-]?\d+$/.test(trimmed)) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
const parsed = Number(trimmed);
|
|
33
|
+
return Number.isSafeInteger(parsed) ? parsed : value;
|
|
34
|
+
}
|
|
35
|
+
function coerceIntegerSchema(defaultValue, min, max) {
|
|
36
|
+
return z.preprocess((value) => {
|
|
37
|
+
if (value === undefined || value === null || value === "") {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "number") {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
const parsed = parseIntegerLike(value);
|
|
44
|
+
if (typeof parsed !== "string") {
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}, z.number().int().min(min).max(max).default(defaultValue));
|
|
49
|
+
}
|
|
50
|
+
function coerceBooleanSchema(defaultValue) {
|
|
51
|
+
return z.preprocess((value) => {
|
|
52
|
+
if (value === undefined || value === null || value === "") {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "boolean") {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
if (value === "true")
|
|
60
|
+
return true;
|
|
61
|
+
if (value === "false")
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}, z.boolean().default(defaultValue));
|
|
66
|
+
}
|
|
67
|
+
function formatStructuredContent(content) {
|
|
68
|
+
return JSON.stringify(content, null, 2);
|
|
69
|
+
}
|
|
70
|
+
function toolResult(structuredContent) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: formatStructuredContent(structuredContent)
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
structuredContent
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function buildSourceSchema(defaultSource) {
|
|
82
|
+
const sourceDefault = {
|
|
83
|
+
actorType: defaultSource.actorType,
|
|
84
|
+
actorLabel: defaultSource.actorLabel,
|
|
85
|
+
toolName: defaultSource.toolName,
|
|
86
|
+
toolVersion: defaultSource.toolVersion
|
|
87
|
+
};
|
|
88
|
+
return z
|
|
89
|
+
.object({
|
|
90
|
+
actorType: z.enum(sourceTypes).default(defaultSource.actorType),
|
|
91
|
+
actorLabel: z.string().min(1).default(defaultSource.actorLabel),
|
|
92
|
+
toolName: z.string().min(1).default(defaultSource.toolName),
|
|
93
|
+
toolVersion: defaultSource.toolVersion
|
|
94
|
+
? z.string().min(1).optional().default(defaultSource.toolVersion)
|
|
95
|
+
: z.string().min(1).optional()
|
|
96
|
+
})
|
|
97
|
+
.default(sourceDefault);
|
|
98
|
+
}
|
|
99
|
+
const workspaceInfoSchema = z.object({
|
|
100
|
+
rootPath: z.string(),
|
|
101
|
+
workspaceName: z.string(),
|
|
102
|
+
schemaVersion: z.number(),
|
|
103
|
+
bindAddress: z.string(),
|
|
104
|
+
enabledIntegrationModes: z.array(z.string()),
|
|
105
|
+
authMode: z.string()
|
|
106
|
+
});
|
|
107
|
+
const healthOutputSchema = z.object({
|
|
108
|
+
status: z.string(),
|
|
109
|
+
workspaceLoaded: z.boolean(),
|
|
110
|
+
workspaceRoot: z.string(),
|
|
111
|
+
schemaVersion: z.number()
|
|
112
|
+
}).passthrough();
|
|
113
|
+
const sourceDescription = "Optional provenance override. If omitted, RecallX MCP uses its own agent identity so durable writes still keep attribution.";
|
|
114
|
+
const readOnlyToolAnnotations = {
|
|
115
|
+
readOnlyHint: true,
|
|
116
|
+
idempotentHint: true
|
|
117
|
+
};
|
|
118
|
+
function createGetToolHandler(apiClient, path) {
|
|
119
|
+
return async () => toolResult(await apiClient.get(path));
|
|
120
|
+
}
|
|
121
|
+
function createPostToolHandler(apiClient, path) {
|
|
122
|
+
return async (input) => toolResult(await apiClient.post(path, input));
|
|
123
|
+
}
|
|
124
|
+
function createNormalizedPostToolHandler(apiClient, path, normalize) {
|
|
125
|
+
return async (input) => toolResult(await apiClient.post(path, normalize(input)));
|
|
126
|
+
}
|
|
127
|
+
function withReadOnlyAnnotations(config) {
|
|
128
|
+
return {
|
|
129
|
+
...config,
|
|
130
|
+
annotations: {
|
|
131
|
+
...readOnlyToolAnnotations,
|
|
132
|
+
...(config.annotations ?? {})
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function classifyMcpError(error) {
|
|
137
|
+
if (error instanceof RecallXApiError) {
|
|
138
|
+
if (error.code === "NETWORK_ERROR") {
|
|
139
|
+
return { errorKind: "network_error", errorCode: error.code, statusCode: error.status };
|
|
140
|
+
}
|
|
141
|
+
if (error.code === "INVALID_RESPONSE") {
|
|
142
|
+
return { errorKind: "invalid_response", errorCode: error.code, statusCode: error.status };
|
|
143
|
+
}
|
|
144
|
+
if (error.code === "EMPTY_RESPONSE") {
|
|
145
|
+
return { errorKind: "empty_response", errorCode: error.code, statusCode: error.status };
|
|
146
|
+
}
|
|
147
|
+
if (error.code === "HTTP_ERROR") {
|
|
148
|
+
return { errorKind: "http_error", errorCode: error.code, statusCode: error.status };
|
|
149
|
+
}
|
|
150
|
+
return { errorKind: "api_error", errorCode: error.code, statusCode: error.status };
|
|
151
|
+
}
|
|
152
|
+
if (error instanceof Error && "issues" in error) {
|
|
153
|
+
return { errorKind: "validation_error", errorCode: "INVALID_INPUT", statusCode: 400 };
|
|
154
|
+
}
|
|
155
|
+
if (error instanceof Error && error.message.startsWith("Invalid arguments for tool ")) {
|
|
156
|
+
return { errorKind: "normalization_error", errorCode: "INVALID_ARGUMENTS", statusCode: 400 };
|
|
157
|
+
}
|
|
158
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string") {
|
|
159
|
+
return {
|
|
160
|
+
errorKind: "api_error",
|
|
161
|
+
errorCode: String(error.code),
|
|
162
|
+
statusCode: 400
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { errorKind: "unexpected_error", errorCode: "UNEXPECTED_ERROR", statusCode: 500 };
|
|
166
|
+
}
|
|
167
|
+
function normalizeStringList(value) {
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
const items = value
|
|
170
|
+
.filter((item) => typeof item === "string")
|
|
171
|
+
.map((item) => item.trim())
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
return items.length ? items : undefined;
|
|
174
|
+
}
|
|
175
|
+
if (typeof value === "string" && value.trim()) {
|
|
176
|
+
return [value.trim()];
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
function mergeStringLists(...values) {
|
|
181
|
+
const merged = values.flatMap((value) => normalizeStringList(value) ?? []);
|
|
182
|
+
return merged.length ? Array.from(new Set(merged)) : undefined;
|
|
183
|
+
}
|
|
184
|
+
function normalizeCommaSeparatedList(value) {
|
|
185
|
+
if (typeof value !== "string") {
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
const parts = value
|
|
189
|
+
.split(",")
|
|
190
|
+
.map((part) => part.trim())
|
|
191
|
+
.filter(Boolean);
|
|
192
|
+
if (!parts.length) {
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
return parts.length === 1 ? parts[0] : parts;
|
|
196
|
+
}
|
|
197
|
+
function assertSupportedEnumValues(toolName, fieldName, values, supportedValues, hints = {}) {
|
|
198
|
+
if (!values?.length) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const supported = new Set(supportedValues);
|
|
202
|
+
const invalid = values.find((value) => !supported.has(value));
|
|
203
|
+
if (invalid) {
|
|
204
|
+
const hint = hints[invalid];
|
|
205
|
+
if (hint) {
|
|
206
|
+
throw new Error(`Invalid arguments for tool ${toolName}: ${hint}`);
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`Invalid arguments for tool ${toolName}: unsupported ${fieldName} '${invalid}'. Expected one of ${supportedValues.join(", ")}.`);
|
|
209
|
+
}
|
|
210
|
+
return values;
|
|
211
|
+
}
|
|
212
|
+
function readSearchQuery(toolName, input) {
|
|
213
|
+
const query = typeof input.query === "string" ? input.query : "";
|
|
214
|
+
const allowEmptyQuery = input.allowEmptyQuery === true;
|
|
215
|
+
if (!query.trim() && !allowEmptyQuery) {
|
|
216
|
+
throw new Error(`Invalid arguments for tool ${toolName}: empty query is disabled by default. If you want browse-style results, pass allowEmptyQuery: true.`);
|
|
217
|
+
}
|
|
218
|
+
return query;
|
|
219
|
+
}
|
|
220
|
+
function normalizeNodeSearchInput(input) {
|
|
221
|
+
const rawFilters = (typeof input.filters === "object" && input.filters ? input.filters : {});
|
|
222
|
+
const filters = {
|
|
223
|
+
types: assertSupportedEnumValues("recallx_search_nodes", "node type", mergeStringLists(input.type, input.types, rawFilters.types), nodeTypes, {
|
|
224
|
+
activity: "`activity` is not a node type. Use `recallx_search_activities` for operational logs."
|
|
225
|
+
}),
|
|
226
|
+
status: assertSupportedEnumValues("recallx_search_nodes", "node status", mergeStringLists(input.status, rawFilters.status), nodeStatuses),
|
|
227
|
+
sourceLabels: mergeStringLists(rawFilters.sourceLabels),
|
|
228
|
+
tags: mergeStringLists(input.tag, rawFilters.tags)
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
query: readSearchQuery("recallx_search_nodes", input),
|
|
232
|
+
filters,
|
|
233
|
+
limit: Number(input.limit ?? 10),
|
|
234
|
+
offset: Number(input.offset ?? 0),
|
|
235
|
+
sort: input.sort === "updated_at" ? "updated_at" : "relevance"
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function normalizeActivitySearchInput(input) {
|
|
239
|
+
const rawFilters = (typeof input.filters === "object" && input.filters ? input.filters : {});
|
|
240
|
+
const filters = {
|
|
241
|
+
targetNodeIds: mergeStringLists(input.targetNodeId, rawFilters.targetNodeIds),
|
|
242
|
+
activityTypes: assertSupportedEnumValues("recallx_search_activities", "activity type", mergeStringLists(input.activityType, rawFilters.activityTypes), activityTypes),
|
|
243
|
+
sourceLabels: mergeStringLists(rawFilters.sourceLabels),
|
|
244
|
+
createdAfter: typeof rawFilters.createdAfter === "string" ? rawFilters.createdAfter : undefined,
|
|
245
|
+
createdBefore: typeof rawFilters.createdBefore === "string" ? rawFilters.createdBefore : undefined
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
query: readSearchQuery("recallx_search_activities", input),
|
|
249
|
+
filters,
|
|
250
|
+
limit: Number(input.limit ?? 10),
|
|
251
|
+
offset: Number(input.offset ?? 0),
|
|
252
|
+
sort: input.sort === "updated_at" ? "updated_at" : "relevance"
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function normalizeWorkspaceSearchInput(input) {
|
|
256
|
+
const rawNodeFilters = (typeof input.nodeFilters === "object" && input.nodeFilters ? input.nodeFilters : {});
|
|
257
|
+
const rawActivityFilters = typeof input.activityFilters === "object" && input.activityFilters ? input.activityFilters : {};
|
|
258
|
+
const scopes = assertSupportedEnumValues("recallx_search_workspace", "scope", mergeStringLists(normalizeCommaSeparatedList(input.scope), normalizeCommaSeparatedList(input.scopes)), workspaceSearchScopes) ?? [...workspaceSearchScopes];
|
|
259
|
+
const nodeFilters = {
|
|
260
|
+
types: assertSupportedEnumValues("recallx_search_workspace", "node type", mergeStringLists(rawNodeFilters.types), nodeTypes),
|
|
261
|
+
status: assertSupportedEnumValues("recallx_search_workspace", "node status", mergeStringLists(rawNodeFilters.status), nodeStatuses),
|
|
262
|
+
sourceLabels: mergeStringLists(rawNodeFilters.sourceLabels),
|
|
263
|
+
tags: mergeStringLists(rawNodeFilters.tags)
|
|
264
|
+
};
|
|
265
|
+
const activityFilters = {
|
|
266
|
+
targetNodeIds: mergeStringLists(rawActivityFilters.targetNodeIds),
|
|
267
|
+
activityTypes: assertSupportedEnumValues("recallx_search_workspace", "activity type", mergeStringLists(rawActivityFilters.activityTypes), activityTypes),
|
|
268
|
+
sourceLabels: mergeStringLists(rawActivityFilters.sourceLabels),
|
|
269
|
+
createdAfter: typeof rawActivityFilters.createdAfter === "string" ? rawActivityFilters.createdAfter : undefined,
|
|
270
|
+
createdBefore: typeof rawActivityFilters.createdBefore === "string" ? rawActivityFilters.createdBefore : undefined
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
query: readSearchQuery("recallx_search_workspace", input),
|
|
274
|
+
scopes,
|
|
275
|
+
nodeFilters,
|
|
276
|
+
activityFilters,
|
|
277
|
+
limit: Number(input.limit ?? 10),
|
|
278
|
+
offset: Number(input.offset ?? 0),
|
|
279
|
+
sort: input.sort === "updated_at" ? "updated_at" : input.sort === "smart" ? "smart" : "relevance"
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export function createRecallXMcpServer(params) {
|
|
283
|
+
const apiClient = params?.apiClient ??
|
|
284
|
+
new RecallXApiClient(process.env.RECALLX_API_URL ?? "http://127.0.0.1:8787/api/v1", process.env.RECALLX_API_TOKEN);
|
|
285
|
+
const defaultSource = params?.defaultSource ?? {
|
|
286
|
+
actorType: "agent",
|
|
287
|
+
actorLabel: process.env.RECALLX_MCP_SOURCE_LABEL ?? "RecallX MCP",
|
|
288
|
+
toolName: process.env.RECALLX_MCP_TOOL_NAME ?? "recallx-mcp",
|
|
289
|
+
toolVersion: params?.serverVersion ?? RECALLX_VERSION
|
|
290
|
+
};
|
|
291
|
+
const sourceSchema = buildSourceSchema(defaultSource).describe(sourceDescription);
|
|
292
|
+
const defaultObservabilityState = {
|
|
293
|
+
enabled: false,
|
|
294
|
+
workspaceRoot: process.cwd(),
|
|
295
|
+
workspaceName: "RecallX MCP",
|
|
296
|
+
retentionDays: 14,
|
|
297
|
+
slowRequestMs: 250,
|
|
298
|
+
capturePayloadShape: true
|
|
299
|
+
};
|
|
300
|
+
let currentObservabilityState = params?.observabilityState ?? defaultObservabilityState;
|
|
301
|
+
const readObservabilityState = async () => {
|
|
302
|
+
if (!params?.getObservabilityState) {
|
|
303
|
+
return currentObservabilityState;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
currentObservabilityState = await params.getObservabilityState();
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Keep the last known-good state so observability refresh failures do not break tool calls.
|
|
310
|
+
}
|
|
311
|
+
return currentObservabilityState;
|
|
312
|
+
};
|
|
313
|
+
const observability = createObservabilityWriter({
|
|
314
|
+
getState: () => currentObservabilityState
|
|
315
|
+
});
|
|
316
|
+
const server = new McpServer({
|
|
317
|
+
name: "recallx-mcp",
|
|
318
|
+
version: params?.serverVersion ?? RECALLX_VERSION
|
|
319
|
+
}, {
|
|
320
|
+
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. 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.",
|
|
321
|
+
capabilities: {
|
|
322
|
+
logging: {}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
const registerTool = (server, name, config, handler) => {
|
|
326
|
+
server.registerTool(name, config, async (...args) => {
|
|
327
|
+
const input = args[0];
|
|
328
|
+
const observabilityState = await readObservabilityState();
|
|
329
|
+
const traceId = `trace_mcp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
330
|
+
const span = observability.startSpan({
|
|
331
|
+
surface: "mcp",
|
|
332
|
+
operation: name,
|
|
333
|
+
traceId,
|
|
334
|
+
details: observabilityState.capturePayloadShape ? summarizePayloadShape(input) : undefined
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
return await observability.withContext({
|
|
338
|
+
traceId,
|
|
339
|
+
requestId: null,
|
|
340
|
+
workspaceRoot: observabilityState.workspaceRoot,
|
|
341
|
+
workspaceName: observabilityState.workspaceName,
|
|
342
|
+
toolName: name,
|
|
343
|
+
surface: "mcp"
|
|
344
|
+
}, () => span.run(async () => {
|
|
345
|
+
const result = await handler(...args);
|
|
346
|
+
span.addDetails({ success: true });
|
|
347
|
+
await span.finish({ outcome: "success" });
|
|
348
|
+
return result;
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
const classified = classifyMcpError(error);
|
|
353
|
+
await span.finish({
|
|
354
|
+
outcome: "error",
|
|
355
|
+
statusCode: classified.statusCode,
|
|
356
|
+
errorCode: classified.errorCode,
|
|
357
|
+
errorKind: classified.errorKind
|
|
358
|
+
});
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
const registerReadOnlyTool = (server, name, config, handler) => {
|
|
364
|
+
registerTool(server, name, withReadOnlyAnnotations(config), handler);
|
|
365
|
+
};
|
|
366
|
+
registerReadOnlyTool(server, "recallx_health", {
|
|
367
|
+
title: "RecallX Health",
|
|
368
|
+
description: "Check whether the running local RecallX API is healthy and which workspace is loaded.",
|
|
369
|
+
inputSchema: z.object({
|
|
370
|
+
includeDetails: z.boolean().optional().default(true)
|
|
371
|
+
}),
|
|
372
|
+
outputSchema: healthOutputSchema
|
|
373
|
+
}, async () => toolResult(await apiClient.get("/health")));
|
|
374
|
+
registerReadOnlyTool(server, "recallx_workspace_current", {
|
|
375
|
+
title: "Current Workspace",
|
|
376
|
+
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.",
|
|
377
|
+
outputSchema: workspaceInfoSchema
|
|
378
|
+
}, createGetToolHandler(apiClient, "/workspace"));
|
|
379
|
+
registerReadOnlyTool(server, "recallx_workspace_list", {
|
|
380
|
+
title: "List Workspaces",
|
|
381
|
+
description: "List known RecallX workspaces and identify the currently active one.",
|
|
382
|
+
outputSchema: z.object({
|
|
383
|
+
current: workspaceInfoSchema,
|
|
384
|
+
items: z.array(workspaceInfoSchema.extend({ isCurrent: z.boolean(), lastOpenedAt: z.string() }))
|
|
385
|
+
})
|
|
386
|
+
}, createGetToolHandler(apiClient, "/workspaces"));
|
|
387
|
+
registerTool(server, "recallx_workspace_create", {
|
|
388
|
+
title: "Create Workspace",
|
|
389
|
+
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.",
|
|
390
|
+
inputSchema: {
|
|
391
|
+
rootPath: z.string().min(1).describe("Absolute or user-resolved path for the new workspace root."),
|
|
392
|
+
workspaceName: z.string().min(1).optional().describe("Human-friendly workspace name.")
|
|
393
|
+
}
|
|
394
|
+
}, createPostToolHandler(apiClient, "/workspaces"));
|
|
395
|
+
registerTool(server, "recallx_workspace_open", {
|
|
396
|
+
title: "Open Workspace",
|
|
397
|
+
description: "Switch the running RecallX service to another existing workspace. Only use this when the user explicitly requests opening or switching workspaces.",
|
|
398
|
+
inputSchema: {
|
|
399
|
+
rootPath: z.string().min(1).describe("Existing workspace root path to open.")
|
|
400
|
+
}
|
|
401
|
+
}, createPostToolHandler(apiClient, "/workspaces/open"));
|
|
402
|
+
registerReadOnlyTool(server, "recallx_semantic_status", {
|
|
403
|
+
title: "Semantic Index Status",
|
|
404
|
+
description: "Read the current semantic indexing status, provider configuration, and queued item counts.",
|
|
405
|
+
outputSchema: z.object({
|
|
406
|
+
enabled: z.boolean(),
|
|
407
|
+
provider: z.string().nullable(),
|
|
408
|
+
model: z.string().nullable(),
|
|
409
|
+
chunkEnabled: z.boolean(),
|
|
410
|
+
lastBackfillAt: z.string().nullable(),
|
|
411
|
+
counts: z.object({
|
|
412
|
+
pending: z.number(),
|
|
413
|
+
processing: z.number(),
|
|
414
|
+
stale: z.number(),
|
|
415
|
+
ready: z.number(),
|
|
416
|
+
failed: z.number()
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
}, createGetToolHandler(apiClient, "/semantic/status"));
|
|
420
|
+
registerReadOnlyTool(server, "recallx_semantic_issues", {
|
|
421
|
+
title: "Semantic Index Issues",
|
|
422
|
+
description: "Read semantic indexing issues with optional status filters and cursor pagination.",
|
|
423
|
+
inputSchema: {
|
|
424
|
+
limit: coerceIntegerSchema(5, 1, 25).describe("Maximum number of semantic issue items to return."),
|
|
425
|
+
cursor: z.string().min(1).optional().describe("Opaque cursor from a previous semantic issues call."),
|
|
426
|
+
statuses: z.array(z.enum(["pending", "stale", "failed"])).max(3).optional().describe("Optional issue statuses to include.")
|
|
427
|
+
},
|
|
428
|
+
outputSchema: z.object({
|
|
429
|
+
items: z.array(z.object({
|
|
430
|
+
nodeId: z.string(),
|
|
431
|
+
title: z.string().nullable(),
|
|
432
|
+
embeddingStatus: z.enum(["pending", "processing", "stale", "ready", "failed"]),
|
|
433
|
+
staleReason: z.string().nullable(),
|
|
434
|
+
updatedAt: z.string()
|
|
435
|
+
})),
|
|
436
|
+
nextCursor: z.string().nullable()
|
|
437
|
+
})
|
|
438
|
+
}, async ({ limit, cursor, statuses }) => {
|
|
439
|
+
const params = new URLSearchParams();
|
|
440
|
+
params.set("limit", String(limit));
|
|
441
|
+
if (cursor) {
|
|
442
|
+
params.set("cursor", cursor);
|
|
443
|
+
}
|
|
444
|
+
if (statuses?.length) {
|
|
445
|
+
params.set("statuses", statuses.join(","));
|
|
446
|
+
}
|
|
447
|
+
return toolResult(await apiClient.get(`/semantic/issues?${params.toString()}`));
|
|
448
|
+
});
|
|
449
|
+
registerReadOnlyTool(server, "recallx_search_nodes", {
|
|
450
|
+
title: "Search Nodes",
|
|
451
|
+
description: "Search durable RecallX nodes by keyword and optional filters. Prefer this for durable-only recall, especially when checking for an existing project in the current workspace by filtering with type=project. Valid node types include note, project, idea, question, decision, reference, artifact_ref, and conversation. `activity` is not a node type.",
|
|
452
|
+
inputSchema: {
|
|
453
|
+
query: z.string().default("").describe("Keyword or phrase query."),
|
|
454
|
+
allowEmptyQuery: coerceBooleanSchema(false).describe("Set true to browse recent durable nodes without a query."),
|
|
455
|
+
type: stringOrStringArraySchema.optional().describe("Alias for filters.types. Example: `note`."),
|
|
456
|
+
types: stringOrStringArraySchema.optional().describe("Node type filter. Accepts a string or array."),
|
|
457
|
+
status: stringOrStringArraySchema.optional().describe("Alias for filters.status."),
|
|
458
|
+
tag: stringOrStringArraySchema.optional().describe("Alias for filters.tags."),
|
|
459
|
+
filters: z
|
|
460
|
+
.object({
|
|
461
|
+
types: stringOrStringArraySchema.optional(),
|
|
462
|
+
status: stringOrStringArraySchema.optional(),
|
|
463
|
+
sourceLabels: stringOrStringArraySchema.optional(),
|
|
464
|
+
tags: stringOrStringArraySchema.optional()
|
|
465
|
+
})
|
|
466
|
+
.default({}),
|
|
467
|
+
limit: coerceIntegerSchema(10, 1, 100),
|
|
468
|
+
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
469
|
+
sort: z.enum(["relevance", "updated_at"]).default("relevance")
|
|
470
|
+
}
|
|
471
|
+
}, createNormalizedPostToolHandler(apiClient, "/nodes/search", normalizeNodeSearchInput));
|
|
472
|
+
registerReadOnlyTool(server, "recallx_search_activities", {
|
|
473
|
+
title: "Search Activities",
|
|
474
|
+
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.",
|
|
475
|
+
inputSchema: {
|
|
476
|
+
query: z.string().default("").describe("Keyword or phrase query."),
|
|
477
|
+
allowEmptyQuery: coerceBooleanSchema(false).describe("Set true to browse recent activity results without a query."),
|
|
478
|
+
activityType: stringOrStringArraySchema.optional().describe("Alias for filters.activityTypes."),
|
|
479
|
+
targetNodeId: stringOrStringArraySchema.optional().describe("Alias for filters.targetNodeIds."),
|
|
480
|
+
filters: z
|
|
481
|
+
.object({
|
|
482
|
+
targetNodeIds: stringOrStringArraySchema.optional(),
|
|
483
|
+
activityTypes: stringOrStringArraySchema.optional(),
|
|
484
|
+
sourceLabels: stringOrStringArraySchema.optional(),
|
|
485
|
+
createdAfter: z.string().optional(),
|
|
486
|
+
createdBefore: z.string().optional()
|
|
487
|
+
})
|
|
488
|
+
.default({}),
|
|
489
|
+
limit: coerceIntegerSchema(10, 1, 100),
|
|
490
|
+
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
491
|
+
sort: z.enum(["relevance", "updated_at"]).default("relevance")
|
|
492
|
+
}
|
|
493
|
+
}, createNormalizedPostToolHandler(apiClient, "/activities/search", normalizeActivitySearchInput));
|
|
494
|
+
registerReadOnlyTool(server, "recallx_search_workspace", {
|
|
495
|
+
title: "Search Workspace",
|
|
496
|
+
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\"`.",
|
|
497
|
+
inputSchema: {
|
|
498
|
+
query: z.string().default("").describe("Keyword or phrase query."),
|
|
499
|
+
allowEmptyQuery: coerceBooleanSchema(false).describe("Set true to browse mixed recent results without a query."),
|
|
500
|
+
scope: workspaceScopeInputSchema.optional().describe("Alias for scopes. Use a single scope like `activities`, not a comma-separated string."),
|
|
501
|
+
scopes: workspaceScopeInputSchema.optional().describe("Array of scopes such as `[\"nodes\", \"activities\"]`."),
|
|
502
|
+
nodeFilters: z
|
|
503
|
+
.object({
|
|
504
|
+
types: stringOrStringArraySchema.optional(),
|
|
505
|
+
status: stringOrStringArraySchema.optional(),
|
|
506
|
+
sourceLabels: stringOrStringArraySchema.optional(),
|
|
507
|
+
tags: stringOrStringArraySchema.optional()
|
|
508
|
+
})
|
|
509
|
+
.optional(),
|
|
510
|
+
activityFilters: z
|
|
511
|
+
.object({
|
|
512
|
+
targetNodeIds: stringOrStringArraySchema.optional(),
|
|
513
|
+
activityTypes: stringOrStringArraySchema.optional(),
|
|
514
|
+
sourceLabels: stringOrStringArraySchema.optional(),
|
|
515
|
+
createdAfter: z.string().optional(),
|
|
516
|
+
createdBefore: z.string().optional()
|
|
517
|
+
})
|
|
518
|
+
.optional(),
|
|
519
|
+
limit: coerceIntegerSchema(10, 1, 100),
|
|
520
|
+
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
521
|
+
sort: z.enum(["relevance", "updated_at", "smart"]).default("relevance")
|
|
522
|
+
}
|
|
523
|
+
}, createNormalizedPostToolHandler(apiClient, "/search", normalizeWorkspaceSearchInput));
|
|
524
|
+
registerReadOnlyTool(server, "recallx_get_node", {
|
|
525
|
+
title: "Get Node",
|
|
526
|
+
description: "Fetch a node together with its related nodes, activities, artifacts, and provenance.",
|
|
527
|
+
inputSchema: {
|
|
528
|
+
nodeId: z.string().min(1).describe("Target node id.")
|
|
529
|
+
}
|
|
530
|
+
}, async ({ nodeId }) => toolResult(await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}`)));
|
|
531
|
+
registerReadOnlyTool(server, "recallx_get_related", {
|
|
532
|
+
title: "Get Node Neighborhood",
|
|
533
|
+
description: "Fetch the canonical RecallX node neighborhood with optional inferred relations.",
|
|
534
|
+
inputSchema: {
|
|
535
|
+
nodeId: z.string().min(1).describe("Target node id."),
|
|
536
|
+
depth: coerceIntegerSchema(1, 1, 1),
|
|
537
|
+
relationTypes: z.array(z.enum(relationTypes)).default([]),
|
|
538
|
+
includeInferred: coerceBooleanSchema(true),
|
|
539
|
+
maxInferred: coerceIntegerSchema(4, 0, 10)
|
|
540
|
+
}
|
|
541
|
+
}, async ({ nodeId, depth, relationTypes: relationTypeFilter, includeInferred, maxInferred }) => {
|
|
542
|
+
const query = new URLSearchParams({
|
|
543
|
+
depth: String(depth),
|
|
544
|
+
include_inferred: includeInferred ? "1" : "0",
|
|
545
|
+
max_inferred: String(maxInferred)
|
|
546
|
+
});
|
|
547
|
+
if (relationTypeFilter.length) {
|
|
548
|
+
query.set("types", relationTypeFilter.join(","));
|
|
549
|
+
}
|
|
550
|
+
return toolResult(await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}/neighborhood?${query.toString()}`));
|
|
551
|
+
});
|
|
552
|
+
registerTool(server, "recallx_upsert_inferred_relation", {
|
|
553
|
+
title: "Upsert Inferred Relation",
|
|
554
|
+
description: "Upsert a lightweight inferred relation for retrieval, graph expansion, and later weight adjustment.",
|
|
555
|
+
inputSchema: {
|
|
556
|
+
fromNodeId: z.string().min(1),
|
|
557
|
+
toNodeId: z.string().min(1),
|
|
558
|
+
relationType: z.enum(relationTypes),
|
|
559
|
+
baseScore: z.number(),
|
|
560
|
+
usageScore: z.number().default(0),
|
|
561
|
+
finalScore: z.number(),
|
|
562
|
+
status: z.enum(inferredRelationStatuses).default("active"),
|
|
563
|
+
generator: z.string().min(1).describe("Short generator label such as deterministic-linker or coaccess-pass."),
|
|
564
|
+
evidence: jsonRecordSchema,
|
|
565
|
+
expiresAt: z.string().optional(),
|
|
566
|
+
metadata: jsonRecordSchema
|
|
567
|
+
}
|
|
568
|
+
}, createPostToolHandler(apiClient, "/inferred-relations"));
|
|
569
|
+
registerTool(server, "recallx_append_relation_usage_event", {
|
|
570
|
+
title: "Append Relation Usage Event",
|
|
571
|
+
description: "Append a lightweight usage signal after a relation actually helped retrieval or final output.",
|
|
572
|
+
inputSchema: {
|
|
573
|
+
relationId: z.string().min(1),
|
|
574
|
+
relationSource: z.enum(relationSources),
|
|
575
|
+
eventType: z.enum(relationUsageEventTypes),
|
|
576
|
+
sessionId: z.string().optional(),
|
|
577
|
+
runId: z.string().optional(),
|
|
578
|
+
source: sourceSchema.optional(),
|
|
579
|
+
delta: z.number(),
|
|
580
|
+
metadata: jsonRecordSchema
|
|
581
|
+
}
|
|
582
|
+
}, createPostToolHandler(apiClient, "/relation-usage-events"));
|
|
583
|
+
registerTool(server, "recallx_append_search_feedback", {
|
|
584
|
+
title: "Append Search Feedback",
|
|
585
|
+
description: "Append a usefulness signal for a node or activity search result after it helped or failed a task.",
|
|
586
|
+
inputSchema: {
|
|
587
|
+
resultType: z.enum(searchFeedbackResultTypes),
|
|
588
|
+
resultId: z.string().min(1),
|
|
589
|
+
verdict: z.enum(searchFeedbackVerdicts),
|
|
590
|
+
query: z.string().optional(),
|
|
591
|
+
sessionId: z.string().optional(),
|
|
592
|
+
runId: z.string().optional(),
|
|
593
|
+
source: sourceSchema.optional(),
|
|
594
|
+
confidence: z.number().min(0).max(1).default(1),
|
|
595
|
+
metadata: jsonRecordSchema
|
|
596
|
+
}
|
|
597
|
+
}, createPostToolHandler(apiClient, "/search-feedback-events"));
|
|
598
|
+
registerTool(server, "recallx_recompute_inferred_relations", {
|
|
599
|
+
title: "Recompute Inferred Relations",
|
|
600
|
+
description: "Run an explicit maintenance pass that refreshes inferred relation usage_score and final_score from usage events.",
|
|
601
|
+
inputSchema: {
|
|
602
|
+
relationIds: z.array(z.string().min(1)).max(200).optional(),
|
|
603
|
+
generator: z.string().min(1).optional(),
|
|
604
|
+
limit: z.number().int().min(1).max(500).default(100)
|
|
605
|
+
}
|
|
606
|
+
}, createPostToolHandler(apiClient, "/inferred-relations/recompute"));
|
|
607
|
+
registerTool(server, "recallx_append_activity", {
|
|
608
|
+
title: "Append Activity",
|
|
609
|
+
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.",
|
|
610
|
+
inputSchema: {
|
|
611
|
+
targetNodeId: z.string().min(1).describe("Target node id."),
|
|
612
|
+
activityType: z.enum(activityTypes),
|
|
613
|
+
body: z.string().default(""),
|
|
614
|
+
source: sourceSchema,
|
|
615
|
+
metadata: jsonRecordSchema
|
|
616
|
+
}
|
|
617
|
+
}, createPostToolHandler(apiClient, "/activities"));
|
|
618
|
+
registerTool(server, "recallx_capture_memory", {
|
|
619
|
+
title: "Capture Memory",
|
|
620
|
+
description: "Safely capture a memory item without choosing low-level storage first. Prefer this as the default write when the conversation is not yet tied to a specific project or node. General short logs can stay at workspace scope and be auto-routed into activities, while reusable content can still land as durable memory.",
|
|
621
|
+
inputSchema: {
|
|
622
|
+
mode: z.enum(captureModes).default("auto"),
|
|
623
|
+
body: z.string().min(1),
|
|
624
|
+
title: z.string().min(1).optional(),
|
|
625
|
+
targetNodeId: z.string().min(1).optional().describe("Optional target node for activity capture."),
|
|
626
|
+
nodeType: z.enum(nodeTypes).default("note"),
|
|
627
|
+
tags: z.array(z.string()).default([]),
|
|
628
|
+
source: sourceSchema,
|
|
629
|
+
metadata: jsonRecordSchema
|
|
630
|
+
}
|
|
631
|
+
}, createPostToolHandler(apiClient, "/capture"));
|
|
632
|
+
registerTool(server, "recallx_create_node", {
|
|
633
|
+
title: "Create Node",
|
|
634
|
+
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`.",
|
|
635
|
+
inputSchema: {
|
|
636
|
+
type: z.enum(nodeTypes),
|
|
637
|
+
title: z.string().min(1),
|
|
638
|
+
body: z.string().default(""),
|
|
639
|
+
summary: z.string().optional(),
|
|
640
|
+
tags: z.array(z.string()).default([]),
|
|
641
|
+
canonicality: z.enum(canonicalities).optional(),
|
|
642
|
+
status: z.enum(nodeStatuses).optional(),
|
|
643
|
+
source: sourceSchema,
|
|
644
|
+
metadata: jsonRecordSchema
|
|
645
|
+
}
|
|
646
|
+
}, async (input) => {
|
|
647
|
+
try {
|
|
648
|
+
return toolResult(await apiClient.post("/nodes", input));
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
if (error instanceof RecallXApiError &&
|
|
652
|
+
error.code === "FORBIDDEN" &&
|
|
653
|
+
error.message.includes("Short log-like agent output")) {
|
|
654
|
+
const redirectError = new Error(`${error.message} Hint: use recallx_capture_memory with mode=auto or mode=activity instead.`);
|
|
655
|
+
redirectError.code = "SHORT_LOG_REDIRECT";
|
|
656
|
+
throw redirectError;
|
|
657
|
+
}
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
registerTool(server, "recallx_create_nodes", {
|
|
662
|
+
title: "Create Nodes",
|
|
663
|
+
description: "Create multiple durable RecallX nodes with provenance. This batch endpoint allows partial success and returns per-item landing or error details.",
|
|
664
|
+
inputSchema: {
|
|
665
|
+
nodes: z
|
|
666
|
+
.array(z.object({
|
|
667
|
+
type: z.enum(nodeTypes),
|
|
668
|
+
title: z.string().min(1),
|
|
669
|
+
body: z.string().default(""),
|
|
670
|
+
summary: z.string().optional(),
|
|
671
|
+
tags: z.array(z.string()).default([]),
|
|
672
|
+
canonicality: z.enum(canonicalities).optional(),
|
|
673
|
+
status: z.enum(nodeStatuses).optional(),
|
|
674
|
+
source: sourceSchema,
|
|
675
|
+
metadata: jsonRecordSchema
|
|
676
|
+
}))
|
|
677
|
+
.min(1)
|
|
678
|
+
.max(100)
|
|
679
|
+
}
|
|
680
|
+
}, async (input) => toolResult(await apiClient.post("/nodes/batch", input)));
|
|
681
|
+
registerTool(server, "recallx_create_relation", {
|
|
682
|
+
title: "Create Relation",
|
|
683
|
+
description: "Create a relation between two nodes. Agent-created relations typically start suggested and are promoted automatically when confidence improves.",
|
|
684
|
+
inputSchema: {
|
|
685
|
+
fromNodeId: z.string().min(1),
|
|
686
|
+
toNodeId: z.string().min(1),
|
|
687
|
+
relationType: z.enum(relationTypes),
|
|
688
|
+
status: z.enum(relationStatuses).optional(),
|
|
689
|
+
source: sourceSchema,
|
|
690
|
+
metadata: jsonRecordSchema
|
|
691
|
+
}
|
|
692
|
+
}, createPostToolHandler(apiClient, "/relations"));
|
|
693
|
+
registerReadOnlyTool(server, "recallx_list_governance_issues", {
|
|
694
|
+
title: "List Governance Issues",
|
|
695
|
+
description: "List contested or low-confidence governance items that may need inspection.",
|
|
696
|
+
inputSchema: {
|
|
697
|
+
states: z.array(z.enum(governanceStates)).default(["contested", "low_confidence"]),
|
|
698
|
+
limit: z.number().int().min(1).max(100).default(20)
|
|
699
|
+
}
|
|
700
|
+
}, async ({ states, limit }) => {
|
|
701
|
+
const query = new URLSearchParams({
|
|
702
|
+
states: states.join(","),
|
|
703
|
+
limit: String(limit)
|
|
704
|
+
});
|
|
705
|
+
return toolResult(await apiClient.get(`/governance/issues?${query.toString()}`));
|
|
706
|
+
});
|
|
707
|
+
registerReadOnlyTool(server, "recallx_get_governance_state", {
|
|
708
|
+
title: "Get Governance State",
|
|
709
|
+
description: "Read the current automatic governance state and recent events for a node or relation.",
|
|
710
|
+
inputSchema: {
|
|
711
|
+
entityType: z.enum(["node", "relation"]),
|
|
712
|
+
entityId: z.string().min(1)
|
|
713
|
+
}
|
|
714
|
+
}, async ({ entityType, entityId }) => toolResult(await apiClient.get(`/governance/state/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`)));
|
|
715
|
+
registerTool(server, "recallx_recompute_governance", {
|
|
716
|
+
title: "Recompute Governance",
|
|
717
|
+
description: "Run a bounded automatic governance recompute pass for nodes, relations, or both.",
|
|
718
|
+
inputSchema: {
|
|
719
|
+
entityType: z.enum(["node", "relation"]).optional(),
|
|
720
|
+
entityIds: z.array(z.string().min(1)).max(200).optional(),
|
|
721
|
+
limit: z.number().int().min(1).max(500).default(100)
|
|
722
|
+
}
|
|
723
|
+
}, createPostToolHandler(apiClient, "/governance/recompute"));
|
|
724
|
+
registerReadOnlyTool(server, "recallx_context_bundle", {
|
|
725
|
+
title: "Build Context Bundle",
|
|
726
|
+
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.",
|
|
727
|
+
inputSchema: {
|
|
728
|
+
targetId: z.string().min(1).optional(),
|
|
729
|
+
mode: z.enum(bundleModes).default("compact"),
|
|
730
|
+
preset: z.enum(bundlePresets).default("for-assistant"),
|
|
731
|
+
options: z
|
|
732
|
+
.object({
|
|
733
|
+
includeRelated: coerceBooleanSchema(true),
|
|
734
|
+
includeInferred: coerceBooleanSchema(true),
|
|
735
|
+
includeRecentActivities: coerceBooleanSchema(true),
|
|
736
|
+
includeDecisions: coerceBooleanSchema(true),
|
|
737
|
+
includeOpenQuestions: coerceBooleanSchema(true),
|
|
738
|
+
maxInferred: coerceIntegerSchema(4, 0, 10),
|
|
739
|
+
maxItems: coerceIntegerSchema(10, 1, 30)
|
|
740
|
+
})
|
|
741
|
+
.default({
|
|
742
|
+
includeRelated: true,
|
|
743
|
+
includeInferred: true,
|
|
744
|
+
includeRecentActivities: true,
|
|
745
|
+
includeDecisions: true,
|
|
746
|
+
includeOpenQuestions: true,
|
|
747
|
+
maxInferred: 4,
|
|
748
|
+
maxItems: 10
|
|
749
|
+
})
|
|
750
|
+
}
|
|
751
|
+
}, async ({ targetId, ...input }) => toolResult(await apiClient.post("/context/bundles", {
|
|
752
|
+
...input,
|
|
753
|
+
...(targetId
|
|
754
|
+
? {
|
|
755
|
+
target: {
|
|
756
|
+
id: targetId
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
: {})
|
|
760
|
+
})));
|
|
761
|
+
registerTool(server, "recallx_semantic_reindex", {
|
|
762
|
+
title: "Queue Semantic Reindex",
|
|
763
|
+
description: "Queue semantic reindexing for a bounded set of recent active workspace nodes.",
|
|
764
|
+
inputSchema: {
|
|
765
|
+
limit: coerceIntegerSchema(250, 1, 1000)
|
|
766
|
+
}
|
|
767
|
+
}, createPostToolHandler(apiClient, "/semantic/reindex"));
|
|
768
|
+
registerTool(server, "recallx_semantic_reindex_node", {
|
|
769
|
+
title: "Queue Node Semantic Reindex",
|
|
770
|
+
description: "Queue semantic reindexing for a specific node id.",
|
|
771
|
+
inputSchema: {
|
|
772
|
+
nodeId: z.string().min(1)
|
|
773
|
+
}
|
|
774
|
+
}, async ({ nodeId }) => toolResult(await apiClient.post(`/semantic/reindex/${encodeURIComponent(nodeId)}`, {})));
|
|
775
|
+
registerReadOnlyTool(server, "recallx_rank_candidates", {
|
|
776
|
+
title: "Rank Candidate Nodes",
|
|
777
|
+
description: "Rank a bounded set of candidate node ids for a target using RecallX request-time retrieval scoring.",
|
|
778
|
+
inputSchema: {
|
|
779
|
+
query: z.string().default(""),
|
|
780
|
+
candidateNodeIds: z.array(z.string().min(1)).min(1).max(100),
|
|
781
|
+
preset: z.enum(bundlePresets).default("for-assistant"),
|
|
782
|
+
targetNodeId: z.string().optional()
|
|
783
|
+
}
|
|
784
|
+
}, createPostToolHandler(apiClient, "/retrieval/rank-candidates"));
|
|
785
|
+
return server;
|
|
786
|
+
}
|