evrex-mcp 0.1.1 → 0.3.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 +1 -1
- package/dist/index.js +199 -91
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Reload Cursor afterwards so it picks up the new server.
|
|
|
49
49
|
| Variable | Required | Purpose |
|
|
50
50
|
|---|---|---|
|
|
51
51
|
| `EVREX_TOKEN` | **yes** | Your evrex API token. The server exits at startup with instructions if it's missing. |
|
|
52
|
-
| `
|
|
52
|
+
| `MODEL_API_KEY` | no | Only used when `EVREX_SYNTHESIZE` is on. Anthropic, OpenAI or Google — the provider is recognised from the key prefix (`sk-ant-…`, `sk-…`, `AIza…`). `ANTHROPIC_API_KEY` is still accepted. |
|
|
53
53
|
| `EVREX_SYNTHESIZE` | no | Set to `1` to have the server summarize evidence into a prose answer before returning it. **Off by default** — see below. |
|
|
54
54
|
| `EVREX_API_BASE_URL` | no | Point at a self-hosted evrex backend. Defaults to the hosted one. |
|
|
55
55
|
| `ANTHROPIC_MODEL` | no | Defaults to `claude-opus-5`. |
|
package/dist/index.js
CHANGED
|
@@ -55,84 +55,10 @@ var evrexApi = {
|
|
|
55
55
|
|
|
56
56
|
// ../../packages/llm-core/src/client.ts
|
|
57
57
|
import Anthropic from "@anthropic-ai/sdk";
|
|
58
|
-
var DEFAULT_MODEL = "claude-opus-5";
|
|
59
58
|
var DEFAULT_MAX_TOKENS = 16e3;
|
|
60
|
-
function createAnthropicClient(apiKey) {
|
|
61
|
-
if (!apiKey || apiKey.trim().length === 0) return null;
|
|
62
|
-
return new Anthropic({ apiKey });
|
|
63
|
-
}
|
|
64
59
|
|
|
65
60
|
// ../../packages/llm-core/src/complete.ts
|
|
66
61
|
import Anthropic2 from "@anthropic-ai/sdk";
|
|
67
|
-
var NOOP_LOGGER = { warn: () => void 0, error: () => void 0 };
|
|
68
|
-
function parseableFormat(schema) {
|
|
69
|
-
return {
|
|
70
|
-
type: "json_schema",
|
|
71
|
-
schema,
|
|
72
|
-
parse: (content) => JSON.parse(content)
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function textOf(message) {
|
|
76
|
-
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
77
|
-
}
|
|
78
|
-
function describe(err) {
|
|
79
|
-
if (err instanceof Anthropic2.RateLimitError) {
|
|
80
|
-
return `rate limited (${err.status}): ${err.message}`;
|
|
81
|
-
}
|
|
82
|
-
if (err instanceof Anthropic2.APIConnectionError) {
|
|
83
|
-
return `could not reach the provider: ${err.message}`;
|
|
84
|
-
}
|
|
85
|
-
if (err instanceof Anthropic2.APIError) {
|
|
86
|
-
return `provider returned ${err.status ?? "an error"}: ${err.message}`;
|
|
87
|
-
}
|
|
88
|
-
return String(err);
|
|
89
|
-
}
|
|
90
|
-
async function completeJson(client2, options) {
|
|
91
|
-
if (!client2) return null;
|
|
92
|
-
const logger = options.logger ?? NOOP_LOGGER;
|
|
93
|
-
const model = options.model ?? DEFAULT_MODEL;
|
|
94
|
-
const max_tokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
95
|
-
const output_config = {};
|
|
96
|
-
if (options.schema) output_config.format = parseableFormat(options.schema);
|
|
97
|
-
if (options.effort) output_config.effort = options.effort;
|
|
98
|
-
const request = {
|
|
99
|
-
model,
|
|
100
|
-
max_tokens,
|
|
101
|
-
system: options.system,
|
|
102
|
-
messages: [{ role: "user", content: options.user }],
|
|
103
|
-
...Object.keys(output_config).length > 0 ? { output_config } : {}
|
|
104
|
-
};
|
|
105
|
-
let raw;
|
|
106
|
-
try {
|
|
107
|
-
if (options.schema) {
|
|
108
|
-
const message = await client2.messages.parse(request);
|
|
109
|
-
raw = message.parsed_output;
|
|
110
|
-
if (raw === null || raw === void 0) {
|
|
111
|
-
logger.warn(
|
|
112
|
-
"Model returned no parseable JSON for a schema-constrained call."
|
|
113
|
-
);
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
const message = await client2.messages.create(request);
|
|
118
|
-
const text = textOf(message).trim();
|
|
119
|
-
if (!text) return null;
|
|
120
|
-
raw = JSON.parse(text);
|
|
121
|
-
}
|
|
122
|
-
} catch (err) {
|
|
123
|
-
logger.error(`Model call failed: ${describe(err)}`);
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
127
|
-
logger.warn("Model returned a non-object JSON body; discarding it.");
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
if (options.validate && !options.validate(raw)) {
|
|
131
|
-
logger.warn("Model response violated the expected schema; discarding it.");
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
return raw;
|
|
135
|
-
}
|
|
136
62
|
|
|
137
63
|
// ../../packages/llm-core/src/extraction.ts
|
|
138
64
|
var MAX_ITEMS_PER_CATEGORY = 6;
|
|
@@ -188,16 +114,26 @@ function isValidSynthesis(result, evidenceCount) {
|
|
|
188
114
|
);
|
|
189
115
|
}
|
|
190
116
|
async function synthesizeAnswer(question, evidence, client2, options = {}) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
117
|
+
if (!client2) return null;
|
|
118
|
+
let body;
|
|
119
|
+
try {
|
|
120
|
+
body = await client2.completeJson({
|
|
121
|
+
system: SYNTHESIS_SYSTEM_PROMPT,
|
|
122
|
+
user: buildSynthesisPrompt(question, evidence),
|
|
123
|
+
schema: SYNTHESIS_SCHEMA,
|
|
124
|
+
model: options.model
|
|
125
|
+
});
|
|
126
|
+
} catch (err) {
|
|
127
|
+
options.logger?.error(
|
|
128
|
+
`synthesis call failed: ${err instanceof Error ? err.message : String(err)}`
|
|
129
|
+
);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (typeof body !== "object" || body === null || !isValidSynthesis(body, evidence.length)) {
|
|
133
|
+
options.logger?.warn("synthesis response cited evidence it was not given");
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const raw = body;
|
|
201
137
|
return {
|
|
202
138
|
sentences: raw.sentences ?? [],
|
|
203
139
|
sentenceEvidence: raw.sentenceEvidence ?? [],
|
|
@@ -205,8 +141,169 @@ async function synthesizeAnswer(question, evidence, client2, options = {}) {
|
|
|
205
141
|
};
|
|
206
142
|
}
|
|
207
143
|
|
|
144
|
+
// ../../packages/llm-core/src/providers.ts
|
|
145
|
+
var DEFAULT_MODELS = {
|
|
146
|
+
anthropic: "claude-opus-5",
|
|
147
|
+
openai: "gpt-4o",
|
|
148
|
+
google: "gemini-2.0-flash"
|
|
149
|
+
};
|
|
150
|
+
function detectProvider(key) {
|
|
151
|
+
const k = (key ?? "").trim();
|
|
152
|
+
if (!k) return null;
|
|
153
|
+
if (k.startsWith("sk-ant-")) return "anthropic";
|
|
154
|
+
if (k.startsWith("AIza")) return "google";
|
|
155
|
+
if (k.startsWith("sk-")) return "openai";
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ../../packages/llm-core/src/provider-clients.ts
|
|
160
|
+
import Anthropic3 from "@anthropic-ai/sdk";
|
|
161
|
+
var NOOP = { warn: () => void 0, error: () => void 0 };
|
|
162
|
+
function parseJsonBody(text) {
|
|
163
|
+
const trimmed = text.trim();
|
|
164
|
+
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/.exec(trimmed);
|
|
165
|
+
const body = fenced ? fenced[1] : trimmed;
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(body);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
var AnthropicClient = class {
|
|
173
|
+
constructor(key, model, logger) {
|
|
174
|
+
this.model = model;
|
|
175
|
+
this.logger = logger;
|
|
176
|
+
this.sdk = new Anthropic3({ apiKey: key });
|
|
177
|
+
}
|
|
178
|
+
provider = "anthropic";
|
|
179
|
+
sdk;
|
|
180
|
+
async completeJson(request) {
|
|
181
|
+
try {
|
|
182
|
+
const message = await this.sdk.messages.create({
|
|
183
|
+
model: request.model ?? this.model,
|
|
184
|
+
max_tokens: request.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
185
|
+
system: request.system,
|
|
186
|
+
messages: [{ role: "user", content: request.user }],
|
|
187
|
+
...request.schema ? {
|
|
188
|
+
output_config: {
|
|
189
|
+
format: { type: "json_schema", schema: request.schema }
|
|
190
|
+
}
|
|
191
|
+
} : {}
|
|
192
|
+
});
|
|
193
|
+
const text = message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
194
|
+
return parseJsonBody(text);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.logger.warn(`anthropic call failed: ${describeError(err)}`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var OpenAiClient = class {
|
|
202
|
+
constructor(key, model, logger) {
|
|
203
|
+
this.key = key;
|
|
204
|
+
this.model = model;
|
|
205
|
+
this.logger = logger;
|
|
206
|
+
}
|
|
207
|
+
provider = "openai";
|
|
208
|
+
async completeJson(request) {
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: {
|
|
213
|
+
"Content-Type": "application/json",
|
|
214
|
+
Authorization: `Bearer ${this.key}`
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
model: request.model ?? this.model,
|
|
218
|
+
messages: [
|
|
219
|
+
{ role: "system", content: request.system },
|
|
220
|
+
{ role: "user", content: request.user }
|
|
221
|
+
],
|
|
222
|
+
...request.schema ? {
|
|
223
|
+
response_format: {
|
|
224
|
+
type: "json_schema",
|
|
225
|
+
json_schema: {
|
|
226
|
+
name: "evrex_result",
|
|
227
|
+
strict: true,
|
|
228
|
+
schema: request.schema
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} : { response_format: { type: "json_object" } }
|
|
232
|
+
})
|
|
233
|
+
});
|
|
234
|
+
if (!res.ok) {
|
|
235
|
+
this.logger.warn(
|
|
236
|
+
`openai call failed: ${res.status} ${(await res.text()).slice(0, 200)}`
|
|
237
|
+
);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const body = await res.json();
|
|
241
|
+
const text = body.choices?.[0]?.message?.content;
|
|
242
|
+
return typeof text === "string" ? parseJsonBody(text) : null;
|
|
243
|
+
} catch (err) {
|
|
244
|
+
this.logger.warn(`openai call failed: ${describeError(err)}`);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var GoogleClient = class {
|
|
250
|
+
constructor(key, model, logger) {
|
|
251
|
+
this.key = key;
|
|
252
|
+
this.model = model;
|
|
253
|
+
this.logger = logger;
|
|
254
|
+
}
|
|
255
|
+
provider = "google";
|
|
256
|
+
async completeJson(request) {
|
|
257
|
+
const model = request.model ?? this.model;
|
|
258
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`;
|
|
259
|
+
const shape = request.schema ? `
|
|
260
|
+
|
|
261
|
+
Respond with JSON matching exactly this schema:
|
|
262
|
+
${JSON.stringify(request.schema)}` : "";
|
|
263
|
+
try {
|
|
264
|
+
const res = await fetch(url, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
"x-goog-api-key": this.key
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
systemInstruction: { parts: [{ text: request.system + shape }] },
|
|
272
|
+
contents: [{ role: "user", parts: [{ text: request.user }] }],
|
|
273
|
+
generationConfig: { responseMimeType: "application/json" }
|
|
274
|
+
})
|
|
275
|
+
});
|
|
276
|
+
if (!res.ok) {
|
|
277
|
+
this.logger.warn(
|
|
278
|
+
`google call failed: ${res.status} ${(await res.text()).slice(0, 200)}`
|
|
279
|
+
);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const body = await res.json();
|
|
283
|
+
const text = body.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("");
|
|
284
|
+
return text ? parseJsonBody(text) : null;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
this.logger.warn(`google call failed: ${describeError(err)}`);
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
function describeError(err) {
|
|
292
|
+
return err instanceof Error ? err.message : String(err);
|
|
293
|
+
}
|
|
294
|
+
function createModelClient(key, options = {}) {
|
|
295
|
+
const provider = detectProvider(key);
|
|
296
|
+
if (!provider || !key) return null;
|
|
297
|
+
const logger = options.logger ?? NOOP;
|
|
298
|
+
const model = options.model ?? DEFAULT_MODELS[provider];
|
|
299
|
+
const trimmed = key.trim();
|
|
300
|
+
if (provider === "anthropic") return new AnthropicClient(trimmed, model, logger);
|
|
301
|
+
if (provider === "openai") return new OpenAiClient(trimmed, model, logger);
|
|
302
|
+
return new GoogleClient(trimmed, model, logger);
|
|
303
|
+
}
|
|
304
|
+
|
|
208
305
|
// src/synthesize.ts
|
|
209
|
-
var ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY ?? void 0;
|
|
306
|
+
var ANTHROPIC_API_KEY = process.env.MODEL_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? void 0;
|
|
210
307
|
var ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL;
|
|
211
308
|
var stderrLogger = {
|
|
212
309
|
warn: (message) => process.stderr.write(`[evrex] ${message}
|
|
@@ -216,7 +313,7 @@ var stderrLogger = {
|
|
|
216
313
|
};
|
|
217
314
|
var client = createAnthropicKeyClient();
|
|
218
315
|
function createAnthropicKeyClient() {
|
|
219
|
-
return
|
|
316
|
+
return createModelClient(ANTHROPIC_API_KEY, { logger: stderrLogger });
|
|
220
317
|
}
|
|
221
318
|
function synthesisEnabled() {
|
|
222
319
|
const flag = process.env.EVREX_SYNTHESIZE;
|
|
@@ -336,6 +433,12 @@ function toRepoRelative(filePath, root) {
|
|
|
336
433
|
}
|
|
337
434
|
return filePath;
|
|
338
435
|
}
|
|
436
|
+
function sameFileRef(a, b) {
|
|
437
|
+
if (a === b) return true;
|
|
438
|
+
if (a.endsWith(`/${b}`)) return true;
|
|
439
|
+
if (b.endsWith(`/${a}`)) return true;
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
339
442
|
async function resolveRepos() {
|
|
340
443
|
const override = process.env.EVREX_REPO_PATH;
|
|
341
444
|
if (override) {
|
|
@@ -361,9 +464,14 @@ async function evrexWhy(filePath, question) {
|
|
|
361
464
|
const sessions = (await Promise.all(sessionIds.map((id) => evrexApi.session(id)))).filter(
|
|
362
465
|
(s) => s !== null
|
|
363
466
|
);
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
const
|
|
467
|
+
const target = isAbsolute(filePath) ? toRepoRelative(filePath, root) : filePath;
|
|
468
|
+
const aboutTarget = (item) => (item.files ?? []).some((f) => sameFileRef(toRepoRelative(f, root), target));
|
|
469
|
+
const byRelevance = (items) => [...items].sort(
|
|
470
|
+
(a, b) => Number(aboutTarget(b)) - Number(aboutTarget(a))
|
|
471
|
+
);
|
|
472
|
+
const rejected = byRelevance(sessions.flatMap((s) => s.rejected)).slice(0, MAX_ITEMS);
|
|
473
|
+
const constraints = byRelevance(sessions.flatMap((s) => s.constraints)).slice(0, MAX_ITEMS);
|
|
474
|
+
const decisions = byRelevance(sessions.flatMap((s) => s.decisions)).slice(0, MAX_ITEMS);
|
|
367
475
|
const parts = [];
|
|
368
476
|
const heuristic = sessions.some((s) => s.insightsSource === "heuristic");
|
|
369
477
|
if (rejected.length > 0) {
|
|
@@ -472,7 +580,7 @@ server.registerTool(
|
|
|
472
580
|
"evrex_why",
|
|
473
581
|
{
|
|
474
582
|
title: "Why does this file/change exist?",
|
|
475
|
-
description: "Prior reasoning for a file: decisions, hard constraints, and explicitly REJECTED approaches, with sources and confidence.
|
|
583
|
+
description: "Prior reasoning for a file: decisions, hard constraints, and explicitly REJECTED approaches, with sources and confidence. Call this BEFORE reasoning from scratch about an existing file \u2014 not only when the user says 'why'. Use it when you are about to change, refactor, simplify, or remove existing code; when a design choice looks odd, redundant, or wrong; when you are about to infer a constraint from what the code happens to do; or when you would otherwise say 'probably' or 'this was likely done because'. Cheaper than the alternative: one call returns the recorded answer instead of many tool calls reconstructing a guess from code \u2014 and reconstruction cannot recover a rejected approach at all, since rejected work leaves no trace in the repo. Returns a plain 'no recorded reasoning found' when it has nothing, so the cost of checking is one call. Not for 'what does this code do' \u2014 read the file for that.",
|
|
476
584
|
inputSchema: {
|
|
477
585
|
file_path: z.string().describe("Path to the file, relative to the repo root or absolute"),
|
|
478
586
|
question: z.string().optional().describe("Optional specific question; defaults to a general 'why' query")
|
|
@@ -487,7 +595,7 @@ server.registerTool(
|
|
|
487
595
|
"evrex_search",
|
|
488
596
|
{
|
|
489
597
|
title: "Search Evrex sessions and commits",
|
|
490
|
-
description: "Free-text search across this repo's indexed
|
|
598
|
+
description: "Free-text search across this repo's indexed agent session history and git commits \u2014 prior discussion and reasoning that lives in conversation, not in code, so grep, git log and git blame cannot find it. Use for open-ended questions ('has this been discussed', 'when/why did we start doing X', 'what do we know about Y'), and before concluding that something was never considered \u2014 absence in the code is not absence of a decision. If you have a specific file path, prefer evrex_why.",
|
|
491
599
|
inputSchema: {
|
|
492
600
|
query: z.string().describe("Free-text search query")
|
|
493
601
|
}
|
package/package.json
CHANGED