@jkudish/jev-mcp 0.8.0 → 0.10.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/CHANGELOG.md +16 -0
- package/README.md +32 -0
- package/dist/http.d.ts +5 -0
- package/dist/http.js +101 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +121 -108
- 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 +9 -6
- package/skills/jev/SKILL.md +63 -0
- package/skills/jev/reference/tools.md +111 -0
package/dist/index.js
CHANGED
|
@@ -15,16 +15,27 @@
|
|
|
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
|
+
const server = new McpServer({ name: "jev-mcp", version: packageVersion });
|
|
35
|
+
for (const args of toolRegistrations)
|
|
36
|
+
server.registerTool(...args);
|
|
37
|
+
return server;
|
|
38
|
+
}
|
|
28
39
|
import { askJev as askProvider } from "./provider.js";
|
|
29
40
|
async function askJev(state, questions, signal) {
|
|
30
41
|
const result = await askProvider(state, questions, MODEL, signal);
|
|
@@ -70,7 +81,7 @@ const candidatesSchema = z
|
|
|
70
81
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
82
|
// jev_verify
|
|
72
83
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
-
|
|
84
|
+
tools.registerTool("jev_verify", {
|
|
74
85
|
title: "Verify claims against evidence",
|
|
75
86
|
description: "Check each claim against provided evidence text with TypeSafe Jev. Returns per claim: " +
|
|
76
87
|
"verdict (verified | contradicted | unsupported), full probability distribution, confidence, " +
|
|
@@ -87,7 +98,7 @@ server.registerTool("jev_verify", {
|
|
|
87
98
|
.optional()
|
|
88
99
|
.describe("Verdicts at or above this confidence stand automatically; below it they are flagged 'review'. Default 0.8."),
|
|
89
100
|
}),
|
|
90
|
-
}, async ({ claims, evidence: rawEvidence, auto_accept },
|
|
101
|
+
}, async ({ claims, evidence: rawEvidence, auto_accept }, ctx) => {
|
|
91
102
|
const autoAccept = auto_accept ?? 0.8;
|
|
92
103
|
const evidenceItems = typeof rawEvidence === "string"
|
|
93
104
|
? [{ id: "evidence", text: rawEvidence }]
|
|
@@ -96,6 +107,14 @@ server.registerTool("jev_verify", {
|
|
|
96
107
|
: [rawEvidence];
|
|
97
108
|
const { items: evidence } = ensureUniqueIds(evidenceItems, "evidence");
|
|
98
109
|
const { items: claimItems } = ensureUniqueIds(claims.map((text) => ({ text })), "claim");
|
|
110
|
+
// Keep the internal no-source option distinct from caller-provided evidence
|
|
111
|
+
// ids. A real evidence item named "none" must remain selectable and must be
|
|
112
|
+
// returned as supporting_evidence rather than being mistaken for the sentinel.
|
|
113
|
+
const evidenceIds = new Set(evidence.map((item) => item.id));
|
|
114
|
+
let noEvidenceKey = "none";
|
|
115
|
+
for (let suffix = 1; evidenceIds.has(noEvidenceKey); suffix++) {
|
|
116
|
+
noEvidenceKey = `none_${suffix}`;
|
|
117
|
+
}
|
|
99
118
|
const questions = {};
|
|
100
119
|
for (const claim of claimItems) {
|
|
101
120
|
questions[`relation_${claim.id}`] = choice(`How does the evidence relate to claim \`${claim.id}\` (${claim.text})?`, {
|
|
@@ -105,7 +124,7 @@ server.registerTool("jev_verify", {
|
|
|
105
124
|
});
|
|
106
125
|
if (evidence.length > 1) {
|
|
107
126
|
const criteria = Object.fromEntries(evidence.map((e) => [e.id, null]));
|
|
108
|
-
criteria[
|
|
127
|
+
criteria[noEvidenceKey] = "No single evidence item contains the content the claim depends on";
|
|
109
128
|
questions[`source_${claim.id}`] = choice(`Which evidence item does claim \`${claim.id}\` (${claim.text}) rest on?`, criteria);
|
|
110
129
|
}
|
|
111
130
|
}
|
|
@@ -114,14 +133,15 @@ server.registerTool("jev_verify", {
|
|
|
114
133
|
claims: claimItems,
|
|
115
134
|
evidence,
|
|
116
135
|
};
|
|
117
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
136
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
118
137
|
const results = claimItems.map((claim) => {
|
|
119
138
|
const relation = answers[`relation_${claim.id}`];
|
|
120
139
|
const validated = validateChoiceAnswer(relation, Object.keys(RELATION_TO_VERDICT));
|
|
121
140
|
// source_* is optional auxiliary information, requested only for multiple
|
|
122
141
|
// evidence items. Its absence does not invalidate the relation verdict,
|
|
123
|
-
// but a present source must name a supplied evidence id
|
|
124
|
-
|
|
142
|
+
// but a present source must name a supplied evidence id or the internal
|
|
143
|
+
// no-evidence option.
|
|
144
|
+
const sourceKeys = [...evidence.map((e) => e.id), noEvidenceKey];
|
|
125
145
|
const source = validateChoiceAnswer(answers[`source_${claim.id}`], sourceKeys);
|
|
126
146
|
const valid = validated && Object.hasOwn(RELATION_TO_VERDICT, validated.choice) &&
|
|
127
147
|
(relation.confidence == null || validated.confidence !== null);
|
|
@@ -149,7 +169,7 @@ server.registerTool("jev_verify", {
|
|
|
149
169
|
probabilities: relation.probabilities ?? null,
|
|
150
170
|
confidence,
|
|
151
171
|
action: confidence === null ? "review" : verifyAction(confidence, autoAccept),
|
|
152
|
-
supporting_evidence: source && source.choice !==
|
|
172
|
+
supporting_evidence: source && source.choice !== noEvidenceKey ? source.choice : null,
|
|
153
173
|
};
|
|
154
174
|
});
|
|
155
175
|
return text({
|
|
@@ -170,7 +190,7 @@ server.registerTool("jev_verify", {
|
|
|
170
190
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
191
|
// jev_screen
|
|
172
192
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
173
|
-
|
|
193
|
+
tools.registerTool("jev_screen", {
|
|
174
194
|
title: "Screen content before it enters agent context",
|
|
175
195
|
description: "Judge fetched or external text with TypeSafe Jev before an agent reads it: probability it contains " +
|
|
176
196
|
"instructions aimed at an AI agent (prompt injection), whether it has substantive content, and (when a purpose " +
|
|
@@ -185,7 +205,7 @@ server.registerTool("jev_screen", {
|
|
|
185
205
|
block_at: z.number().min(0).max(1).optional().describe("Injection probability at or above which content is blocked. Default 0.75."),
|
|
186
206
|
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
207
|
}),
|
|
188
|
-
}, async ({ text: content, purpose, block_at, review_at },
|
|
208
|
+
}, async ({ text: content, purpose, block_at, review_at }, ctx) => {
|
|
189
209
|
const blockAt = block_at ?? 0.75;
|
|
190
210
|
const reviewAt = review_at ?? 0.25;
|
|
191
211
|
const questions = {
|
|
@@ -205,7 +225,7 @@ server.registerTool("jev_screen", {
|
|
|
205
225
|
});
|
|
206
226
|
}
|
|
207
227
|
const state = { content, purpose: purpose ?? null };
|
|
208
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
228
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
209
229
|
const injection = validateNoulAnswer(answers.injection);
|
|
210
230
|
const substance = validateNoulAnswer(answers.substance);
|
|
211
231
|
const relevance = purpose ? validateNoulAnswer(answers.relevance) : undefined;
|
|
@@ -237,7 +257,7 @@ server.registerTool("jev_screen", {
|
|
|
237
257
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
258
|
// jev_noul
|
|
239
259
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
-
|
|
260
|
+
tools.registerTool("jev_noul", {
|
|
241
261
|
title: "Calibrated probability for propositions",
|
|
242
262
|
description: "Return a calibrated probability for each stated proposition with TypeSafe Jev, in one batched request: " +
|
|
243
263
|
"high means likely, low means unlikely, middling means genuinely uncertain. Supplied context informs the " +
|
|
@@ -260,7 +280,7 @@ server.registerTool("jev_noul", {
|
|
|
260
280
|
.optional()
|
|
261
281
|
.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
282
|
}),
|
|
263
|
-
}, async ({ propositions, context: rawContext, auto_accept },
|
|
283
|
+
}, async ({ propositions, context: rawContext, auto_accept }, ctx) => {
|
|
264
284
|
const autoAccept = auto_accept ?? 0.85;
|
|
265
285
|
const contextItems = typeof rawContext === "string"
|
|
266
286
|
? [{ id: "context", text: rawContext }]
|
|
@@ -285,7 +305,7 @@ server.registerTool("jev_noul", {
|
|
|
285
305
|
});
|
|
286
306
|
}
|
|
287
307
|
const state = { propositions: items, context: contextItems.length ? contextItems : null };
|
|
288
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
308
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
289
309
|
const rows = items.map((p) => ({
|
|
290
310
|
id: p.id,
|
|
291
311
|
proposition: p.text,
|
|
@@ -325,7 +345,7 @@ server.registerTool("jev_noul", {
|
|
|
325
345
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
326
346
|
// jev_find
|
|
327
347
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
328
|
-
|
|
348
|
+
tools.registerTool("jev_find", {
|
|
329
349
|
title: "Semantic search over candidates",
|
|
330
350
|
description: "Rank candidates against a plain-language query with TypeSafe Jev — no embeddings needed. " +
|
|
331
351
|
"One Choice scores every candidate id by how well it answers the query, plus a Noul checks whether " +
|
|
@@ -337,7 +357,7 @@ server.registerTool("jev_find", {
|
|
|
337
357
|
candidates: candidatesSchema,
|
|
338
358
|
top_k: z.number().int().min(1).max(50).optional().describe("How many ranked candidates to return. Default 5."),
|
|
339
359
|
}),
|
|
340
|
-
}, async ({ query, candidates: rawCandidates, top_k },
|
|
360
|
+
}, async ({ query, candidates: rawCandidates, top_k }, ctx) => {
|
|
341
361
|
const topK = top_k ?? 5;
|
|
342
362
|
const { items: candidates } = ensureUniqueIds(rawCandidates.map((c) => ({ id: c.id ?? "", text: truncate(c.text, MAX_CANDIDATE_CHARS) })), "candidate");
|
|
343
363
|
const criteria = Object.fromEntries(candidates.map((c) => [c.id, null]));
|
|
@@ -349,7 +369,7 @@ server.registerTool("jev_find", {
|
|
|
349
369
|
}),
|
|
350
370
|
};
|
|
351
371
|
const state = { query, candidates };
|
|
352
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
372
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
353
373
|
const exists = validateNoulAnswer(answers.exists);
|
|
354
374
|
const best = validateChoiceAnswer(answers.best, candidates.map((c) => c.id));
|
|
355
375
|
if (exists === null || best === null) {
|
|
@@ -383,7 +403,7 @@ server.registerTool("jev_find", {
|
|
|
383
403
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
384
404
|
// jev_classify
|
|
385
405
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
386
|
-
|
|
406
|
+
tools.registerTool("jev_classify", {
|
|
387
407
|
title: "Classify items against a shared label set",
|
|
388
408
|
description: "Assign each item to one class from a shared catalog with TypeSafe Jev, in one batched request: " +
|
|
389
409
|
"the class catalog is sent once and every item becomes an independent Choice question. " +
|
|
@@ -405,36 +425,65 @@ server.registerTool("jev_classify", {
|
|
|
405
425
|
"what belongs, what does not, precedence over overlapping classes, and a short example."),
|
|
406
426
|
purpose: z.string().optional().describe("What this classification is for; shared across all items."),
|
|
407
427
|
context: z
|
|
408
|
-
.union([z.string(), z.record(z.any())])
|
|
428
|
+
.union([z.string(), z.record(z.string(), z.any())])
|
|
409
429
|
.optional()
|
|
410
430
|
.describe("Shared context available to every item's judgment: policies, catalogs, anything stable."),
|
|
411
431
|
auto_accept: z.number().min(0).max(1).optional().describe("Minimum top probability for auto. Default 0.85."),
|
|
412
432
|
minimum_margin: z.number().min(0).max(1).optional().describe("Minimum winner-to-runner-up gap for auto. Default 0.5."),
|
|
413
433
|
}),
|
|
414
|
-
}, async ({ items: rawItems, classes: rawClasses, purpose, context, auto_accept, minimum_margin },
|
|
434
|
+
}, async ({ items: rawItems, classes: rawClasses, purpose, context, auto_accept, minimum_margin }, ctx) => {
|
|
415
435
|
const autoAccept = auto_accept ?? 0.85;
|
|
416
436
|
const minMargin = minimum_margin ?? 0.5;
|
|
417
437
|
// Preserve caller IDs exactly; use opaque internal keys (i0/c0) for the
|
|
418
438
|
// wire so sanitization can never rename or collide externally, and
|
|
419
439
|
// reject duplicate supplied IDs rather than silently suffixing them.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
440
|
+
// Generated fallbacks (item0/class0 style) avoid every supplied or
|
|
441
|
+
// already-used ID, so an omitted id can never collide with an explicit
|
|
442
|
+
// one — the same two-phase pattern jev_rerank uses for candidate IDs.
|
|
443
|
+
const suppliedItemIds = new Set();
|
|
444
|
+
for (const it of rawItems) {
|
|
423
445
|
if (it.id != null) {
|
|
424
|
-
if (
|
|
446
|
+
if (suppliedItemIds.has(it.id))
|
|
425
447
|
throw new Error(`Duplicate item id: ${it.id}`);
|
|
426
|
-
|
|
448
|
+
suppliedItemIds.add(it.id);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const usedItemIds = new Set(suppliedItemIds);
|
|
452
|
+
const items = rawItems.map((it, i) => {
|
|
453
|
+
let external;
|
|
454
|
+
if (it.id != null) {
|
|
455
|
+
external = it.id;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
external = `item${i}`;
|
|
459
|
+
let suffix = 2;
|
|
460
|
+
while (usedItemIds.has(external))
|
|
461
|
+
external = `item${i}_${suffix++}`;
|
|
427
462
|
}
|
|
463
|
+
usedItemIds.add(external);
|
|
428
464
|
return { external, key: `i${i}`, text: truncate(it.text, MAX_ITEM_CHARS) };
|
|
429
465
|
});
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
const external = c.id ?? `class${i}`;
|
|
466
|
+
const suppliedClassIds = new Set();
|
|
467
|
+
for (const c of rawClasses) {
|
|
433
468
|
if (c.id != null) {
|
|
434
|
-
if (
|
|
469
|
+
if (suppliedClassIds.has(c.id))
|
|
435
470
|
throw new Error(`Duplicate class id: ${c.id}`);
|
|
436
|
-
|
|
471
|
+
suppliedClassIds.add(c.id);
|
|
437
472
|
}
|
|
473
|
+
}
|
|
474
|
+
const usedClassIds = new Set(suppliedClassIds);
|
|
475
|
+
const classes = rawClasses.map((c, i) => {
|
|
476
|
+
let external;
|
|
477
|
+
if (c.id != null) {
|
|
478
|
+
external = c.id;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
external = `class${i}`;
|
|
482
|
+
let suffix = 2;
|
|
483
|
+
while (usedClassIds.has(external))
|
|
484
|
+
external = `class${i}_${suffix++}`;
|
|
485
|
+
}
|
|
486
|
+
usedClassIds.add(external);
|
|
438
487
|
return { external, key: `c${i}`, description: truncate(c.description, MAX_ITEM_CHARS) };
|
|
439
488
|
});
|
|
440
489
|
if (items.length * classes.length > 8_000) {
|
|
@@ -455,7 +504,7 @@ server.registerTool("jev_classify", {
|
|
|
455
504
|
for (const item of items) {
|
|
456
505
|
questions[item.key] = choice({ task: "Which class does this item belong to?", item: { id: item.key, text: item.text } }, criteria);
|
|
457
506
|
}
|
|
458
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
507
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
459
508
|
const keyToExternal = new Map(classes.map((c) => [c.key, c.external]));
|
|
460
509
|
const results = items.map((item) => {
|
|
461
510
|
const answer = validateChoiceAnswer(answers[item.key], classes.map((c) => c.key));
|
|
@@ -484,7 +533,10 @@ server.registerTool("jev_classify", {
|
|
|
484
533
|
decision: classificationDecision(topProbability, margin, autoAccept, minMargin),
|
|
485
534
|
};
|
|
486
535
|
});
|
|
487
|
-
|
|
536
|
+
// Null prototype: a caller-supplied class id such as "__proto__" or
|
|
537
|
+
// "constructor" must be tallied as its own key, not swallowed by (or
|
|
538
|
+
// read from) Object.prototype.
|
|
539
|
+
const byClass = Object.create(null);
|
|
488
540
|
for (const r of results) {
|
|
489
541
|
if (r.classification !== null)
|
|
490
542
|
byClass[r.classification] = (byClass[r.classification] ?? 0) + 1;
|
|
@@ -508,7 +560,7 @@ server.registerTool("jev_classify", {
|
|
|
508
560
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
509
561
|
// jev_decide
|
|
510
562
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
511
|
-
|
|
563
|
+
tools.registerTool("jev_decide", {
|
|
512
564
|
title: "Decide between bounded alternatives",
|
|
513
565
|
description: "One unresolved, bounded decision where semantic judgment over supplied evidence could change your plan: " +
|
|
514
566
|
"implementation alternatives, product tradeoffs with known preferences, workflow selection. " +
|
|
@@ -537,7 +589,7 @@ server.registerTool("jev_decide", {
|
|
|
537
589
|
.optional()
|
|
538
590
|
.describe("Include ask_user / investigate / none as Choosable options so the model can decline to rank. Default true."),
|
|
539
591
|
}),
|
|
540
|
-
}, async ({ decision, evidence, priorities, candidates, requirements: reqs, escape_hatches },
|
|
592
|
+
}, async ({ decision, evidence, priorities, candidates, requirements: reqs, escape_hatches }, ctx) => {
|
|
541
593
|
const includeHatches = escape_hatches ?? true;
|
|
542
594
|
const requirements = reqs ?? [];
|
|
543
595
|
// Reject duplicate candidate IDs and collisions with active escape hatches
|
|
@@ -576,7 +628,7 @@ server.registerTool("jev_decide", {
|
|
|
576
628
|
candidates: candidateKeys.map((c) => ({ id: c.key, description: c.description })),
|
|
577
629
|
requirements,
|
|
578
630
|
};
|
|
579
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
631
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
580
632
|
const keyToId = new Map(candidateKeys.map((c) => [c.key, c.id]));
|
|
581
633
|
const expectedRecKeys = new Set([...candidateKeys.map((c) => c.key), ...(includeHatches ? Object.keys(DECIDE_ESCAPE_HATCHES) : [])]);
|
|
582
634
|
const expectedCheckKeys = new Set(["supported", "contradicted", "unknown"]);
|
|
@@ -617,7 +669,7 @@ server.registerTool("jev_decide", {
|
|
|
617
669
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
618
670
|
// jev_rerank
|
|
619
671
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
620
|
-
|
|
672
|
+
tools.registerTool("jev_rerank", {
|
|
621
673
|
title: "Score every candidate's relevance and return them sorted",
|
|
622
674
|
description: "Rerank candidates against a query with TypeSafe Jev: one independent relevance probability per candidate, " +
|
|
623
675
|
"all in a single request, then sorted by score. Unlike jev_find (which picks one best answer), rerank scores " +
|
|
@@ -629,7 +681,7 @@ server.registerTool("jev_rerank", {
|
|
|
629
681
|
candidates: candidatesSchema,
|
|
630
682
|
top_k: z.number().int().min(1).max(250).optional().describe("How many ranked candidates to return. Default: all."),
|
|
631
683
|
}),
|
|
632
|
-
}, async ({ query, candidates: rawCandidates, top_k },
|
|
684
|
+
}, async ({ query, candidates: rawCandidates, top_k }, ctx) => {
|
|
633
685
|
const topK = top_k ?? null;
|
|
634
686
|
// Caller IDs are preserved verbatim; opaque wire keys (classify pattern).
|
|
635
687
|
// Duplicate supplied IDs are rejected rather than silently renamed, and
|
|
@@ -672,7 +724,7 @@ server.registerTool("jev_rerank", {
|
|
|
672
724
|
false: "The candidate is about a different subject, or only shares vocabulary with the query",
|
|
673
725
|
});
|
|
674
726
|
});
|
|
675
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
727
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
676
728
|
// One invalid Noul makes the whole ordering untrustworthy; never sort a
|
|
677
729
|
// missing answer as a confident zero.
|
|
678
730
|
const scores = candidates.map((_, i) => {
|
|
@@ -713,7 +765,7 @@ server.registerTool("jev_rerank", {
|
|
|
713
765
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
714
766
|
// jev_compare
|
|
715
767
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
716
|
-
|
|
768
|
+
tools.registerTool("jev_compare", {
|
|
717
769
|
title: "Compare two passages for factual agreement",
|
|
718
770
|
description: "Judge the relation between two passages with TypeSafe Jev: same_fact, contradicts, or different_facts, " +
|
|
719
771
|
"with the full probability distribution, confidence, and an auto-versus-review decision. " +
|
|
@@ -732,7 +784,7 @@ server.registerTool("jev_compare", {
|
|
|
732
784
|
auto_accept: z.number().min(0).max(1).optional().describe("Minimum top probability for auto. Default 0.85."),
|
|
733
785
|
minimum_margin: z.number().min(0).max(1).optional().describe("Minimum winner-to-runner-up gap for auto. Default 0.5."),
|
|
734
786
|
}),
|
|
735
|
-
}, async ({ passage_a, passage_b, aspects: rawAspects, purpose, auto_accept, minimum_margin },
|
|
787
|
+
}, async ({ passage_a, passage_b, aspects: rawAspects, purpose, auto_accept, minimum_margin }, ctx) => {
|
|
736
788
|
const autoAccept = auto_accept ?? 0.85;
|
|
737
789
|
const minMargin = minimum_margin ?? 0.5;
|
|
738
790
|
const aspects = rawAspects ?? [];
|
|
@@ -748,7 +800,7 @@ server.registerTool("jev_compare", {
|
|
|
748
800
|
questions[`aspect_${i}`] = choice(`Judging only the aspect "${aspect}" of the two passages in the state, which relation holds?`, { ...ASPECT_RELATIONS });
|
|
749
801
|
});
|
|
750
802
|
const state = { purpose: purpose ?? null, passage_a: a, passage_b: b, aspects };
|
|
751
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
803
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
752
804
|
const expected = new Set(Object.keys(COMPARE_RELATIONS));
|
|
753
805
|
// classify-grade validation via the shared validateChoiceAnswer; the
|
|
754
806
|
// overall and aspect questions share the same three relation keys.
|
|
@@ -785,60 +837,9 @@ server.registerTool("jev_compare", {
|
|
|
785
837
|
usage,
|
|
786
838
|
});
|
|
787
839
|
});
|
|
788
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
789
|
-
// jev_extract
|
|
790
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
791
|
-
import { Worker } from "node:worker_threads";
|
|
792
840
|
// Caller-supplied regex runs in a throwaway worker with a hard deadline, so a
|
|
793
841
|
// 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", {
|
|
842
|
+
tools.registerTool("jev_extract", {
|
|
842
843
|
title: "Extract fields by regex, Jev picks the right match",
|
|
843
844
|
description: "Extract structured fields from a document with TypeSafe Jev as the picker, not the generator: your regex " +
|
|
844
845
|
"finds candidate substrings in code, Jev chooses which candidate is the field's true value, and the result is " +
|
|
@@ -861,7 +862,7 @@ server.registerTool("jev_extract", {
|
|
|
861
862
|
auto_accept: z.number().min(0).max(1).optional().describe("Minimum top probability for auto. Default 0.85."),
|
|
862
863
|
minimum_margin: z.number().min(0).max(1).optional().describe("Minimum winner-to-runner-up gap for auto. Default 0.5."),
|
|
863
864
|
}),
|
|
864
|
-
}, async ({ document, fields: rawFields, purpose, auto_accept, minimum_margin },
|
|
865
|
+
}, async ({ document, fields: rawFields, purpose, auto_accept, minimum_margin }, ctx) => {
|
|
865
866
|
const autoAccept = auto_accept ?? 0.85;
|
|
866
867
|
const minMargin = minimum_margin ?? 0.5;
|
|
867
868
|
const doc = truncate(document, 50000);
|
|
@@ -878,9 +879,14 @@ server.registerTool("jev_extract", {
|
|
|
878
879
|
// value returned.
|
|
879
880
|
const fields = [];
|
|
880
881
|
for (let i = 0; i < rawFields.length; i++) {
|
|
882
|
+
// A cancelled request's regex work serves nobody; stop scheduling it.
|
|
883
|
+
if (ctx.mcpReq.signal.aborted)
|
|
884
|
+
break;
|
|
881
885
|
const f = rawFields[i];
|
|
882
|
-
|
|
883
|
-
|
|
886
|
+
// Strip every g (and non-letters) before appending exactly one, so
|
|
887
|
+
// multi-letter flags like "gi" never become "gig" and throw.
|
|
888
|
+
const flags = (f.flags ?? "").replace(/[^a-z]/g, "").replace(/g/g, "") + "g";
|
|
889
|
+
const result = await runRegex(doc, f.pattern, flags, ctx.mcpReq.signal);
|
|
884
890
|
fields.push({
|
|
885
891
|
...f,
|
|
886
892
|
key: `f${i}`,
|
|
@@ -908,7 +914,7 @@ server.registerTool("jev_extract", {
|
|
|
908
914
|
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);
|
|
909
915
|
stateFields.push({ id: f.key, description: f.description, pattern: f.pattern });
|
|
910
916
|
}
|
|
911
|
-
const { answers, usage, provider, model } = stateFields.length > 0 ? await askJev({ purpose: purpose ?? null, document: doc, fields: stateFields }, questions,
|
|
917
|
+
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 };
|
|
912
918
|
const results = fields.map((f) => {
|
|
913
919
|
const flags = { candidates_truncated: f.truncated, matches_skipped_too_long: f.tooLong };
|
|
914
920
|
// An incomplete candidate universe (capped or overlong-skipped matches)
|
|
@@ -1187,7 +1193,7 @@ function projectReviewHalf(answers, thresholds, truncated) {
|
|
|
1187
1193
|
reasonCodes.push("accepted");
|
|
1188
1194
|
return { ...base, action, composite, reason_codes: reasonCodes, limiting_rubrics: limitingRubrics };
|
|
1189
1195
|
}
|
|
1190
|
-
|
|
1196
|
+
tools.registerTool("jev_review", {
|
|
1191
1197
|
title: "Review a proposed patch",
|
|
1192
1198
|
description: "Score a proposed diff against the request with TypeSafe Jev before the task is called done. " +
|
|
1193
1199
|
"Returns 0..2 rubric scores for correctness, spec match, test gap, and blast radius (the last two lower the " +
|
|
@@ -1221,7 +1227,7 @@ server.registerTool("jev_review", {
|
|
|
1221
1227
|
.optional()
|
|
1222
1228
|
.describe("Weighted composite at or above this is required for auto. Default 0.7."),
|
|
1223
1229
|
}),
|
|
1224
|
-
}, async ({ request, diff, tests, auto_accept, review_at, composite_floor },
|
|
1230
|
+
}, async ({ request, diff, tests, auto_accept, review_at, composite_floor }, ctx) => {
|
|
1225
1231
|
const { autoAccept, reviewAt } = resolvePolicyThresholds(auto_accept ?? 0.8, review_at);
|
|
1226
1232
|
const compositeFloor = composite_floor ?? DEFAULT_COMPOSITE_FLOOR;
|
|
1227
1233
|
const truncated = request.length > MAX_REVIEW_DOC_CHARS ||
|
|
@@ -1233,7 +1239,7 @@ server.registerTool("jev_review", {
|
|
|
1233
1239
|
diff: truncate(diff, MAX_REVIEW_DOC_CHARS),
|
|
1234
1240
|
tests: tests ? truncate(tests, MAX_REVIEW_DOC_CHARS) : null,
|
|
1235
1241
|
};
|
|
1236
|
-
const { answers, usage, provider, model } = await askJev(state, reviewQuestions(),
|
|
1242
|
+
const { answers, usage, provider, model } = await askJev(state, reviewQuestions(), ctx.mcpReq.signal);
|
|
1237
1243
|
return text({
|
|
1238
1244
|
tool: "jev_review",
|
|
1239
1245
|
model,
|
|
@@ -1243,7 +1249,7 @@ server.registerTool("jev_review", {
|
|
|
1243
1249
|
usage,
|
|
1244
1250
|
});
|
|
1245
1251
|
});
|
|
1246
|
-
|
|
1252
|
+
tools.registerTool("jev_gate", {
|
|
1247
1253
|
title: "Gate completion: review a patch and verify claims",
|
|
1248
1254
|
description: "Review a proposed patch and verify completion claims against supplied evidence in one TypeSafe Jev call. " +
|
|
1249
1255
|
"Auto only when the patch review is accepted and every claim is verified at or above auto_accept. " +
|
|
@@ -1286,7 +1292,7 @@ server.registerTool("jev_gate", {
|
|
|
1286
1292
|
.optional()
|
|
1287
1293
|
.describe("Weighted composite at or above this is required for auto. Default 0.7."),
|
|
1288
1294
|
}),
|
|
1289
|
-
}, async ({ request, diff, claims, evidence: rawEvidence, tests, auto_accept, review_at, composite_floor },
|
|
1295
|
+
}, async ({ request, diff, claims, evidence: rawEvidence, tests, auto_accept, review_at, composite_floor }, ctx) => {
|
|
1290
1296
|
const { autoAccept, reviewAt } = resolvePolicyThresholds(auto_accept ?? 0.8, review_at);
|
|
1291
1297
|
const compositeFloor = composite_floor ?? DEFAULT_COMPOSITE_FLOOR;
|
|
1292
1298
|
const evidence = normalizeEvidence(rawEvidence);
|
|
@@ -1332,7 +1338,7 @@ server.registerTool("jev_gate", {
|
|
|
1332
1338
|
"diff and tests belong to the separate patch review. If a claim needs a diff or test log as support, it " +
|
|
1333
1339
|
"must be supplied in evidence." + ANTI_INJECTION, VERIFY_CLAIM_CRITERIA);
|
|
1334
1340
|
});
|
|
1335
|
-
const { answers, usage, provider, model } = await askJev(state, questions,
|
|
1341
|
+
const { answers, usage, provider, model } = await askJev(state, questions, ctx.mcpReq.signal);
|
|
1336
1342
|
const review = projectReviewHalf(answers, { autoAccept, reviewAt, compositeFloor }, truncated);
|
|
1337
1343
|
// classify-grade validation via the shared validateChoiceAnswer: exact
|
|
1338
1344
|
// keys, finite [0,1] probabilities summing to one within the shared
|
|
@@ -1409,6 +1415,13 @@ server.registerTool("jev_gate", {
|
|
|
1409
1415
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1410
1416
|
// Boot
|
|
1411
1417
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1412
|
-
|
|
1413
|
-
|
|
1418
|
+
if (process.argv.includes("--http") || process.env.JEV_MCP_TRANSPORT === "http") {
|
|
1419
|
+
const { serveHttp } = await import("./http.js");
|
|
1420
|
+
const { url } = await serveHttp(createServer);
|
|
1421
|
+
console.error(`[jev-mcp] ready — model ${MODEL}, stateless HTTP at ${url}`);
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
serveStdio(createServer);
|
|
1425
|
+
console.error(`[jev-mcp] ready — model ${MODEL}`);
|
|
1426
|
+
}
|
|
1414
1427
|
//# sourceMappingURL=index.js.map
|