@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/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/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
+ // 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
- server.registerTool("jev_verify", {
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 }, extra) => {
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["none"] = "No single evidence item contains the content the claim depends on";
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, extra.signal);
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 (or "none").
124
- const sourceKeys = [...evidence.map((e) => e.id), "none"];
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 !== "none" ? source.choice : null,
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
- server.registerTool("jev_screen", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_noul", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_find", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_classify", {
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 }, extra) => {
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
- const seenItemIds = new Set();
421
- const items = rawItems.map((it, i) => {
422
- const external = it.id ?? `item${i}`;
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 (seenItemIds.has(it.id))
449
+ if (suppliedItemIds.has(it.id))
425
450
  throw new Error(`Duplicate item id: ${it.id}`);
426
- seenItemIds.add(it.id);
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 seenClassIds = new Set();
431
- const classes = rawClasses.map((c, i) => {
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 (seenClassIds.has(c.id))
472
+ if (suppliedClassIds.has(c.id))
435
473
  throw new Error(`Duplicate class id: ${c.id}`);
436
- seenClassIds.add(c.id);
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, extra.signal);
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
- const byClass = {};
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
- server.registerTool("jev_decide", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_rerank", {
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 }, extra) => {
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, extra.signal);
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
- server.registerTool("jev_compare", {
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 }, extra) => {
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, extra.signal);
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
- 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", {
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 }, extra) => {
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, extra.signal) : { answers: {}, usage: null, provider: "none", model: MODEL };
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
- server.registerTool("jev_review", {
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 }, extra) => {
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(), extra.signal);
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
- server.registerTool("jev_gate", {
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 }, extra) => {
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, extra.signal);
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
- await server.connect(new StdioServerTransport());
1415
- console.error(`[jev-mcp] ready — model ${MODEL}`);
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