@jcode.labs/ragmir 2.16.2 → 2.17.1
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 +37 -172
- package/dist/access-log.d.ts +1 -0
- package/dist/access-log.d.ts.map +1 -1
- package/dist/access-log.js +57 -24
- package/dist/access-log.js.map +1 -1
- package/dist/cli.js +15 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/config.js +6 -14
- package/dist/config.js.map +1 -1
- package/dist/context-resources.d.ts +3 -3
- package/dist/context-resources.d.ts.map +1 -1
- package/dist/context-resources.js +13 -4
- package/dist/context-resources.js.map +1 -1
- package/dist/destroy.d.ts.map +1 -1
- package/dist/destroy.js +10 -6
- package/dist/destroy.js.map +1 -1
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +11 -3
- package/dist/doctor.js.map +1 -1
- package/dist/embeddings.d.ts +4 -0
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +68 -9
- package/dist/embeddings.js.map +1 -1
- package/dist/evaluate.d.ts +5 -0
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +50 -9
- package/dist/evaluate.js.map +1 -1
- package/dist/files.d.ts +3 -10
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +49 -15
- package/dist/files.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +2 -2
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +356 -163
- package/dist/ingest.js.map +1 -1
- package/dist/ingestion-state.d.ts +53 -0
- package/dist/ingestion-state.d.ts.map +1 -0
- package/dist/ingestion-state.js +257 -0
- package/dist/ingestion-state.js.map +1 -0
- package/dist/mcp-output.d.ts +14 -0
- package/dist/mcp-output.d.ts.map +1 -1
- package/dist/mcp-output.js +121 -0
- package/dist/mcp-output.js.map +1 -1
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +362 -116
- package/dist/mcp.js.map +1 -1
- package/dist/ocr.d.ts.map +1 -1
- package/dist/ocr.js +6 -12
- package/dist/ocr.js.map +1 -1
- package/dist/operation.d.ts.map +1 -1
- package/dist/operation.js +3 -2
- package/dist/operation.js.map +1 -1
- package/dist/parsing.d.ts +1 -0
- package/dist/parsing.d.ts.map +1 -1
- package/dist/parsing.js +86 -27
- package/dist/parsing.js.map +1 -1
- package/dist/project-config-file.d.ts +11 -0
- package/dist/project-config-file.d.ts.map +1 -0
- package/dist/project-config-file.js +55 -0
- package/dist/project-config-file.js.map +1 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +12 -4
- package/dist/query.js.map +1 -1
- package/dist/research.d.ts.map +1 -1
- package/dist/research.js +3 -2
- package/dist/research.js.map +1 -1
- package/dist/security.d.ts +2 -2
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +57 -21
- package/dist/security.js.map +1 -1
- package/dist/semantic-config.d.ts.map +1 -1
- package/dist/semantic-config.js +16 -20
- package/dist/semantic-config.js.map +1 -1
- package/dist/skill.d.ts +2 -13
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js.map +1 -1
- package/dist/sources.d.ts.map +1 -1
- package/dist/sources.js +24 -40
- package/dist/sources.js.map +1 -1
- package/dist/store.d.ts +5 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +45 -13
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +39 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/skills/ragmir/SKILL.md +5 -3
- package/skills/ragmir-audio-summary/listening-style.md +11 -11
- package/skills/ragmir-audio-summary/narration-templates.md +22 -22
- package/skills/ragmir-audio-summary/split-lines.py +2 -1
package/dist/mcp.js
CHANGED
|
@@ -1,106 +1,253 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { accessLogUsageReport, recordMcpOutput } from "./access-log.js";
|
|
6
|
+
import { accessLogUsageReport, MAX_USAGE_REPORT_DAYS, recordMcpOutput } from "./access-log.js";
|
|
7
|
+
import { createRagmirClient } from "./client.js";
|
|
7
8
|
import { findProjectConfig, loadConfig } from "./config.js";
|
|
8
|
-
import { getKnowledgeBaseContext, getKnowledgeBaseSourceCatalog } from "./context-resources.js";
|
|
9
9
|
import { RAGMIR_PROJECT_ROOT_ENV } from "./defaults.js";
|
|
10
10
|
import { evaluateGoldenQueries } from "./evaluate.js";
|
|
11
11
|
import { audit } from "./ingest.js";
|
|
12
12
|
import { knowledgeBaseIdentity } from "./knowledge-bases.js";
|
|
13
13
|
import { ingestionLimits } from "./limits.js";
|
|
14
|
-
import { budgetMcpJson, fitAskPayload, fitExpandedCitation, fitResearchPayload, fitSearchPayload, MIN_MCP_OUTPUT_BYTES, resolveMcpOutputBudget, } from "./mcp-output.js";
|
|
14
|
+
import { budgetMcpJson, fitAskPayload, fitExpandedCitation, fitMcpJsonOutput, fitResearchPayload, fitSearchPayload, MIN_MCP_OUTPUT_BYTES, resolveMcpOutputBudget, } from "./mcp-output.js";
|
|
15
15
|
import { routePrompt } from "./prompt-routing.js";
|
|
16
|
-
import {
|
|
17
|
-
import { compactResearchReport, compactSearchResults, research } from "./research.js";
|
|
16
|
+
import { compactResearchReport, compactSearchResults } from "./research.js";
|
|
18
17
|
import { securityAudit } from "./security.js";
|
|
19
|
-
import { countRows } from "./store.js";
|
|
20
18
|
import { VERSION } from "./version.js";
|
|
21
|
-
const
|
|
22
|
-
|
|
19
|
+
const MAX_MCP_INPUT_CHARACTERS = 20_000;
|
|
20
|
+
const MAX_MCP_PATH_CHARACTERS = 500;
|
|
21
|
+
const MAX_MCP_OUTPUT_BYTES = 1_048_576;
|
|
22
|
+
const STRICT_MCP_FRESHNESS_WARNING = "Index freshness requires attention. Run `rgr doctor` locally for detailed diagnostics.";
|
|
23
|
+
const STRICT_MCP_NEXT_STEP = "Run `rgr doctor` locally for detailed next steps.";
|
|
24
|
+
const LOCAL_NON_DESTRUCTIVE_TOOL_ANNOTATIONS = {
|
|
25
|
+
readOnlyHint: false,
|
|
26
|
+
destructiveHint: false,
|
|
27
|
+
idempotentHint: false,
|
|
28
|
+
openWorldHint: false,
|
|
29
|
+
};
|
|
30
|
+
const PURE_LOCAL_TOOL_ANNOTATIONS = {
|
|
31
|
+
...LOCAL_NON_DESTRUCTIVE_TOOL_ANNOTATIONS,
|
|
32
|
+
readOnlyHint: true,
|
|
33
|
+
idempotentHint: true,
|
|
34
|
+
};
|
|
35
|
+
const POTENTIALLY_NETWORKED_TOOL_ANNOTATIONS = {
|
|
36
|
+
...LOCAL_NON_DESTRUCTIVE_TOOL_ANNOTATIONS,
|
|
37
|
+
openWorldHint: true,
|
|
38
|
+
};
|
|
39
|
+
const queryToolInputSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
query: z.string().trim().min(1).max(MAX_MCP_INPUT_CHARACTERS),
|
|
23
42
|
topK: z.number().int().positive().optional(),
|
|
24
43
|
contextRadius: z.number().int().min(0).optional(),
|
|
25
|
-
maxBytes: z.number().int().min(MIN_MCP_OUTPUT_BYTES).optional(),
|
|
26
|
-
includePaths: z.array(z.string().min(1).max(
|
|
27
|
-
excludePaths: z.array(z.string().min(1).max(
|
|
28
|
-
contextPaths: z.array(z.string().min(1).max(
|
|
44
|
+
maxBytes: z.number().int().min(MIN_MCP_OUTPUT_BYTES).max(MAX_MCP_OUTPUT_BYTES).optional(),
|
|
45
|
+
includePaths: z.array(z.string().min(1).max(MAX_MCP_PATH_CHARACTERS)).max(20).optional(),
|
|
46
|
+
excludePaths: z.array(z.string().min(1).max(MAX_MCP_PATH_CHARACTERS)).max(20).optional(),
|
|
47
|
+
contextPaths: z.array(z.string().min(1).max(MAX_MCP_PATH_CHARACTERS)).max(20).optional(),
|
|
29
48
|
explain: z.boolean().optional(),
|
|
30
|
-
})
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
31
51
|
const askToolInputSchema = queryToolInputSchema.extend({
|
|
32
52
|
compact: z.boolean().optional(),
|
|
33
53
|
});
|
|
34
|
-
const researchToolInputSchema = z
|
|
35
|
-
|
|
54
|
+
const researchToolInputSchema = z
|
|
55
|
+
.object({
|
|
56
|
+
query: z.string().trim().min(1).max(MAX_MCP_INPUT_CHARACTERS),
|
|
36
57
|
topK: z.number().int().positive().optional(),
|
|
37
58
|
includeCode: z.boolean().optional(),
|
|
38
59
|
compact: z.boolean().optional(),
|
|
39
|
-
maxBytes: z.number().int().min(MIN_MCP_OUTPUT_BYTES).optional(),
|
|
40
|
-
includePaths: z.array(z.string().min(1).max(
|
|
41
|
-
excludePaths: z.array(z.string().min(1).max(
|
|
42
|
-
contextPaths: z.array(z.string().min(1).max(
|
|
43
|
-
})
|
|
60
|
+
maxBytes: z.number().int().min(MIN_MCP_OUTPUT_BYTES).max(MAX_MCP_OUTPUT_BYTES).optional(),
|
|
61
|
+
includePaths: z.array(z.string().min(1).max(MAX_MCP_PATH_CHARACTERS)).max(20).optional(),
|
|
62
|
+
excludePaths: z.array(z.string().min(1).max(MAX_MCP_PATH_CHARACTERS)).max(20).optional(),
|
|
63
|
+
contextPaths: z.array(z.string().min(1).max(MAX_MCP_PATH_CHARACTERS)).max(20).optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
44
66
|
const searchToolInputSchema = queryToolInputSchema.extend({
|
|
45
67
|
compact: z.boolean().optional(),
|
|
46
68
|
});
|
|
47
|
-
const evaluateToolInputSchema = z
|
|
48
|
-
|
|
69
|
+
const evaluateToolInputSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
goldenPath: z.string().min(1).max(MAX_MCP_PATH_CHARACTERS),
|
|
49
72
|
topK: z.number().int().positive().optional(),
|
|
50
73
|
failUnder: z.number().min(0).max(1).optional(),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})
|
|
58
|
-
|
|
74
|
+
maxBytes: z.number().int().min(MIN_MCP_OUTPUT_BYTES).max(MAX_MCP_OUTPUT_BYTES).optional(),
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
const auditToolInputSchema = z
|
|
78
|
+
.object({
|
|
79
|
+
maxBytes: z.number().int().min(MIN_MCP_OUTPUT_BYTES).max(MAX_MCP_OUTPUT_BYTES).optional(),
|
|
80
|
+
})
|
|
81
|
+
.strict();
|
|
82
|
+
const usageReportInputSchema = z
|
|
83
|
+
.object({
|
|
84
|
+
days: z.number().int().positive().max(MAX_USAGE_REPORT_DAYS).optional(),
|
|
85
|
+
})
|
|
86
|
+
.strict();
|
|
87
|
+
const promptRouteInputSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
prompt: z.string().trim().min(1).max(MAX_MCP_INPUT_CHARACTERS),
|
|
90
|
+
})
|
|
91
|
+
.strict();
|
|
92
|
+
const expandToolInputSchema = z
|
|
93
|
+
.object({
|
|
59
94
|
citation: z.string().min(1).max(2_000),
|
|
60
95
|
contextRadius: z.number().int().min(0).max(3).optional(),
|
|
61
|
-
maxBytes: z.number().int().min(MIN_MCP_OUTPUT_BYTES).optional(),
|
|
62
|
-
})
|
|
96
|
+
maxBytes: z.number().int().min(MIN_MCP_OUTPUT_BYTES).max(MAX_MCP_OUTPUT_BYTES).optional(),
|
|
97
|
+
})
|
|
98
|
+
.strict();
|
|
99
|
+
class LifecycleMcpServer extends McpServer {
|
|
100
|
+
closeClient;
|
|
101
|
+
constructor(closeClient) {
|
|
102
|
+
super({
|
|
103
|
+
name: "ragmir",
|
|
104
|
+
version: VERSION,
|
|
105
|
+
});
|
|
106
|
+
this.closeClient = closeClient;
|
|
107
|
+
}
|
|
108
|
+
async close() {
|
|
109
|
+
try {
|
|
110
|
+
await super.close();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await this.closeClient();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function createMcpClientLifecycle(cwd) {
|
|
118
|
+
let clientPromise;
|
|
119
|
+
let clientConfigSignature;
|
|
120
|
+
let closePromise;
|
|
121
|
+
let closing = false;
|
|
122
|
+
return {
|
|
123
|
+
getClient(config) {
|
|
124
|
+
return (async () => {
|
|
125
|
+
const effectiveConfig = config ?? (await loadConfig(cwd));
|
|
126
|
+
if (closing) {
|
|
127
|
+
throw new Error("The MCP server is closed.");
|
|
128
|
+
}
|
|
129
|
+
const signature = JSON.stringify(effectiveConfig);
|
|
130
|
+
if (clientPromise && clientConfigSignature === signature) {
|
|
131
|
+
return clientPromise;
|
|
132
|
+
}
|
|
133
|
+
const previous = clientPromise;
|
|
134
|
+
const pending = (async () => {
|
|
135
|
+
if (previous) {
|
|
136
|
+
let previousClient;
|
|
137
|
+
try {
|
|
138
|
+
previousClient = await previous;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// A failed lazy initialization has no open client to close.
|
|
142
|
+
}
|
|
143
|
+
await previousClient?.close();
|
|
144
|
+
}
|
|
145
|
+
if (closing) {
|
|
146
|
+
throw new Error("The MCP server is closed.");
|
|
147
|
+
}
|
|
148
|
+
return createRagmirClient({ cwd });
|
|
149
|
+
})();
|
|
150
|
+
clientPromise = pending;
|
|
151
|
+
clientConfigSignature = signature;
|
|
152
|
+
void pending.catch(() => {
|
|
153
|
+
if (clientPromise === pending) {
|
|
154
|
+
clientPromise = undefined;
|
|
155
|
+
clientConfigSignature = undefined;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return pending;
|
|
159
|
+
})();
|
|
160
|
+
},
|
|
161
|
+
close() {
|
|
162
|
+
closing = true;
|
|
163
|
+
if (!closePromise) {
|
|
164
|
+
closePromise = (async () => {
|
|
165
|
+
const pending = clientPromise;
|
|
166
|
+
if (!pending) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
let client;
|
|
170
|
+
try {
|
|
171
|
+
client = await pending;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// A failed lazy initialization has no open client to close.
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
await client.close();
|
|
178
|
+
})();
|
|
179
|
+
}
|
|
180
|
+
return closePromise;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
63
184
|
export function createMcpServer(cwd = resolveMcpProjectRoot()) {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
185
|
+
const clientLifecycle = createMcpClientLifecycle(cwd);
|
|
186
|
+
const server = new LifecycleMcpServer(() => clientLifecycle.close());
|
|
187
|
+
server.server.onclose = () => {
|
|
188
|
+
void clientLifecycle.close().catch(() => undefined);
|
|
189
|
+
};
|
|
68
190
|
server.registerResource("ragmir-context", "ragmir://context", {
|
|
69
191
|
title: "Ragmir Knowledge Base Context",
|
|
70
192
|
description: "Active base identity, readiness, freshness, coverage, and available operations.",
|
|
71
193
|
mimeType: "application/json",
|
|
72
|
-
}, async (uri) =>
|
|
194
|
+
}, async (uri, { signal }) => {
|
|
195
|
+
throwIfMcpAborted(signal);
|
|
196
|
+
const config = await loadConfig(cwd);
|
|
197
|
+
const client = await clientLifecycle.getClient(config);
|
|
198
|
+
const context = await abortableMcpOperation(client.status({ signal }), signal);
|
|
199
|
+
const output = config.privacyProfile !== "strict"
|
|
200
|
+
? context
|
|
201
|
+
: {
|
|
202
|
+
...context,
|
|
203
|
+
indexFreshness: {
|
|
204
|
+
...context.indexFreshness,
|
|
205
|
+
warning: context.indexFreshness.warning === null ? null : STRICT_MCP_FRESHNESS_WARNING,
|
|
206
|
+
},
|
|
207
|
+
nextSteps: context.nextSteps.length === 0 ? [] : [STRICT_MCP_NEXT_STEP],
|
|
208
|
+
};
|
|
209
|
+
return jsonResource(uri, output, mcpOutputBudget(config.mcpMaxOutputBytes), "ragmir://context");
|
|
210
|
+
});
|
|
73
211
|
server.registerResource("ragmir-sources", "ragmir://sources", {
|
|
74
212
|
title: "Ragmir Source Catalog",
|
|
75
213
|
description: "Bounded source coverage, skipped-file counts, and index drift for the active base.",
|
|
76
214
|
mimeType: "application/json",
|
|
77
|
-
}, async (uri) =>
|
|
215
|
+
}, async (uri, { signal }) => {
|
|
216
|
+
throwIfMcpAborted(signal);
|
|
217
|
+
const config = await loadConfig(cwd);
|
|
218
|
+
const client = await clientLifecycle.getClient(config);
|
|
219
|
+
const sources = await abortableMcpOperation(client.sources({ signal }), signal);
|
|
220
|
+
return jsonResource(uri, sources, mcpOutputBudget(config.mcpMaxOutputBytes), "ragmir://sources");
|
|
221
|
+
});
|
|
78
222
|
server.registerTool("ragmir_status", {
|
|
79
223
|
title: "Ragmir Status",
|
|
80
224
|
description: "Show active Ragmir configuration and indexed chunk count.",
|
|
81
|
-
inputSchema: z.object({}),
|
|
82
|
-
|
|
225
|
+
inputSchema: z.object({}).strict(),
|
|
226
|
+
annotations: LOCAL_NON_DESTRUCTIVE_TOOL_ANNOTATIONS,
|
|
227
|
+
}, async (_input, { signal }) => {
|
|
228
|
+
throwIfMcpAborted(signal);
|
|
83
229
|
const config = await loadConfig(cwd);
|
|
230
|
+
const client = await clientLifecycle.getClient(config);
|
|
231
|
+
const context = await abortableMcpOperation(client.status({ signal }), signal);
|
|
84
232
|
const identity = knowledgeBaseIdentity(config.projectRoot);
|
|
85
|
-
const chunksIndexed = await countRows(config);
|
|
86
233
|
const strict = config.privacyProfile === "strict";
|
|
87
234
|
const output = {
|
|
88
235
|
knowledgeBaseId: identity?.id ?? null,
|
|
89
236
|
projectRoot: strict ? "." : config.projectRoot,
|
|
90
|
-
rawDir: strict ?
|
|
237
|
+
rawDir: strict ? privateProjectPath(config.projectRoot, config.rawDir) : config.rawDir,
|
|
91
238
|
storageDir: strict
|
|
92
|
-
?
|
|
239
|
+
? privateProjectPath(config.projectRoot, config.storageDir)
|
|
93
240
|
: config.storageDir,
|
|
94
241
|
sourcesFile: strict
|
|
95
|
-
?
|
|
242
|
+
? privateProjectPath(config.projectRoot, config.sourcesFile)
|
|
96
243
|
: config.sourcesFile,
|
|
97
244
|
privacyProfile: config.privacyProfile,
|
|
98
245
|
retrievalProfile: config.retrievalProfile,
|
|
99
246
|
embeddingProvider: config.embeddingProvider,
|
|
100
|
-
embeddingModel: config.embeddingModel,
|
|
247
|
+
embeddingModel: strict ? privateMcpPath(config.embeddingModel) : config.embeddingModel,
|
|
101
248
|
embeddingModelRevision: config.embeddingModelRevision,
|
|
102
249
|
embeddingModelPath: strict
|
|
103
|
-
?
|
|
250
|
+
? privateProjectPath(config.projectRoot, config.embeddingModelPath)
|
|
104
251
|
: config.embeddingModelPath,
|
|
105
252
|
transformersAllowRemoteModels: config.transformersAllowRemoteModels,
|
|
106
253
|
llmGeneration: false,
|
|
@@ -118,29 +265,37 @@ export function createMcpServer(cwd = resolveMcpProjectRoot()) {
|
|
|
118
265
|
legacyWordCommand: config.legacyWordCommand,
|
|
119
266
|
legacyWordTimeoutMs: config.legacyWordTimeoutMs,
|
|
120
267
|
ingestionLimits: ingestionLimits(config),
|
|
121
|
-
chunksIndexed,
|
|
268
|
+
chunksIndexed: context.coverage.chunksIndexed,
|
|
122
269
|
};
|
|
123
|
-
return
|
|
270
|
+
return boundedJsonResult(output, mcpOutputBudget(config.mcpMaxOutputBytes), "ragmir_status");
|
|
124
271
|
});
|
|
125
272
|
server.registerTool("ragmir_route_prompt", {
|
|
126
273
|
title: "Ragmir Prompt Router",
|
|
127
274
|
description: "Classify a prompt and suggest whether an agent should use Ragmir local context.",
|
|
128
275
|
inputSchema: promptRouteInputSchema,
|
|
129
|
-
|
|
276
|
+
annotations: PURE_LOCAL_TOOL_ANNOTATIONS,
|
|
277
|
+
}, async ({ prompt }, { signal }) => {
|
|
278
|
+
throwIfMcpAborted(signal);
|
|
279
|
+
const config = await loadConfig(cwd);
|
|
280
|
+
return boundedJsonResult(routePrompt(prompt), mcpOutputBudget(config.mcpMaxOutputBytes), "ragmir_route_prompt");
|
|
281
|
+
});
|
|
130
282
|
server.registerTool("ragmir_search", {
|
|
131
283
|
title: "Ragmir Search",
|
|
132
284
|
description: "Retrieve relevant passages from the local Ragmir knowledge base.",
|
|
133
285
|
inputSchema: searchToolInputSchema,
|
|
286
|
+
annotations: POTENTIALLY_NETWORKED_TOOL_ANNOTATIONS,
|
|
134
287
|
}, async ({ query, topK, contextRadius, compact, maxBytes, includePaths, excludePaths, contextPaths, explain, }, { signal }) => {
|
|
288
|
+
throwIfMcpAborted(signal);
|
|
135
289
|
const config = await loadConfig(cwd);
|
|
136
290
|
const options = await searchOptions(cwd, topK, contextRadius, includePaths, excludePaths, contextPaths, explain);
|
|
137
|
-
const
|
|
291
|
+
const client = await clientLifecycle.getClient(config);
|
|
292
|
+
const results = await client.search(query, { ...options, signal });
|
|
138
293
|
const compactResults = compactSearchResults(results);
|
|
139
294
|
const compactOutput = config.privacyProfile === "strict" || compact === true;
|
|
140
295
|
const preferred = compactOutput ? compactResults : results;
|
|
141
296
|
const bounded = budgetMcpJson({
|
|
142
297
|
tool: "ragmir_search",
|
|
143
|
-
maxBytes:
|
|
298
|
+
maxBytes: mcpOutputBudget(config.mcpMaxOutputBytes, maxBytes),
|
|
144
299
|
fullValue: results,
|
|
145
300
|
preferredValue: preferred,
|
|
146
301
|
compactValue: compactResults,
|
|
@@ -154,13 +309,16 @@ export function createMcpServer(cwd = resolveMcpProjectRoot()) {
|
|
|
154
309
|
title: "Ragmir Ask",
|
|
155
310
|
description: "Return cited retrieval context for a question without calling an LLM.",
|
|
156
311
|
inputSchema: askToolInputSchema,
|
|
312
|
+
annotations: POTENTIALLY_NETWORKED_TOOL_ANNOTATIONS,
|
|
157
313
|
}, async ({ query, topK, contextRadius, compact, maxBytes, includePaths, excludePaths, contextPaths, explain, }, { signal }) => {
|
|
314
|
+
throwIfMcpAborted(signal);
|
|
158
315
|
const config = await loadConfig(cwd);
|
|
159
316
|
const options = await searchOptions(cwd, topK, contextRadius, includePaths, excludePaths, contextPaths, explain);
|
|
160
317
|
const cancellableOptions = { ...options, signal };
|
|
318
|
+
const client = await clientLifecycle.getClient(config);
|
|
161
319
|
let fullPayload;
|
|
162
320
|
if (config.privacyProfile === "strict") {
|
|
163
|
-
const results = await search(query, cancellableOptions);
|
|
321
|
+
const results = await client.search(query, cancellableOptions);
|
|
164
322
|
fullPayload = {
|
|
165
323
|
answer: "Strict privacy profile returns compact cited retrieval only.",
|
|
166
324
|
sources: results,
|
|
@@ -168,7 +326,7 @@ export function createMcpServer(cwd = resolveMcpProjectRoot()) {
|
|
|
168
326
|
};
|
|
169
327
|
}
|
|
170
328
|
else {
|
|
171
|
-
fullPayload = await ask(query, cancellableOptions);
|
|
329
|
+
fullPayload = await client.ask(query, cancellableOptions);
|
|
172
330
|
}
|
|
173
331
|
const compactPayload = {
|
|
174
332
|
answer: "Ragmir returns compact cited retrieval only. Expand a citation when needed.",
|
|
@@ -178,7 +336,7 @@ export function createMcpServer(cwd = resolveMcpProjectRoot()) {
|
|
|
178
336
|
const compactOutput = config.privacyProfile === "strict" || compact === true;
|
|
179
337
|
const bounded = budgetMcpJson({
|
|
180
338
|
tool: "ragmir_ask",
|
|
181
|
-
maxBytes:
|
|
339
|
+
maxBytes: mcpOutputBudget(config.mcpMaxOutputBytes, maxBytes),
|
|
182
340
|
fullValue: fullPayload,
|
|
183
341
|
preferredValue: compactOutput ? compactPayload : fullPayload,
|
|
184
342
|
compactValue: compactPayload,
|
|
@@ -192,22 +350,25 @@ export function createMcpServer(cwd = resolveMcpProjectRoot()) {
|
|
|
192
350
|
title: "Ragmir Research",
|
|
193
351
|
description: "Run an audit-backed multi-query research pass with cited evidence and optional code matches.",
|
|
194
352
|
inputSchema: researchToolInputSchema,
|
|
353
|
+
annotations: POTENTIALLY_NETWORKED_TOOL_ANNOTATIONS,
|
|
195
354
|
}, async ({ query, topK, includeCode, compact, maxBytes, includePaths, excludePaths, contextPaths }, { signal }) => {
|
|
355
|
+
throwIfMcpAborted(signal);
|
|
196
356
|
const config = await loadConfig(cwd);
|
|
197
357
|
const options = await searchOptions(cwd, topK, undefined, includePaths, excludePaths, contextPaths);
|
|
198
|
-
const researchOptions = {
|
|
358
|
+
const researchOptions = { signal };
|
|
199
359
|
addOption(researchOptions, "topK", options.topK);
|
|
200
360
|
addOption(researchOptions, "includeCode", includeCode);
|
|
201
361
|
addOption(researchOptions, "includePaths", options.includePaths);
|
|
202
362
|
addOption(researchOptions, "excludePaths", options.excludePaths);
|
|
203
363
|
addOption(researchOptions, "contextPaths", options.contextPaths);
|
|
204
|
-
const
|
|
364
|
+
const client = await clientLifecycle.getClient(config);
|
|
365
|
+
const result = await client.research(query, researchOptions);
|
|
205
366
|
const compactResult = compactResearchReport(result);
|
|
206
367
|
const compactOutput = config.privacyProfile === "strict" || compact === true;
|
|
207
368
|
const preferred = compactOutput ? compactResult : result;
|
|
208
369
|
const bounded = budgetMcpJson({
|
|
209
370
|
tool: "ragmir_research",
|
|
210
|
-
maxBytes:
|
|
371
|
+
maxBytes: mcpOutputBudget(config.mcpMaxOutputBytes, maxBytes),
|
|
211
372
|
fullValue: result,
|
|
212
373
|
preferredValue: preferred,
|
|
213
374
|
compactValue: compactResult,
|
|
@@ -221,16 +382,18 @@ export function createMcpServer(cwd = resolveMcpProjectRoot()) {
|
|
|
221
382
|
title: "Ragmir Expand",
|
|
222
383
|
description: "Expand one Ragmir citation into a bounded exact passage window.",
|
|
223
384
|
inputSchema: expandToolInputSchema,
|
|
385
|
+
annotations: LOCAL_NON_DESTRUCTIVE_TOOL_ANNOTATIONS,
|
|
224
386
|
}, async ({ citation, contextRadius, maxBytes }, { signal }) => {
|
|
387
|
+
throwIfMcpAborted(signal);
|
|
225
388
|
const config = await loadConfig(cwd);
|
|
226
|
-
const
|
|
227
|
-
|
|
389
|
+
const client = await clientLifecycle.getClient(config);
|
|
390
|
+
const expanded = await client.expandCitation(citation, {
|
|
228
391
|
signal,
|
|
229
392
|
...(contextRadius === undefined ? {} : { contextRadius }),
|
|
230
393
|
});
|
|
231
394
|
const bounded = budgetMcpJson({
|
|
232
395
|
tool: "ragmir_expand",
|
|
233
|
-
maxBytes:
|
|
396
|
+
maxBytes: mcpOutputBudget(config.mcpMaxOutputBytes, maxBytes),
|
|
234
397
|
fullValue: expanded,
|
|
235
398
|
preferredValue: expanded,
|
|
236
399
|
compacted: false,
|
|
@@ -242,61 +405,87 @@ export function createMcpServer(cwd = resolveMcpProjectRoot()) {
|
|
|
242
405
|
server.registerTool("ragmir_audit", {
|
|
243
406
|
title: "Ragmir Audit",
|
|
244
407
|
description: "Compare supported source files on disk with the current vector index.",
|
|
245
|
-
inputSchema:
|
|
246
|
-
|
|
408
|
+
inputSchema: auditToolInputSchema,
|
|
409
|
+
annotations: LOCAL_NON_DESTRUCTIVE_TOOL_ANNOTATIONS,
|
|
410
|
+
}, async ({ maxBytes }, { signal }) => {
|
|
411
|
+
throwIfMcpAborted(signal);
|
|
412
|
+
const config = await loadConfig(cwd);
|
|
413
|
+
const report = await abortableMcpOperation(audit(cwd, { signal }), signal);
|
|
414
|
+
return boundedJsonResult(report, mcpOutputBudget(config.mcpMaxOutputBytes, maxBytes), "ragmir_audit");
|
|
415
|
+
});
|
|
247
416
|
server.registerTool("ragmir_evaluate", {
|
|
248
417
|
title: "Ragmir Evaluate",
|
|
249
418
|
description: "Measure retrieval recall against a local golden query file.",
|
|
250
419
|
inputSchema: evaluateToolInputSchema,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
420
|
+
annotations: POTENTIALLY_NETWORKED_TOOL_ANNOTATIONS,
|
|
421
|
+
}, async ({ goldenPath, topK, failUnder, maxBytes }, { signal }) => {
|
|
422
|
+
throwIfMcpAborted(signal);
|
|
423
|
+
const config = await loadConfig(cwd);
|
|
424
|
+
try {
|
|
425
|
+
const options = evaluationOptions(cwd, goldenPath, topK, config.mcpMaxTopK);
|
|
426
|
+
const result = await abortableMcpOperation(evaluateGoldenQueries({ ...options, signal }), signal);
|
|
427
|
+
const safeResult = { ...result, goldenPath: options.goldenPath };
|
|
428
|
+
const output = failUnder === undefined
|
|
429
|
+
? safeResult
|
|
430
|
+
: {
|
|
431
|
+
...safeResult,
|
|
432
|
+
minimumRecall: failUnder,
|
|
433
|
+
passed: result.recall >= failUnder,
|
|
434
|
+
};
|
|
435
|
+
return boundedJsonResult(output, mcpOutputBudget(config.mcpMaxOutputBytes, maxBytes), "ragmir_evaluate");
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
if (signal.aborted || config.privacyProfile !== "strict") {
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
throw new Error("ragmir_evaluate could not read or evaluate the project-relative golden file.");
|
|
255
442
|
}
|
|
256
|
-
const minimumRecall = failUnder;
|
|
257
|
-
return textResult({
|
|
258
|
-
...result,
|
|
259
|
-
minimumRecall,
|
|
260
|
-
passed: result.recall >= minimumRecall,
|
|
261
|
-
});
|
|
262
443
|
});
|
|
263
444
|
server.registerTool("ragmir_security_audit", {
|
|
264
445
|
title: "Ragmir Security Audit",
|
|
265
446
|
description: "Show local privacy, provider, redaction, MCP, and gitignore posture.",
|
|
266
|
-
inputSchema: z.object({}),
|
|
267
|
-
|
|
447
|
+
inputSchema: z.object({}).strict(),
|
|
448
|
+
annotations: PURE_LOCAL_TOOL_ANNOTATIONS,
|
|
449
|
+
}, async (_input, { signal }) => {
|
|
450
|
+
throwIfMcpAborted(signal);
|
|
268
451
|
const config = await loadConfig(cwd);
|
|
269
|
-
const report = await securityAudit(cwd);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
452
|
+
const report = await abortableMcpOperation(securityAudit(cwd, { signal }), signal);
|
|
453
|
+
const output = config.privacyProfile !== "strict"
|
|
454
|
+
? report
|
|
455
|
+
: {
|
|
456
|
+
...report,
|
|
457
|
+
projectRoot: ".",
|
|
458
|
+
providers: {
|
|
459
|
+
...report.providers,
|
|
460
|
+
embeddingModel: privateMcpPath(report.providers.embeddingModel),
|
|
461
|
+
embeddingModelPath: privateProjectPath(config.projectRoot, report.providers.embeddingModelPath),
|
|
462
|
+
},
|
|
463
|
+
accessLog: {
|
|
464
|
+
...report.accessLog,
|
|
465
|
+
path: privateProjectPath(config.projectRoot, report.accessLog.path),
|
|
466
|
+
},
|
|
467
|
+
storage: {
|
|
468
|
+
...report.storage,
|
|
469
|
+
path: privateProjectPath(config.projectRoot, report.storage.path),
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
return boundedJsonResult(output, mcpOutputBudget(config.mcpMaxOutputBytes), "ragmir_security_audit");
|
|
289
473
|
});
|
|
290
474
|
server.registerTool("ragmir_usage_report", {
|
|
291
475
|
title: "Ragmir Usage Report",
|
|
292
476
|
description: "Summarize the metadata-only local access log.",
|
|
293
477
|
inputSchema: usageReportInputSchema,
|
|
294
|
-
|
|
478
|
+
annotations: PURE_LOCAL_TOOL_ANNOTATIONS,
|
|
479
|
+
}, async ({ days }, { signal }) => {
|
|
480
|
+
throwIfMcpAborted(signal);
|
|
295
481
|
const options = { cwd };
|
|
482
|
+
options.signal = signal;
|
|
296
483
|
if (days !== undefined) {
|
|
297
484
|
options.days = days;
|
|
298
485
|
}
|
|
299
|
-
|
|
486
|
+
const config = await loadConfig(cwd);
|
|
487
|
+
const report = await abortableMcpOperation(accessLogUsageReport(options), signal);
|
|
488
|
+
return boundedJsonResult(report, mcpOutputBudget(config.mcpMaxOutputBytes), "ragmir_usage_report");
|
|
300
489
|
});
|
|
301
490
|
return server;
|
|
302
491
|
}
|
|
@@ -319,27 +508,61 @@ export function resolveMcpProjectRoot(env = process.env, fallback = process.cwd(
|
|
|
319
508
|
}
|
|
320
509
|
return env.CLAUDE_PROJECT_DIR ?? fallback;
|
|
321
510
|
}
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
content: [
|
|
325
|
-
{
|
|
326
|
-
type: "text",
|
|
327
|
-
text: JSON.stringify(value, null, 2),
|
|
328
|
-
},
|
|
329
|
-
],
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
function jsonResource(uri, value) {
|
|
511
|
+
function jsonResource(uri, value, maxBytes, source) {
|
|
512
|
+
const bounded = fitMcpJsonOutput(value, maxBytes, source);
|
|
333
513
|
return {
|
|
334
514
|
contents: [
|
|
335
515
|
{
|
|
336
516
|
uri: uri.href,
|
|
337
517
|
mimeType: "application/json",
|
|
338
|
-
text:
|
|
518
|
+
text: bounded.text,
|
|
339
519
|
},
|
|
340
520
|
],
|
|
521
|
+
_meta: { "ragmir/output": bounded.metadata },
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function boundedJsonResult(value, maxBytes, source) {
|
|
525
|
+
const bounded = fitMcpJsonOutput(value, maxBytes, source);
|
|
526
|
+
return {
|
|
527
|
+
content: [{ type: "text", text: bounded.text }],
|
|
528
|
+
_meta: { "ragmir/output": bounded.metadata },
|
|
341
529
|
};
|
|
342
530
|
}
|
|
531
|
+
function mcpOutputBudget(configured, requested) {
|
|
532
|
+
return resolveMcpOutputBudget(Math.min(configured, MAX_MCP_OUTPUT_BYTES), requested);
|
|
533
|
+
}
|
|
534
|
+
function throwIfMcpAborted(signal) {
|
|
535
|
+
if (!signal.aborted) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (signal.reason instanceof Error) {
|
|
539
|
+
throw signal.reason;
|
|
540
|
+
}
|
|
541
|
+
throw new Error("The MCP request was cancelled.");
|
|
542
|
+
}
|
|
543
|
+
function abortableMcpOperation(operation, signal) {
|
|
544
|
+
throwIfMcpAborted(signal);
|
|
545
|
+
return new Promise((resolve, reject) => {
|
|
546
|
+
const onAbort = () => {
|
|
547
|
+
cleanup();
|
|
548
|
+
try {
|
|
549
|
+
throwIfMcpAborted(signal);
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
reject(error);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
556
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
557
|
+
void operation.then((value) => {
|
|
558
|
+
cleanup();
|
|
559
|
+
resolve(value);
|
|
560
|
+
}, (error) => {
|
|
561
|
+
cleanup();
|
|
562
|
+
reject(error);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
343
566
|
async function recordBudgetedOutput(config, bounded) {
|
|
344
567
|
await recordMcpOutput(config, {
|
|
345
568
|
tool: bounded.metadata.tool,
|
|
@@ -364,27 +587,50 @@ export async function searchOptions(cwd, topK, contextRadius, includePaths, excl
|
|
|
364
587
|
addOption(result, "explain", explain);
|
|
365
588
|
return result;
|
|
366
589
|
}
|
|
367
|
-
|
|
368
|
-
const config = await loadConfig(cwd);
|
|
590
|
+
function evaluationOptions(cwd, goldenPath, topK, maxTopK) {
|
|
369
591
|
const result = {
|
|
370
592
|
cwd,
|
|
371
593
|
goldenPath: projectRelativeGoldenPath(cwd, goldenPath),
|
|
372
|
-
maxTopK
|
|
594
|
+
maxTopK,
|
|
373
595
|
};
|
|
374
596
|
if (topK === undefined) {
|
|
375
597
|
return result;
|
|
376
598
|
}
|
|
377
|
-
return { ...result, topK: Math.min(topK,
|
|
599
|
+
return { ...result, topK: Math.min(topK, maxTopK) };
|
|
378
600
|
}
|
|
379
601
|
export function projectRelativeGoldenPath(cwd, goldenPath) {
|
|
380
|
-
|
|
602
|
+
if (path.isAbsolute(goldenPath)) {
|
|
603
|
+
throw new Error("ragmir_evaluate goldenPath must stay inside the MCP project root.");
|
|
604
|
+
}
|
|
605
|
+
const root = realpathSync.native(path.resolve(cwd));
|
|
381
606
|
const absolutePath = path.resolve(root, goldenPath);
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
607
|
+
const lexicalRelativePath = path.relative(root, absolutePath);
|
|
608
|
+
if (pathEscapesRoot(lexicalRelativePath)) {
|
|
609
|
+
throw new Error("ragmir_evaluate goldenPath must stay inside the MCP project root.");
|
|
610
|
+
}
|
|
611
|
+
const resolvedPath = realpathSync.native(absolutePath);
|
|
612
|
+
const relativePath = path.relative(root, resolvedPath);
|
|
613
|
+
if (pathEscapesRoot(relativePath)) {
|
|
384
614
|
throw new Error("ragmir_evaluate goldenPath must stay inside the MCP project root.");
|
|
385
615
|
}
|
|
386
616
|
return relativePath;
|
|
387
617
|
}
|
|
618
|
+
function pathEscapesRoot(relativePath) {
|
|
619
|
+
return (relativePath.length === 0 ||
|
|
620
|
+
relativePath === ".." ||
|
|
621
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
622
|
+
path.isAbsolute(relativePath));
|
|
623
|
+
}
|
|
624
|
+
function privateMcpPath(value) {
|
|
625
|
+
return path.isAbsolute(value) ? "<absolute-path>" : value;
|
|
626
|
+
}
|
|
627
|
+
function privateProjectPath(projectRoot, value) {
|
|
628
|
+
const relativePath = path.relative(projectRoot, path.resolve(value));
|
|
629
|
+
if (relativePath.length === 0) {
|
|
630
|
+
return ".";
|
|
631
|
+
}
|
|
632
|
+
return pathEscapesRoot(relativePath) ? "<outside-project>" : relativePath;
|
|
633
|
+
}
|
|
388
634
|
function addOption(target, key, value) {
|
|
389
635
|
if (value !== undefined) {
|
|
390
636
|
target[key] = value;
|