@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/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/sdk/server/mcp.js";
19
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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, MAX_EXTRACT_CANDIDATES, MAX_EXTRACT_CANDIDATE_CHARS, MAX_EXTRACT_FIELDS, MAX_EXTRACT_TOTAL_CHARS, MAX_RERANK_CANDIDATES, MAX_RERANK_TOTAL_CHARS, REGEX_TIMEOUT_MS, rerankByScore, RELATION_TO_VERDICT, screenRecommendation, truncate, verifyAction, VERIFY_CLAIM_CRITERIA, worstAction, } from "./lib.js";
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 server = new McpServer({ name: "jev-mcp", version: packageVersion });
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
- server.registerTool("jev_verify", {
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 }, extra) => {
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["none"] = "No single evidence item contains the content the claim depends on";
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, extra.signal);
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 (or "none").
124
- const sourceKeys = [...evidence.map((e) => e.id), "none"];
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 !== "none" ? source.choice : null,
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
- server.registerTool("jev_screen", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_noul", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_find", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_classify", {
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 }, extra) => {
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
- const seenItemIds = new Set();
421
- const items = rawItems.map((it, i) => {
422
- const external = it.id ?? `item${i}`;
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 (seenItemIds.has(it.id))
446
+ if (suppliedItemIds.has(it.id))
425
447
  throw new Error(`Duplicate item id: ${it.id}`);
426
- seenItemIds.add(it.id);
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 seenClassIds = new Set();
431
- const classes = rawClasses.map((c, i) => {
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 (seenClassIds.has(c.id))
469
+ if (suppliedClassIds.has(c.id))
435
470
  throw new Error(`Duplicate class id: ${c.id}`);
436
- seenClassIds.add(c.id);
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, extra.signal);
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
- const byClass = {};
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
- server.registerTool("jev_decide", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_rerank", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_compare", {
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 }, extra) => {
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, extra.signal);
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
- const REGEX_WORKER_SOURCE = `
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 }, extra) => {
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
- const flags = ((f.flags ?? "").replace(/[^a-z]/g, "") + "g").replace(/g+/g, "g");
883
- const result = await runRegex(doc, f.pattern, flags);
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, extra.signal) : { answers: {}, usage: null, provider: "none", model: MODEL };
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
- server.registerTool("jev_review", {
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 }, extra) => {
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(), extra.signal);
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
- server.registerTool("jev_gate", {
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 }, extra) => {
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, extra.signal);
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
- await server.connect(new StdioServerTransport());
1413
- console.error(`[jev-mcp] ready — model ${MODEL}`);
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