recallx-headless 1.1.0 → 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/app/cli/src/cli.js +32 -1
- package/app/cli/src/format.js +14 -0
- package/app/mcp/server.js +248 -127
- package/app/server/app.js +412 -4
- package/app/server/config.js +4 -2
- package/app/server/index.js +12 -1
- package/app/server/project-graph.js +13 -6
- package/app/server/repositories.js +120 -8
- 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 +118 -6
- package/app/shared/contracts.js +41 -0
- package/app/shared/version.js +1 -1
- package/package.json +1 -1
package/app/cli/src/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
6
6
|
import { getApiBase, getAuthToken, requestJson } from "./http.js";
|
|
7
7
|
import { RECALLX_VERSION } from "../../shared/version.js";
|
|
8
8
|
import { applyCliUpdate, getCliUpdatePlan } from "./update.js";
|
|
9
|
-
import { renderActivitySearchResults, renderBundleMarkdown, renderGovernanceIssues, renderJson, renderNode, renderRelated, renderSearchResults, renderTelemetryErrors, renderTelemetrySummary, renderText, renderUpdateResult, renderWorkspaceSearchResults, renderWorkspaces, } from "./format.js";
|
|
9
|
+
import { renderActivitySearchResults, renderBundleMarkdown, renderGovernanceIssues, renderJson, renderNode, renderRelated, renderSearchResults, renderTelemetryErrors, renderTelemetrySummary, renderText, renderUpdateResult, renderWorkspaceBackups, renderWorkspaceSearchResults, renderWorkspaces, } from "./format.js";
|
|
10
10
|
const DEFAULT_SOURCE = {
|
|
11
11
|
actorType: "human",
|
|
12
12
|
actorLabel: "recallx-cli",
|
|
@@ -465,6 +465,27 @@ async function runWorkspace(apiBase, token, format, args, positionals) {
|
|
|
465
465
|
action,
|
|
466
466
|
rootPath: args.root || args.path || positionals[1],
|
|
467
467
|
});
|
|
468
|
+
case "backups": {
|
|
469
|
+
const data = await requestJson(apiBase, "/workspaces/backups", { token });
|
|
470
|
+
outputData(data, format, "workspace-backups");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
case "backup":
|
|
474
|
+
return runPostCommand(apiBase, token, format, "/workspaces/backups", "workspace-backup", {
|
|
475
|
+
label: args.label || args.name || positionals[1],
|
|
476
|
+
});
|
|
477
|
+
case "export":
|
|
478
|
+
return runPostCommand(apiBase, token, format, "/workspaces/export", "workspace-export", {
|
|
479
|
+
format: args.format || args.kind || "json",
|
|
480
|
+
});
|
|
481
|
+
case "restore":
|
|
482
|
+
validateRequired(args.backup || args.id || positionals[1], "workspace restore requires --backup");
|
|
483
|
+
validateRequired(args.root || args.path || positionals[2], "workspace restore requires --root");
|
|
484
|
+
return runPostCommand(apiBase, token, format, "/workspaces/restore", "workspace-restore", {
|
|
485
|
+
backupId: args.backup || args.id || positionals[1],
|
|
486
|
+
targetRootPath: args.root || args.path || positionals[2],
|
|
487
|
+
workspaceName: args.name || args.title,
|
|
488
|
+
});
|
|
468
489
|
}
|
|
469
490
|
throw new Error(`Unknown workspace action: ${action}`);
|
|
470
491
|
}
|
|
@@ -547,6 +568,9 @@ function outputData(data, format, command) {
|
|
|
547
568
|
case "workspace-list":
|
|
548
569
|
writeStdout(renderWorkspaces(payload));
|
|
549
570
|
return;
|
|
571
|
+
case "workspace-backups":
|
|
572
|
+
writeStdout(renderWorkspaceBackups(payload));
|
|
573
|
+
return;
|
|
550
574
|
case "append":
|
|
551
575
|
case "create":
|
|
552
576
|
case "link":
|
|
@@ -557,6 +581,9 @@ function outputData(data, format, command) {
|
|
|
557
581
|
case "workspace-current":
|
|
558
582
|
case "workspace-create":
|
|
559
583
|
case "workspace-open":
|
|
584
|
+
case "workspace-backup":
|
|
585
|
+
case "workspace-export":
|
|
586
|
+
case "workspace-restore":
|
|
560
587
|
writeStdout(renderText(payload));
|
|
561
588
|
return;
|
|
562
589
|
case "observability-summary":
|
|
@@ -696,6 +723,10 @@ Usage:
|
|
|
696
723
|
recallx workspace list
|
|
697
724
|
recallx workspace create --root /path/to/workspace [--name "Personal"]
|
|
698
725
|
recallx workspace open --root /path/to/workspace
|
|
726
|
+
recallx workspace backups
|
|
727
|
+
recallx workspace backup [--label "before-upgrade"]
|
|
728
|
+
recallx workspace export [--format json|markdown]
|
|
729
|
+
recallx workspace restore --backup <id> --root /path/to/restore [--name "Recovered"]
|
|
699
730
|
recallx observability summary [--since 24h]
|
|
700
731
|
recallx observability errors [--since 24h] [--surface mcp] [--limit 50]
|
|
701
732
|
|
package/app/cli/src/format.js
CHANGED
|
@@ -144,6 +144,20 @@ export function renderWorkspaces(data) {
|
|
|
144
144
|
.join("\n\n")}\n`;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
export function renderWorkspaceBackups(data) {
|
|
148
|
+
const items = data?.items || [];
|
|
149
|
+
if (!items.length) {
|
|
150
|
+
return "No backups.\n";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `${items
|
|
154
|
+
.map(
|
|
155
|
+
(item, index) =>
|
|
156
|
+
`${index + 1}. ${item.label || item.id}\n id: ${item.id}\n created: ${item.createdAt}\n path: ${item.backupPath}`,
|
|
157
|
+
)
|
|
158
|
+
.join("\n\n")}\n`;
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
export function renderUpdateResult(data) {
|
|
148
162
|
const lines = [
|
|
149
163
|
`package: ${data?.packageName || ""}`,
|
package/app/mcp/server.js
CHANGED
|
@@ -587,11 +587,55 @@ export function createRecallXMcpServer(params) {
|
|
|
587
587
|
const observability = createObservabilityWriter({
|
|
588
588
|
getState: () => currentObservabilityState
|
|
589
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
|
+
}
|
|
590
634
|
const server = new McpServer({
|
|
591
635
|
name: "recallx-mcp",
|
|
592
636
|
version: params?.serverVersion ?? RECALLX_VERSION
|
|
593
637
|
}, {
|
|
594
|
-
instructions: "Use RecallX as the default local memory layer for meaningful work, not as an afterthought. Treat the current workspace as the default scope, and do not create or open another workspace unless the user explicitly asks. Before making assumptions or starting a meaningful task, read context first: confirm the active workspace, use recallx_search_workspace as the broad default when the target is still unclear, narrow with recallx_search_nodes or recallx_search_activities when needed, and build a compact recallx_context_bundle before deep execution when a node or project is known. When the work is clearly project-shaped, search for an existing project inside the current workspace first, create one only if no suitable project exists, and then anchor follow-up context and routine logs to that project. Once a project is known, do not keep writing untargeted workspace captures for routine work logs: append activity to that project or pass targetNodeId on capture writes. Reserve workspace-scope inbox activity for genuinely untargeted, cross-project, or not-yet-classified short logs. Prefer read tools before durable writes, prefer compact context over repeated broad browsing, and write back concise summaries, decisions, or feedback when RecallX materially helped the task. Include source details on durable writes when you want caller-specific provenance.",
|
|
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.",
|
|
595
639
|
capabilities: {
|
|
596
640
|
logging: {}
|
|
597
641
|
}
|
|
@@ -645,19 +689,25 @@ export function createRecallXMcpServer(params) {
|
|
|
645
689
|
}),
|
|
646
690
|
outputSchema: healthOutputSchema
|
|
647
691
|
}, async () => toolResult("recallx_health", await apiClient.get("/health")));
|
|
648
|
-
registerReadOnlyTool(server, "
|
|
649
|
-
title: "
|
|
650
|
-
description: "Read the
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
title: "List Workspaces",
|
|
655
|
-
description: "List known RecallX workspaces and identify the currently active one.",
|
|
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
|
+
},
|
|
656
698
|
outputSchema: z.object({
|
|
657
699
|
current: workspaceInfoSchema,
|
|
658
|
-
items: z.array(workspaceInfoSchema.extend({ isCurrent: z.boolean(), lastOpenedAt: z.string() }))
|
|
700
|
+
items: z.array(workspaceInfoSchema.extend({ isCurrent: z.boolean(), lastOpenedAt: z.string() })).optional()
|
|
659
701
|
})
|
|
660
|
-
},
|
|
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
|
+
});
|
|
661
711
|
registerTool(server, "recallx_workspace_create", {
|
|
662
712
|
title: "Create Workspace",
|
|
663
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.",
|
|
@@ -673,9 +723,14 @@ export function createRecallXMcpServer(params) {
|
|
|
673
723
|
rootPath: z.string().min(1).describe("Existing workspace root path to open.")
|
|
674
724
|
}
|
|
675
725
|
}, createPostToolHandler("recallx_workspace_open", apiClient, "/workspaces/open"));
|
|
676
|
-
registerReadOnlyTool(server, "
|
|
677
|
-
title: "Semantic
|
|
678
|
-
description: "Read
|
|
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
|
+
},
|
|
679
734
|
outputSchema: z.object({
|
|
680
735
|
enabled: z.boolean(),
|
|
681
736
|
provider: z.string().nullable(),
|
|
@@ -688,37 +743,30 @@ export function createRecallXMcpServer(params) {
|
|
|
688
743
|
stale: z.number(),
|
|
689
744
|
ready: z.number(),
|
|
690
745
|
failed: z.number()
|
|
691
|
-
})
|
|
692
|
-
|
|
693
|
-
}, createGetToolHandler("recallx_semantic_status", apiClient, "/semantic/status"));
|
|
694
|
-
registerReadOnlyTool(server, "recallx_semantic_issues", {
|
|
695
|
-
title: "Semantic Index Issues",
|
|
696
|
-
description: "Read semantic indexing issues with optional status filters and cursor pagination.",
|
|
697
|
-
inputSchema: {
|
|
698
|
-
limit: coerceIntegerSchema(5, 1, 25).describe("Maximum number of semantic issue items to return."),
|
|
699
|
-
cursor: z.string().min(1).optional().describe("Opaque cursor from a previous semantic issues call."),
|
|
700
|
-
statuses: z.array(z.enum(["pending", "stale", "failed"])).max(3).optional().describe("Optional issue statuses to include.")
|
|
701
|
-
},
|
|
702
|
-
outputSchema: z.object({
|
|
703
|
-
items: z.array(z.object({
|
|
746
|
+
}),
|
|
747
|
+
issues: z.array(z.object({
|
|
704
748
|
nodeId: z.string(),
|
|
705
749
|
title: z.string().nullable(),
|
|
706
750
|
embeddingStatus: z.enum(["pending", "processing", "stale", "ready", "failed"]),
|
|
707
751
|
staleReason: z.string().nullable(),
|
|
708
752
|
updatedAt: z.string()
|
|
709
|
-
})),
|
|
710
|
-
nextCursor: z.string().nullable()
|
|
753
|
+
})).optional(),
|
|
754
|
+
nextCursor: z.string().nullable().optional()
|
|
711
755
|
})
|
|
712
|
-
}, async ({
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
if (
|
|
716
|
-
params
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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;
|
|
720
768
|
}
|
|
721
|
-
return toolResult("
|
|
769
|
+
return toolResult("recallx_semantic_overview", result);
|
|
722
770
|
});
|
|
723
771
|
registerReadOnlyTool(server, "recallx_search_nodes", {
|
|
724
772
|
title: "Search Nodes",
|
|
@@ -742,7 +790,14 @@ export function createRecallXMcpServer(params) {
|
|
|
742
790
|
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
743
791
|
sort: z.enum(["relevance", "updated_at"]).default("relevance")
|
|
744
792
|
}
|
|
745
|
-
},
|
|
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
|
+
});
|
|
746
801
|
registerReadOnlyTool(server, "recallx_search_activities", {
|
|
747
802
|
title: "Search Activities",
|
|
748
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.",
|
|
@@ -764,7 +819,14 @@ export function createRecallXMcpServer(params) {
|
|
|
764
819
|
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
765
820
|
sort: z.enum(["relevance", "updated_at"]).default("relevance")
|
|
766
821
|
}
|
|
767
|
-
},
|
|
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
|
+
});
|
|
768
830
|
registerReadOnlyTool(server, "recallx_search_workspace", {
|
|
769
831
|
title: "Search Workspace",
|
|
770
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\"`.",
|
|
@@ -794,7 +856,15 @@ export function createRecallXMcpServer(params) {
|
|
|
794
856
|
offset: coerceIntegerSchema(0, 0, 10_000),
|
|
795
857
|
sort: z.enum(["relevance", "updated_at", "smart"]).default("relevance")
|
|
796
858
|
}
|
|
797
|
-
},
|
|
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
|
+
});
|
|
798
868
|
registerReadOnlyTool(server, "recallx_get_node", {
|
|
799
869
|
title: "Get Node",
|
|
800
870
|
description: "Fetch a node together with its related nodes, activities, artifacts, and provenance.",
|
|
@@ -823,61 +893,102 @@ export function createRecallXMcpServer(params) {
|
|
|
823
893
|
}
|
|
824
894
|
return toolResult("recallx_get_related", await apiClient.get(`/nodes/${encodeURIComponent(nodeId)}/neighborhood?${query.toString()}`));
|
|
825
895
|
});
|
|
826
|
-
registerTool(server, "
|
|
827
|
-
title: "
|
|
828
|
-
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.",
|
|
829
899
|
inputSchema: {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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."),
|
|
836
907
|
status: z.enum(inferredRelationStatuses).default("active"),
|
|
837
|
-
generator: z.string().min(1).describe("
|
|
908
|
+
generator: z.string().min(1).optional().describe("Generator label for upsert or filter for recompute."),
|
|
838
909
|
evidence: jsonRecordSchema,
|
|
839
910
|
expiresAt: z.string().optional(),
|
|
840
|
-
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.")
|
|
841
914
|
}
|
|
842
|
-
},
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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));
|
|
855
934
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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.",
|
|
860
945
|
inputSchema: {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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."),
|
|
865
954
|
sessionId: z.string().optional(),
|
|
866
955
|
runId: z.string().optional(),
|
|
867
956
|
source: sourceSchema.optional(),
|
|
868
957
|
confidence: z.number().min(0).max(1).default(1),
|
|
958
|
+
delta: z.number().default(1).describe("Score delta for relation feedback."),
|
|
869
959
|
metadata: jsonRecordSchema
|
|
870
960
|
}
|
|
871
|
-
},
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
+
}));
|
|
977
|
+
}
|
|
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.");
|
|
879
980
|
}
|
|
880
|
-
|
|
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
|
+
});
|
|
881
992
|
registerTool(server, "recallx_append_activity", {
|
|
882
993
|
title: "Append Activity",
|
|
883
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.",
|
|
@@ -919,7 +1030,9 @@ export function createRecallXMcpServer(params) {
|
|
|
919
1030
|
}
|
|
920
1031
|
}, async (input) => {
|
|
921
1032
|
try {
|
|
922
|
-
|
|
1033
|
+
const result = await apiClient.post("/nodes", input);
|
|
1034
|
+
await autoAppendSearchFeedback();
|
|
1035
|
+
return toolResult("recallx_create_node", result);
|
|
923
1036
|
}
|
|
924
1037
|
catch (error) {
|
|
925
1038
|
if (error instanceof RecallXApiError &&
|
|
@@ -951,7 +1064,11 @@ export function createRecallXMcpServer(params) {
|
|
|
951
1064
|
.min(1)
|
|
952
1065
|
.max(100)
|
|
953
1066
|
}
|
|
954
|
-
}, 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
|
+
});
|
|
955
1072
|
registerTool(server, "recallx_create_relation", {
|
|
956
1073
|
title: "Create Relation",
|
|
957
1074
|
description: "Create a relation between two nodes. Agent-created relations typically start suggested and are promoted automatically when confidence improves.",
|
|
@@ -964,37 +1081,36 @@ export function createRecallXMcpServer(params) {
|
|
|
964
1081
|
metadata: jsonRecordSchema
|
|
965
1082
|
}
|
|
966
1083
|
}, createPostToolHandler("recallx_create_relation", apiClient, "/relations"));
|
|
967
|
-
registerReadOnlyTool(server, "
|
|
968
|
-
title: "
|
|
969
|
-
description: "
|
|
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.",
|
|
970
1087
|
inputSchema: {
|
|
971
|
-
|
|
972
|
-
|
|
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));
|
|
973
1107
|
}
|
|
974
|
-
}, async ({ states, limit }) => {
|
|
975
1108
|
const query = new URLSearchParams({
|
|
976
1109
|
states: states.join(","),
|
|
977
1110
|
limit: String(limit)
|
|
978
1111
|
});
|
|
979
|
-
return toolResult("
|
|
1112
|
+
return toolResult("recallx_governance", await apiClient.get(`/governance/issues?${query.toString()}`));
|
|
980
1113
|
});
|
|
981
|
-
registerReadOnlyTool(server, "recallx_get_governance_state", {
|
|
982
|
-
title: "Get Governance State",
|
|
983
|
-
description: "Read the current automatic governance state and recent events for a node or relation.",
|
|
984
|
-
inputSchema: {
|
|
985
|
-
entityType: z.enum(["node", "relation"]),
|
|
986
|
-
entityId: z.string().min(1)
|
|
987
|
-
}
|
|
988
|
-
}, async ({ entityType, entityId }) => toolResult("recallx_get_governance_state", await apiClient.get(`/governance/state/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`)));
|
|
989
|
-
registerTool(server, "recallx_recompute_governance", {
|
|
990
|
-
title: "Recompute Governance",
|
|
991
|
-
description: "Run a bounded automatic governance recompute pass for nodes, relations, or both.",
|
|
992
|
-
inputSchema: {
|
|
993
|
-
entityType: z.enum(["node", "relation"]).optional(),
|
|
994
|
-
entityIds: z.array(z.string().min(1)).max(200).optional(),
|
|
995
|
-
limit: z.number().int().min(1).max(500).default(100)
|
|
996
|
-
}
|
|
997
|
-
}, createPostToolHandler("recallx_recompute_governance", apiClient, "/governance/recompute"));
|
|
998
1114
|
registerReadOnlyTool(server, "recallx_context_bundle", {
|
|
999
1115
|
title: "Build Context Bundle",
|
|
1000
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.",
|
|
@@ -1022,30 +1138,35 @@ export function createRecallXMcpServer(params) {
|
|
|
1022
1138
|
maxItems: 10
|
|
1023
1139
|
})
|
|
1024
1140
|
}
|
|
1025
|
-
}, async ({ targetId, ...input }) =>
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1141
|
+
}, async ({ targetId, ...input }) => {
|
|
1142
|
+
const result = await apiClient.post("/context/bundles", {
|
|
1143
|
+
...input,
|
|
1144
|
+
...(targetId
|
|
1145
|
+
? {
|
|
1146
|
+
target: {
|
|
1147
|
+
id: targetId
|
|
1148
|
+
}
|
|
1031
1149
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
+
});
|
|
1035
1157
|
registerTool(server, "recallx_semantic_reindex", {
|
|
1036
|
-
title: "
|
|
1037
|
-
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.",
|
|
1038
1160
|
inputSchema: {
|
|
1039
|
-
|
|
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).")
|
|
1040
1163
|
}
|
|
1041
|
-
},
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
description: "Queue semantic reindexing for a specific node id.",
|
|
1045
|
-
inputSchema: {
|
|
1046
|
-
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)}`, {}));
|
|
1047
1167
|
}
|
|
1048
|
-
|
|
1168
|
+
return toolResult("recallx_semantic_reindex", await apiClient.post("/semantic/reindex", { limit }));
|
|
1169
|
+
});
|
|
1049
1170
|
registerReadOnlyTool(server, "recallx_rank_candidates", {
|
|
1050
1171
|
title: "Rank Candidate Nodes",
|
|
1051
1172
|
description: "Rank a bounded set of candidate node ids for a target using RecallX request-time retrieval scoring.",
|