@jkudish/jev-mcp 0.9.0 → 0.10.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/CHANGELOG.md +14 -0
- package/README.md +17 -1
- package/dist/http.d.ts +5 -0
- package/dist/http.js +113 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +120 -106
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +13 -1
- package/dist/lib.js +58 -0
- package/dist/lib.js.map +1 -1
- package/dist/provider.js +22 -15
- package/dist/provider.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -15,16 +15,30 @@
|
|
|
15
15
|
// jev_extract — regex candidates, Jev picks the right verbatim value
|
|
16
16
|
// jev_review — score a proposed diff before the task is called done
|
|
17
17
|
// jev_gate — review a patch and verify completion claims in one call
|
|
18
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
19
|
-
import {
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
19
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
20
20
|
import { choice, noul, score } from "@typesafe-ai/sdk";
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { createRequire } from "node:module";
|
|
23
|
-
import { classificationDecision, claimAction, contradictsRecommendation, DECIDE_ESCAPE_HATCHES, DEFAULT_COMPOSITE_FLOOR, ensureUniqueIds, existsVerdict, hasNonEmptyEvidence, isRecord, marginOf, MAX_CANDIDATES_DECIDE, MAX_CLASSES, MAX_ITEM_CHARS, MAX_ITEMS, MAX_REQUIREMENTS, MAX_CANDIDATES, MAX_CANDIDATE_CHARS, PROBABILITY_SUM_TOLERANCE, SCORE_MEAN_TOLERANCE, MAX_GATE_CLAIMS, MAX_GATE_EVIDENCE_CHARS, MAX_GATE_EVIDENCE_ITEMS, MAX_PROPOSITIONS, MAX_PROPOSITION_CHARS, MAX_NOUL_TOTAL_CHARS, MAX_CLAIM_CHARS, MAX_REVIEW_DOC_CHARS, normalizeEvidence, rankCandidates, requireCompleteContext, resolvePolicyThresholds, REVIEW_WEIGHTS, reviewAction, reviewComposite, COMPARE_RELATIONS, ASPECT_RELATIONS, MAX_COMPARE_ASPECTS,
|
|
23
|
+
import { classificationDecision, claimAction, contradictsRecommendation, DECIDE_ESCAPE_HATCHES, DEFAULT_COMPOSITE_FLOOR, ensureUniqueIds, existsVerdict, hasNonEmptyEvidence, isRecord, marginOf, MAX_CANDIDATES_DECIDE, MAX_CLASSES, MAX_ITEM_CHARS, MAX_ITEMS, MAX_REQUIREMENTS, MAX_CANDIDATES, MAX_CANDIDATE_CHARS, PROBABILITY_SUM_TOLERANCE, SCORE_MEAN_TOLERANCE, MAX_GATE_CLAIMS, MAX_GATE_EVIDENCE_CHARS, MAX_GATE_EVIDENCE_ITEMS, MAX_PROPOSITIONS, MAX_PROPOSITION_CHARS, MAX_NOUL_TOTAL_CHARS, MAX_CLAIM_CHARS, MAX_REVIEW_DOC_CHARS, normalizeEvidence, rankCandidates, requireCompleteContext, resolvePolicyThresholds, REVIEW_WEIGHTS, reviewAction, reviewComposite, COMPARE_RELATIONS, ASPECT_RELATIONS, MAX_COMPARE_ASPECTS, MAX_EXTRACT_FIELDS, MAX_EXTRACT_TOTAL_CHARS, MAX_RERANK_CANDIDATES, MAX_RERANK_TOTAL_CHARS, rerankByScore, runRegex, RELATION_TO_VERDICT, screenRecommendation, truncate, verifyAction, VERIFY_CLAIM_CRITERIA, worstAction, } from "./lib.js";
|
|
24
24
|
const MODEL = process.env.JEV_MCP_MODEL ?? "jev-latest";
|
|
25
25
|
// Resolved at runtime so the MCP handshake version always matches the package.
|
|
26
26
|
const { version: packageVersion } = createRequire(import.meta.url)("../package.json");
|
|
27
|
-
const
|
|
27
|
+
const toolRegistrations = [];
|
|
28
|
+
const tools = {
|
|
29
|
+
registerTool: ((...args) => {
|
|
30
|
+
toolRegistrations.push(args);
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
export function createServer() {
|
|
34
|
+
// The tool list is static (no change notifications are ever emitted), so it
|
|
35
|
+
// advertises no listChanged capability and no client has a reason to hold a
|
|
36
|
+
// subscriptions/listen stream open, which would occupy an HTTP slot idle.
|
|
37
|
+
const server = new McpServer({ name: "jev-mcp", version: packageVersion }, { capabilities: { tools: { listChanged: false } } });
|
|
38
|
+
for (const args of toolRegistrations)
|
|
39
|
+
server.registerTool(...args);
|
|
40
|
+
return server;
|
|
41
|
+
}
|
|
28
42
|
import { askJev as askProvider } from "./provider.js";
|
|
29
43
|
async function askJev(state, questions, signal) {
|
|
30
44
|
const result = await askProvider(state, questions, MODEL, signal);
|
|
@@ -70,7 +84,7 @@ const candidatesSchema = z
|
|
|
70
84
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
85
|
// jev_verify
|
|
72
86
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
-
|
|
87
|
+
tools.registerTool("jev_verify", {
|
|
74
88
|
title: "Verify claims against evidence",
|
|
75
89
|
description: "Check each claim against provided evidence text with TypeSafe Jev. Returns per claim: " +
|
|
76
90
|
"verdict (verified | contradicted | unsupported), full probability distribution, confidence, " +
|
|
@@ -87,7 +101,7 @@ server.registerTool("jev_verify", {
|
|
|
87
101
|
.optional()
|
|
88
102
|
.describe("Verdicts at or above this confidence stand automatically; below it they are flagged 'review'. Default 0.8."),
|
|
89
103
|
}),
|
|
90
|
-
}, async ({ claims, evidence: rawEvidence, auto_accept },
|
|
104
|
+
}, async ({ claims, evidence: rawEvidence, auto_accept }, ctx) => {
|
|
91
105
|
const autoAccept = auto_accept ?? 0.8;
|
|
92
106
|
const evidenceItems = typeof rawEvidence === "string"
|
|
93
107
|
? [{ id: "evidence", text: rawEvidence }]
|
|
@@ -96,6 +110,14 @@ server.registerTool("jev_verify", {
|
|
|
96
110
|
: [rawEvidence];
|
|
97
111
|
const { items: evidence } = ensureUniqueIds(evidenceItems, "evidence");
|
|
98
112
|
const { items: claimItems } = ensureUniqueIds(claims.map((text) => ({ text })), "claim");
|
|
113
|
+
// Keep the internal no-source option distinct from caller-provided evidence
|
|
114
|
+
// ids. A real evidence item named "none" must remain selectable and must be
|
|
115
|
+
// returned as supporting_evidence rather than being mistaken for the sentinel.
|
|
116
|
+
const evidenceIds = new Set(evidence.map((item) => item.id));
|
|
117
|
+
let noEvidenceKey = "none";
|
|
118
|
+
for (let suffix = 1; evidenceIds.has(noEvidenceKey); suffix++) {
|
|
119
|
+
noEvidenceKey = `none_${suffix}`;
|
|
120
|
+
}
|
|
99
121
|
const questions = {};
|
|
100
122
|
for (const claim of claimItems) {
|
|
101
123
|
questions[`relation_${claim.id}`] = choice(`How does the evidence relate to claim \`${claim.id}\` (${claim.text})?`, {
|
|
@@ -105,7 +127,7 @@ server.registerTool("jev_verify", {
|
|
|
105
127
|
});
|
|
106
128
|
if (evidence.length > 1) {
|
|
107
129
|
const criteria = Object.fromEntries(evidence.map((e) => [e.id, null]));
|
|
108
|
-
criteria[
|
|
130
|
+
criteria[noEvidenceKey] = "No single evidence item contains the content the claim depends on";
|
|
109
131
|
questions[`source_${claim.id}`] = choice(`Which evidence item does claim \`${claim.id}\` (${claim.text}) rest on?`, criteria);
|
|
110
132
|
}
|
|
111
133
|
}
|
|
@@ -114,14 +136,15 @@ server.registerTool("jev_verify", {
|
|
|
114
136
|
claims: claimItems,
|
|
115
137
|
evidence,
|
|
116
138
|
};
|
|
117
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
139
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
118
140
|
const results = claimItems.map((claim) => {
|
|
119
141
|
const relation = answers[`relation_${claim.id}`];
|
|
120
142
|
const validated = validateChoiceAnswer(relation, Object.keys(RELATION_TO_VERDICT));
|
|
121
143
|
// source_* is optional auxiliary information, requested only for multiple
|
|
122
144
|
// evidence items. Its absence does not invalidate the relation verdict,
|
|
123
|
-
// but a present source must name a supplied evidence id
|
|
124
|
-
|
|
145
|
+
// but a present source must name a supplied evidence id or the internal
|
|
146
|
+
// no-evidence option.
|
|
147
|
+
const sourceKeys = [...evidence.map((e) => e.id), noEvidenceKey];
|
|
125
148
|
const source = validateChoiceAnswer(answers[`source_${claim.id}`], sourceKeys);
|
|
126
149
|
const valid = validated && Object.hasOwn(RELATION_TO_VERDICT, validated.choice) &&
|
|
127
150
|
(relation.confidence == null || validated.confidence !== null);
|
|
@@ -149,7 +172,7 @@ server.registerTool("jev_verify", {
|
|
|
149
172
|
probabilities: relation.probabilities ?? null,
|
|
150
173
|
confidence,
|
|
151
174
|
action: confidence === null ? "review" : verifyAction(confidence, autoAccept),
|
|
152
|
-
supporting_evidence: source && source.choice !==
|
|
175
|
+
supporting_evidence: source && source.choice !== noEvidenceKey ? source.choice : null,
|
|
153
176
|
};
|
|
154
177
|
});
|
|
155
178
|
return text({
|
|
@@ -170,7 +193,7 @@ server.registerTool("jev_verify", {
|
|
|
170
193
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
194
|
// jev_screen
|
|
172
195
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
173
|
-
|
|
196
|
+
tools.registerTool("jev_screen", {
|
|
174
197
|
title: "Screen content before it enters agent context",
|
|
175
198
|
description: "Judge fetched or external text with TypeSafe Jev before an agent reads it: probability it contains " +
|
|
176
199
|
"instructions aimed at an AI agent (prompt injection), whether it has substantive content, and (when a purpose " +
|
|
@@ -185,7 +208,7 @@ server.registerTool("jev_screen", {
|
|
|
185
208
|
block_at: z.number().min(0).max(1).optional().describe("Injection probability at or above which content is blocked. Default 0.75."),
|
|
186
209
|
review_at: z.number().min(0).max(1).optional().describe("Injection probability at or above which content is flagged for review. Default 0.25."),
|
|
187
210
|
}),
|
|
188
|
-
}, async ({ text: content, purpose, block_at, review_at },
|
|
211
|
+
}, async ({ text: content, purpose, block_at, review_at }, ctx) => {
|
|
189
212
|
const blockAt = block_at ?? 0.75;
|
|
190
213
|
const reviewAt = review_at ?? 0.25;
|
|
191
214
|
const questions = {
|
|
@@ -205,7 +228,7 @@ server.registerTool("jev_screen", {
|
|
|
205
228
|
});
|
|
206
229
|
}
|
|
207
230
|
const state = { content, purpose: purpose ?? null };
|
|
208
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
231
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
209
232
|
const injection = validateNoulAnswer(answers.injection);
|
|
210
233
|
const substance = validateNoulAnswer(answers.substance);
|
|
211
234
|
const relevance = purpose ? validateNoulAnswer(answers.relevance) : undefined;
|
|
@@ -237,7 +260,7 @@ server.registerTool("jev_screen", {
|
|
|
237
260
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
261
|
// jev_noul
|
|
239
262
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
-
|
|
263
|
+
tools.registerTool("jev_noul", {
|
|
241
264
|
title: "Calibrated probability for propositions",
|
|
242
265
|
description: "Return a calibrated probability for each stated proposition with TypeSafe Jev, in one batched request: " +
|
|
243
266
|
"high means likely, low means unlikely, middling means genuinely uncertain. Supplied context informs the " +
|
|
@@ -260,7 +283,7 @@ server.registerTool("jev_noul", {
|
|
|
260
283
|
.optional()
|
|
261
284
|
.describe("Decisiveness threshold: probability at or above this marks the proposition likely, at or below (1 - this) unlikely, between them uncertain. Must exceed 0.5. Default 0.85."),
|
|
262
285
|
}),
|
|
263
|
-
}, async ({ propositions, context: rawContext, auto_accept },
|
|
286
|
+
}, async ({ propositions, context: rawContext, auto_accept }, ctx) => {
|
|
264
287
|
const autoAccept = auto_accept ?? 0.85;
|
|
265
288
|
const contextItems = typeof rawContext === "string"
|
|
266
289
|
? [{ id: "context", text: rawContext }]
|
|
@@ -285,7 +308,7 @@ server.registerTool("jev_noul", {
|
|
|
285
308
|
});
|
|
286
309
|
}
|
|
287
310
|
const state = { propositions: items, context: contextItems.length ? contextItems : null };
|
|
288
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
311
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
289
312
|
const rows = items.map((p) => ({
|
|
290
313
|
id: p.id,
|
|
291
314
|
proposition: p.text,
|
|
@@ -325,7 +348,7 @@ server.registerTool("jev_noul", {
|
|
|
325
348
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
326
349
|
// jev_find
|
|
327
350
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
328
|
-
|
|
351
|
+
tools.registerTool("jev_find", {
|
|
329
352
|
title: "Semantic search over candidates",
|
|
330
353
|
description: "Rank candidates against a plain-language query with TypeSafe Jev — no embeddings needed. " +
|
|
331
354
|
"One Choice scores every candidate id by how well it answers the query, plus a Noul checks whether " +
|
|
@@ -337,7 +360,7 @@ server.registerTool("jev_find", {
|
|
|
337
360
|
candidates: candidatesSchema,
|
|
338
361
|
top_k: z.number().int().min(1).max(50).optional().describe("How many ranked candidates to return. Default 5."),
|
|
339
362
|
}),
|
|
340
|
-
}, async ({ query, candidates: rawCandidates, top_k },
|
|
363
|
+
}, async ({ query, candidates: rawCandidates, top_k }, ctx) => {
|
|
341
364
|
const topK = top_k ?? 5;
|
|
342
365
|
const { items: candidates } = ensureUniqueIds(rawCandidates.map((c) => ({ id: c.id ?? "", text: truncate(c.text, MAX_CANDIDATE_CHARS) })), "candidate");
|
|
343
366
|
const criteria = Object.fromEntries(candidates.map((c) => [c.id, null]));
|
|
@@ -349,7 +372,7 @@ server.registerTool("jev_find", {
|
|
|
349
372
|
}),
|
|
350
373
|
};
|
|
351
374
|
const state = { query, candidates };
|
|
352
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
375
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
353
376
|
const exists = validateNoulAnswer(answers.exists);
|
|
354
377
|
const best = validateChoiceAnswer(answers.best, candidates.map((c) => c.id));
|
|
355
378
|
if (exists === null || best === null) {
|
|
@@ -383,7 +406,7 @@ server.registerTool("jev_find", {
|
|
|
383
406
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
384
407
|
// jev_classify
|
|
385
408
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
386
|
-
|
|
409
|
+
tools.registerTool("jev_classify", {
|
|
387
410
|
title: "Classify items against a shared label set",
|
|
388
411
|
description: "Assign each item to one class from a shared catalog with TypeSafe Jev, in one batched request: " +
|
|
389
412
|
"the class catalog is sent once and every item becomes an independent Choice question. " +
|
|
@@ -411,30 +434,59 @@ server.registerTool("jev_classify", {
|
|
|
411
434
|
auto_accept: z.number().min(0).max(1).optional().describe("Minimum top probability for auto. Default 0.85."),
|
|
412
435
|
minimum_margin: z.number().min(0).max(1).optional().describe("Minimum winner-to-runner-up gap for auto. Default 0.5."),
|
|
413
436
|
}),
|
|
414
|
-
}, async ({ items: rawItems, classes: rawClasses, purpose, context, auto_accept, minimum_margin },
|
|
437
|
+
}, async ({ items: rawItems, classes: rawClasses, purpose, context, auto_accept, minimum_margin }, ctx) => {
|
|
415
438
|
const autoAccept = auto_accept ?? 0.85;
|
|
416
439
|
const minMargin = minimum_margin ?? 0.5;
|
|
417
440
|
// Preserve caller IDs exactly; use opaque internal keys (i0/c0) for the
|
|
418
441
|
// wire so sanitization can never rename or collide externally, and
|
|
419
442
|
// reject duplicate supplied IDs rather than silently suffixing them.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
443
|
+
// Generated fallbacks (item0/class0 style) avoid every supplied or
|
|
444
|
+
// already-used ID, so an omitted id can never collide with an explicit
|
|
445
|
+
// one — the same two-phase pattern jev_rerank uses for candidate IDs.
|
|
446
|
+
const suppliedItemIds = new Set();
|
|
447
|
+
for (const it of rawItems) {
|
|
423
448
|
if (it.id != null) {
|
|
424
|
-
if (
|
|
449
|
+
if (suppliedItemIds.has(it.id))
|
|
425
450
|
throw new Error(`Duplicate item id: ${it.id}`);
|
|
426
|
-
|
|
451
|
+
suppliedItemIds.add(it.id);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const usedItemIds = new Set(suppliedItemIds);
|
|
455
|
+
const items = rawItems.map((it, i) => {
|
|
456
|
+
let external;
|
|
457
|
+
if (it.id != null) {
|
|
458
|
+
external = it.id;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
external = `item${i}`;
|
|
462
|
+
let suffix = 2;
|
|
463
|
+
while (usedItemIds.has(external))
|
|
464
|
+
external = `item${i}_${suffix++}`;
|
|
427
465
|
}
|
|
466
|
+
usedItemIds.add(external);
|
|
428
467
|
return { external, key: `i${i}`, text: truncate(it.text, MAX_ITEM_CHARS) };
|
|
429
468
|
});
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
const external = c.id ?? `class${i}`;
|
|
469
|
+
const suppliedClassIds = new Set();
|
|
470
|
+
for (const c of rawClasses) {
|
|
433
471
|
if (c.id != null) {
|
|
434
|
-
if (
|
|
472
|
+
if (suppliedClassIds.has(c.id))
|
|
435
473
|
throw new Error(`Duplicate class id: ${c.id}`);
|
|
436
|
-
|
|
474
|
+
suppliedClassIds.add(c.id);
|
|
437
475
|
}
|
|
476
|
+
}
|
|
477
|
+
const usedClassIds = new Set(suppliedClassIds);
|
|
478
|
+
const classes = rawClasses.map((c, i) => {
|
|
479
|
+
let external;
|
|
480
|
+
if (c.id != null) {
|
|
481
|
+
external = c.id;
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
external = `class${i}`;
|
|
485
|
+
let suffix = 2;
|
|
486
|
+
while (usedClassIds.has(external))
|
|
487
|
+
external = `class${i}_${suffix++}`;
|
|
488
|
+
}
|
|
489
|
+
usedClassIds.add(external);
|
|
438
490
|
return { external, key: `c${i}`, description: truncate(c.description, MAX_ITEM_CHARS) };
|
|
439
491
|
});
|
|
440
492
|
if (items.length * classes.length > 8_000) {
|
|
@@ -455,7 +507,7 @@ server.registerTool("jev_classify", {
|
|
|
455
507
|
for (const item of items) {
|
|
456
508
|
questions[item.key] = choice({ task: "Which class does this item belong to?", item: { id: item.key, text: item.text } }, criteria);
|
|
457
509
|
}
|
|
458
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
510
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
459
511
|
const keyToExternal = new Map(classes.map((c) => [c.key, c.external]));
|
|
460
512
|
const results = items.map((item) => {
|
|
461
513
|
const answer = validateChoiceAnswer(answers[item.key], classes.map((c) => c.key));
|
|
@@ -484,7 +536,10 @@ server.registerTool("jev_classify", {
|
|
|
484
536
|
decision: classificationDecision(topProbability, margin, autoAccept, minMargin),
|
|
485
537
|
};
|
|
486
538
|
});
|
|
487
|
-
|
|
539
|
+
// Null prototype: a caller-supplied class id such as "__proto__" or
|
|
540
|
+
// "constructor" must be tallied as its own key, not swallowed by (or
|
|
541
|
+
// read from) Object.prototype.
|
|
542
|
+
const byClass = Object.create(null);
|
|
488
543
|
for (const r of results) {
|
|
489
544
|
if (r.classification !== null)
|
|
490
545
|
byClass[r.classification] = (byClass[r.classification] ?? 0) + 1;
|
|
@@ -508,7 +563,7 @@ server.registerTool("jev_classify", {
|
|
|
508
563
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
509
564
|
// jev_decide
|
|
510
565
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
511
|
-
|
|
566
|
+
tools.registerTool("jev_decide", {
|
|
512
567
|
title: "Decide between bounded alternatives",
|
|
513
568
|
description: "One unresolved, bounded decision where semantic judgment over supplied evidence could change your plan: " +
|
|
514
569
|
"implementation alternatives, product tradeoffs with known preferences, workflow selection. " +
|
|
@@ -537,7 +592,7 @@ server.registerTool("jev_decide", {
|
|
|
537
592
|
.optional()
|
|
538
593
|
.describe("Include ask_user / investigate / none as Choosable options so the model can decline to rank. Default true."),
|
|
539
594
|
}),
|
|
540
|
-
}, async ({ decision, evidence, priorities, candidates, requirements: reqs, escape_hatches },
|
|
595
|
+
}, async ({ decision, evidence, priorities, candidates, requirements: reqs, escape_hatches }, ctx) => {
|
|
541
596
|
const includeHatches = escape_hatches ?? true;
|
|
542
597
|
const requirements = reqs ?? [];
|
|
543
598
|
// Reject duplicate candidate IDs and collisions with active escape hatches
|
|
@@ -576,7 +631,7 @@ server.registerTool("jev_decide", {
|
|
|
576
631
|
candidates: candidateKeys.map((c) => ({ id: c.key, description: c.description })),
|
|
577
632
|
requirements,
|
|
578
633
|
};
|
|
579
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
634
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
580
635
|
const keyToId = new Map(candidateKeys.map((c) => [c.key, c.id]));
|
|
581
636
|
const expectedRecKeys = new Set([...candidateKeys.map((c) => c.key), ...(includeHatches ? Object.keys(DECIDE_ESCAPE_HATCHES) : [])]);
|
|
582
637
|
const expectedCheckKeys = new Set(["supported", "contradicted", "unknown"]);
|
|
@@ -617,7 +672,7 @@ server.registerTool("jev_decide", {
|
|
|
617
672
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
618
673
|
// jev_rerank
|
|
619
674
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
620
|
-
|
|
675
|
+
tools.registerTool("jev_rerank", {
|
|
621
676
|
title: "Score every candidate's relevance and return them sorted",
|
|
622
677
|
description: "Rerank candidates against a query with TypeSafe Jev: one independent relevance probability per candidate, " +
|
|
623
678
|
"all in a single request, then sorted by score. Unlike jev_find (which picks one best answer), rerank scores " +
|
|
@@ -629,7 +684,7 @@ server.registerTool("jev_rerank", {
|
|
|
629
684
|
candidates: candidatesSchema,
|
|
630
685
|
top_k: z.number().int().min(1).max(250).optional().describe("How many ranked candidates to return. Default: all."),
|
|
631
686
|
}),
|
|
632
|
-
}, async ({ query, candidates: rawCandidates, top_k },
|
|
687
|
+
}, async ({ query, candidates: rawCandidates, top_k }, ctx) => {
|
|
633
688
|
const topK = top_k ?? null;
|
|
634
689
|
// Caller IDs are preserved verbatim; opaque wire keys (classify pattern).
|
|
635
690
|
// Duplicate supplied IDs are rejected rather than silently renamed, and
|
|
@@ -672,7 +727,7 @@ server.registerTool("jev_rerank", {
|
|
|
672
727
|
false: "The candidate is about a different subject, or only shares vocabulary with the query",
|
|
673
728
|
});
|
|
674
729
|
});
|
|
675
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
730
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
676
731
|
// One invalid Noul makes the whole ordering untrustworthy; never sort a
|
|
677
732
|
// missing answer as a confident zero.
|
|
678
733
|
const scores = candidates.map((_, i) => {
|
|
@@ -713,7 +768,7 @@ server.registerTool("jev_rerank", {
|
|
|
713
768
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
714
769
|
// jev_compare
|
|
715
770
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
716
|
-
|
|
771
|
+
tools.registerTool("jev_compare", {
|
|
717
772
|
title: "Compare two passages for factual agreement",
|
|
718
773
|
description: "Judge the relation between two passages with TypeSafe Jev: same_fact, contradicts, or different_facts, " +
|
|
719
774
|
"with the full probability distribution, confidence, and an auto-versus-review decision. " +
|
|
@@ -732,7 +787,7 @@ server.registerTool("jev_compare", {
|
|
|
732
787
|
auto_accept: z.number().min(0).max(1).optional().describe("Minimum top probability for auto. Default 0.85."),
|
|
733
788
|
minimum_margin: z.number().min(0).max(1).optional().describe("Minimum winner-to-runner-up gap for auto. Default 0.5."),
|
|
734
789
|
}),
|
|
735
|
-
}, async ({ passage_a, passage_b, aspects: rawAspects, purpose, auto_accept, minimum_margin },
|
|
790
|
+
}, async ({ passage_a, passage_b, aspects: rawAspects, purpose, auto_accept, minimum_margin }, ctx) => {
|
|
736
791
|
const autoAccept = auto_accept ?? 0.85;
|
|
737
792
|
const minMargin = minimum_margin ?? 0.5;
|
|
738
793
|
const aspects = rawAspects ?? [];
|
|
@@ -748,7 +803,7 @@ server.registerTool("jev_compare", {
|
|
|
748
803
|
questions[`aspect_${i}`] = choice(`Judging only the aspect "${aspect}" of the two passages in the state, which relation holds?`, { ...ASPECT_RELATIONS });
|
|
749
804
|
});
|
|
750
805
|
const state = { purpose: purpose ?? null, passage_a: a, passage_b: b, aspects };
|
|
751
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
806
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
752
807
|
const expected = new Set(Object.keys(COMPARE_RELATIONS));
|
|
753
808
|
// classify-grade validation via the shared validateChoiceAnswer; the
|
|
754
809
|
// overall and aspect questions share the same three relation keys.
|
|
@@ -785,60 +840,9 @@ server.registerTool("jev_compare", {
|
|
|
785
840
|
usage,
|
|
786
841
|
});
|
|
787
842
|
});
|
|
788
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
789
|
-
// jev_extract
|
|
790
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
791
|
-
import { Worker } from "node:worker_threads";
|
|
792
843
|
// Caller-supplied regex runs in a throwaway worker with a hard deadline, so a
|
|
793
844
|
// catastrophic backtracking pattern can never hang the MCP server itself.
|
|
794
|
-
|
|
795
|
-
import { parentPort, workerData } from "node:worker_threads";
|
|
796
|
-
const { document, pattern, flags, maxCandidates, maxCandidateChars } = workerData;
|
|
797
|
-
try {
|
|
798
|
-
const re = new RegExp(pattern, flags);
|
|
799
|
-
const seen = new Set();
|
|
800
|
-
const candidates = [];
|
|
801
|
-
let truncated = false;
|
|
802
|
-
let tooLong = 0;
|
|
803
|
-
for (const match of document.matchAll(re)) {
|
|
804
|
-
const value = match[0];
|
|
805
|
-
if (value.length === 0 || seen.has(value)) continue;
|
|
806
|
-
seen.add(value);
|
|
807
|
-
if (value.length > maxCandidateChars) { tooLong += 1; continue; }
|
|
808
|
-
if (candidates.length >= maxCandidates) { truncated = true; break; }
|
|
809
|
-
candidates.push(value);
|
|
810
|
-
}
|
|
811
|
-
parentPort.postMessage({ candidates, truncated, tooLong });
|
|
812
|
-
} catch (error) {
|
|
813
|
-
parentPort.postMessage({ candidates: [], truncated: false, tooLong: 0, error: String(error && error.message ? error.message : error) });
|
|
814
|
-
}
|
|
815
|
-
`;
|
|
816
|
-
function runRegex(document, pattern, flags) {
|
|
817
|
-
return new Promise((resolve) => {
|
|
818
|
-
let settled = false;
|
|
819
|
-
const worker = new Worker(REGEX_WORKER_SOURCE, {
|
|
820
|
-
eval: true,
|
|
821
|
-
workerData: { document, pattern, flags, maxCandidates: MAX_EXTRACT_CANDIDATES, maxCandidateChars: MAX_EXTRACT_CANDIDATE_CHARS },
|
|
822
|
-
});
|
|
823
|
-
const finish = (value) => {
|
|
824
|
-
if (settled)
|
|
825
|
-
return;
|
|
826
|
-
settled = true;
|
|
827
|
-
clearTimeout(timer);
|
|
828
|
-
void worker.terminate();
|
|
829
|
-
resolve(value);
|
|
830
|
-
};
|
|
831
|
-
const timer = setTimeout(() => finish({
|
|
832
|
-
candidates: [],
|
|
833
|
-
truncated: false,
|
|
834
|
-
tooLong: 0,
|
|
835
|
-
error: `regex timed out after ${REGEX_TIMEOUT_MS}ms; simplify the pattern`,
|
|
836
|
-
}), REGEX_TIMEOUT_MS);
|
|
837
|
-
worker.on("message", (message) => finish(message));
|
|
838
|
-
worker.on("error", (error) => finish({ candidates: [], truncated: false, tooLong: 0, error: error.message }));
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
server.registerTool("jev_extract", {
|
|
845
|
+
tools.registerTool("jev_extract", {
|
|
842
846
|
title: "Extract fields by regex, Jev picks the right match",
|
|
843
847
|
description: "Extract structured fields from a document with TypeSafe Jev as the picker, not the generator: your regex " +
|
|
844
848
|
"finds candidate substrings in code, Jev chooses which candidate is the field's true value, and the result is " +
|
|
@@ -861,7 +865,7 @@ server.registerTool("jev_extract", {
|
|
|
861
865
|
auto_accept: z.number().min(0).max(1).optional().describe("Minimum top probability for auto. Default 0.85."),
|
|
862
866
|
minimum_margin: z.number().min(0).max(1).optional().describe("Minimum winner-to-runner-up gap for auto. Default 0.5."),
|
|
863
867
|
}),
|
|
864
|
-
}, async ({ document, fields: rawFields, purpose, auto_accept, minimum_margin },
|
|
868
|
+
}, async ({ document, fields: rawFields, purpose, auto_accept, minimum_margin }, ctx) => {
|
|
865
869
|
const autoAccept = auto_accept ?? 0.85;
|
|
866
870
|
const minMargin = minimum_margin ?? 0.5;
|
|
867
871
|
const doc = truncate(document, 50000);
|
|
@@ -878,11 +882,14 @@ server.registerTool("jev_extract", {
|
|
|
878
882
|
// value returned.
|
|
879
883
|
const fields = [];
|
|
880
884
|
for (let i = 0; i < rawFields.length; i++) {
|
|
885
|
+
// A cancelled request's regex work serves nobody; stop scheduling it.
|
|
886
|
+
if (ctx.mcpReq.signal.aborted)
|
|
887
|
+
break;
|
|
881
888
|
const f = rawFields[i];
|
|
882
889
|
// Strip every g (and non-letters) before appending exactly one, so
|
|
883
890
|
// multi-letter flags like "gi" never become "gig" and throw.
|
|
884
891
|
const flags = (f.flags ?? "").replace(/[^a-z]/g, "").replace(/g/g, "") + "g";
|
|
885
|
-
const result = await runRegex(doc, f.pattern, flags);
|
|
892
|
+
const result = await runRegex(doc, f.pattern, flags, ctx.mcpReq.signal);
|
|
886
893
|
fields.push({
|
|
887
894
|
...f,
|
|
888
895
|
key: `f${i}`,
|
|
@@ -910,7 +917,7 @@ server.registerTool("jev_extract", {
|
|
|
910
917
|
questions[f.key] = choice(`Which candidate is the correct value of the field "${f.id}" (${f.description}) in the document in the state? Pick the exact substring the document presents as this field's value.`, criteria);
|
|
911
918
|
stateFields.push({ id: f.key, description: f.description, pattern: f.pattern });
|
|
912
919
|
}
|
|
913
|
-
const { answers, usage, provider, model } = stateFields.length > 0 ? await askJev({ purpose: purpose ?? null, document: doc, fields: stateFields }, questions,
|
|
920
|
+
const { answers, usage, provider, model } = stateFields.length > 0 ? await askJev({ purpose: purpose ?? null, document: doc, fields: stateFields }, questions, ctx.mcpReq.signal) : { answers: {}, usage: null, provider: "none", model: MODEL };
|
|
914
921
|
const results = fields.map((f) => {
|
|
915
922
|
const flags = { candidates_truncated: f.truncated, matches_skipped_too_long: f.tooLong };
|
|
916
923
|
// An incomplete candidate universe (capped or overlong-skipped matches)
|
|
@@ -1189,7 +1196,7 @@ function projectReviewHalf(answers, thresholds, truncated) {
|
|
|
1189
1196
|
reasonCodes.push("accepted");
|
|
1190
1197
|
return { ...base, action, composite, reason_codes: reasonCodes, limiting_rubrics: limitingRubrics };
|
|
1191
1198
|
}
|
|
1192
|
-
|
|
1199
|
+
tools.registerTool("jev_review", {
|
|
1193
1200
|
title: "Review a proposed patch",
|
|
1194
1201
|
description: "Score a proposed diff against the request with TypeSafe Jev before the task is called done. " +
|
|
1195
1202
|
"Returns 0..2 rubric scores for correctness, spec match, test gap, and blast radius (the last two lower the " +
|
|
@@ -1223,7 +1230,7 @@ server.registerTool("jev_review", {
|
|
|
1223
1230
|
.optional()
|
|
1224
1231
|
.describe("Weighted composite at or above this is required for auto. Default 0.7."),
|
|
1225
1232
|
}),
|
|
1226
|
-
}, async ({ request, diff, tests, auto_accept, review_at, composite_floor },
|
|
1233
|
+
}, async ({ request, diff, tests, auto_accept, review_at, composite_floor }, ctx) => {
|
|
1227
1234
|
const { autoAccept, reviewAt } = resolvePolicyThresholds(auto_accept ?? 0.8, review_at);
|
|
1228
1235
|
const compositeFloor = composite_floor ?? DEFAULT_COMPOSITE_FLOOR;
|
|
1229
1236
|
const truncated = request.length > MAX_REVIEW_DOC_CHARS ||
|
|
@@ -1235,7 +1242,7 @@ server.registerTool("jev_review", {
|
|
|
1235
1242
|
diff: truncate(diff, MAX_REVIEW_DOC_CHARS),
|
|
1236
1243
|
tests: tests ? truncate(tests, MAX_REVIEW_DOC_CHARS) : null,
|
|
1237
1244
|
};
|
|
1238
|
-
const { answers, usage, provider, model } = await askJev(state, reviewQuestions(),
|
|
1245
|
+
const { answers, usage, provider, model } = await askJev(state, reviewQuestions(), ctx.mcpReq.signal);
|
|
1239
1246
|
return text({
|
|
1240
1247
|
tool: "jev_review",
|
|
1241
1248
|
model,
|
|
@@ -1245,7 +1252,7 @@ server.registerTool("jev_review", {
|
|
|
1245
1252
|
usage,
|
|
1246
1253
|
});
|
|
1247
1254
|
});
|
|
1248
|
-
|
|
1255
|
+
tools.registerTool("jev_gate", {
|
|
1249
1256
|
title: "Gate completion: review a patch and verify claims",
|
|
1250
1257
|
description: "Review a proposed patch and verify completion claims against supplied evidence in one TypeSafe Jev call. " +
|
|
1251
1258
|
"Auto only when the patch review is accepted and every claim is verified at or above auto_accept. " +
|
|
@@ -1288,7 +1295,7 @@ server.registerTool("jev_gate", {
|
|
|
1288
1295
|
.optional()
|
|
1289
1296
|
.describe("Weighted composite at or above this is required for auto. Default 0.7."),
|
|
1290
1297
|
}),
|
|
1291
|
-
}, async ({ request, diff, claims, evidence: rawEvidence, tests, auto_accept, review_at, composite_floor },
|
|
1298
|
+
}, async ({ request, diff, claims, evidence: rawEvidence, tests, auto_accept, review_at, composite_floor }, ctx) => {
|
|
1292
1299
|
const { autoAccept, reviewAt } = resolvePolicyThresholds(auto_accept ?? 0.8, review_at);
|
|
1293
1300
|
const compositeFloor = composite_floor ?? DEFAULT_COMPOSITE_FLOOR;
|
|
1294
1301
|
const evidence = normalizeEvidence(rawEvidence);
|
|
@@ -1334,7 +1341,7 @@ server.registerTool("jev_gate", {
|
|
|
1334
1341
|
"diff and tests belong to the separate patch review. If a claim needs a diff or test log as support, it " +
|
|
1335
1342
|
"must be supplied in evidence." + ANTI_INJECTION, VERIFY_CLAIM_CRITERIA);
|
|
1336
1343
|
});
|
|
1337
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
1344
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
1338
1345
|
const review = projectReviewHalf(answers, { autoAccept, reviewAt, compositeFloor }, truncated);
|
|
1339
1346
|
// classify-grade validation via the shared validateChoiceAnswer: exact
|
|
1340
1347
|
// keys, finite [0,1] probabilities summing to one within the shared
|
|
@@ -1411,6 +1418,13 @@ server.registerTool("jev_gate", {
|
|
|
1411
1418
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1412
1419
|
// Boot
|
|
1413
1420
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1414
|
-
|
|
1415
|
-
|
|
1421
|
+
if (process.argv.includes("--http") || process.env.JEV_MCP_TRANSPORT === "http") {
|
|
1422
|
+
const { serveHttp } = await import("./http.js");
|
|
1423
|
+
const { url } = await serveHttp(createServer);
|
|
1424
|
+
console.error(`[jev-mcp] ready — model ${MODEL}, stateless HTTP at ${url}`);
|
|
1425
|
+
}
|
|
1426
|
+
else {
|
|
1427
|
+
serveStdio(createServer);
|
|
1428
|
+
console.error(`[jev-mcp] ready — model ${MODEL}`);
|
|
1429
|
+
}
|
|
1416
1430
|
//# sourceMappingURL=index.js.map
|