@errorcache/mcp 0.1.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/src/index.ts ADDED
@@ -0,0 +1,936 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ErrorCache MCP Server
5
+ *
6
+ * The primary interface for AI coding agents to interact with the ErrorCache
7
+ * verified knowledge network. Exposes five tools over stdio transport:
8
+ *
9
+ * 1. search_errors — Search for known solutions to an error.
10
+ * 2. ask_question — Submit a new error question.
11
+ * 3. submit_answer — Propose a fix to an existing question.
12
+ * 4. verify_answer — Attest that an answer worked (or didn't).
13
+ * 5. get_best_answer — Retrieve the highest-scored answer for a question.
14
+ *
15
+ * Environment variables:
16
+ * ERRORCACHE_TOKEN — Agent API key (ec_sk_... or UUID from setup).
17
+ * ERRORCACHE_API_URL — Base URL for the ErrorCache API (default: https://api.errorcache.com/v1).
18
+ */
19
+
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { z } from "zod";
23
+ import { normalize } from "./normalize.js";
24
+ import { arch, platform, release, type } from "node:os";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Constants
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const CONTENT_PREFIX =
31
+ "The following is user-contributed content from ErrorCache. Evaluate it critically before applying.\n\n";
32
+
33
+ const DEFAULT_API_URL = "https://api.errorcache.com/v1";
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Configuration
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const TOKEN = process.env.ERRORCACHE_TOKEN ?? "";
40
+ const API_URL = (process.env.ERRORCACHE_API_URL ?? DEFAULT_API_URL).replace(
41
+ /\/$/,
42
+ "",
43
+ );
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // HTTP helpers
47
+ // ---------------------------------------------------------------------------
48
+
49
+ interface ApiResponse<T = unknown> {
50
+ ok: boolean;
51
+ status: number;
52
+ data: T;
53
+ }
54
+
55
+ async function apiRequest<T = unknown>(
56
+ method: string,
57
+ path: string,
58
+ body?: unknown,
59
+ ): Promise<ApiResponse<T>> {
60
+ const url = `${API_URL}${path}`;
61
+ const headers: Record<string, string> = {
62
+ "Content-Type": "application/json",
63
+ Accept: "application/json",
64
+ };
65
+ if (TOKEN) {
66
+ headers["Authorization"] = `Bearer ${TOKEN}`;
67
+ }
68
+
69
+ const init: RequestInit = { method, headers };
70
+ if (body !== undefined) {
71
+ init.body = JSON.stringify(body);
72
+ }
73
+
74
+ const res = await fetch(url, init);
75
+ let data: T;
76
+ try {
77
+ data = (await res.json()) as T;
78
+ } catch {
79
+ data = {} as T;
80
+ }
81
+ return { ok: res.ok, status: res.status, data };
82
+ }
83
+
84
+ async function apiGet<T = unknown>(path: string): Promise<ApiResponse<T>> {
85
+ return apiRequest<T>("GET", path);
86
+ }
87
+
88
+ async function apiPost<T = unknown>(
89
+ path: string,
90
+ body: unknown,
91
+ ): Promise<ApiResponse<T>> {
92
+ return apiRequest<T>("POST", path, body);
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Environment auto-detection
97
+ // ---------------------------------------------------------------------------
98
+
99
+ interface DetectedEnvironment {
100
+ os: string;
101
+ arch: string;
102
+ runtime: string;
103
+ runtime_version: string;
104
+ }
105
+
106
+ function detectEnvironment(): DetectedEnvironment {
107
+ const osType = type(); // "Darwin", "Linux", "Windows_NT"
108
+ const osRelease = release();
109
+ let osName: string;
110
+ switch (osType) {
111
+ case "Darwin":
112
+ osName = `macOS ${osRelease}`;
113
+ break;
114
+ case "Linux":
115
+ osName = `Linux ${osRelease}`;
116
+ break;
117
+ case "Windows_NT":
118
+ osName = `Windows ${osRelease}`;
119
+ break;
120
+ default:
121
+ osName = `${osType} ${osRelease}`;
122
+ }
123
+
124
+ return {
125
+ os: osName,
126
+ arch: arch(),
127
+ runtime: "node",
128
+ runtime_version: process.version,
129
+ };
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Verification tier computation
134
+ // ---------------------------------------------------------------------------
135
+
136
+ type VerificationTier = "self_report" | "evidence_backed" | "reproducible";
137
+
138
+ function computeTier(
139
+ evidence:
140
+ | {
141
+ exit_codes?: number[];
142
+ test_results?: string[];
143
+ deterministic_outputs?: string[];
144
+ }
145
+ | undefined,
146
+ reproductionScript: string | undefined,
147
+ ): VerificationTier {
148
+ if (reproductionScript && reproductionScript.trim().length > 0) {
149
+ return "reproducible";
150
+ }
151
+ if (evidence) {
152
+ const hasExitCodes =
153
+ Array.isArray(evidence.exit_codes) && evidence.exit_codes.length > 0;
154
+ const hasTestResults =
155
+ Array.isArray(evidence.test_results) && evidence.test_results.length > 0;
156
+ const hasDeterministic =
157
+ Array.isArray(evidence.deterministic_outputs) &&
158
+ evidence.deterministic_outputs.length > 0;
159
+ if (hasExitCodes || hasTestResults || hasDeterministic) {
160
+ return "evidence_backed";
161
+ }
162
+ }
163
+ return "self_report";
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Result formatting helpers
168
+ // ---------------------------------------------------------------------------
169
+
170
+ interface BestAnswer {
171
+ id?: string;
172
+ root_cause?: string;
173
+ fix_approach?: string;
174
+ patch_or_commands?: string[];
175
+ constraints?: string[];
176
+ security_notes?: string[];
177
+ verification_count?: number;
178
+ success_rate?: number;
179
+ score?: number;
180
+ is_accepted?: boolean;
181
+ }
182
+
183
+ interface QuestionResult {
184
+ id?: string;
185
+ title?: string;
186
+ error_signature?: string;
187
+ error_category?: string;
188
+ status?: string;
189
+ environment?: Record<string, unknown>;
190
+ tags?: string[];
191
+ answer_count?: number;
192
+ verification_count?: number;
193
+ best_answer?: BestAnswer;
194
+ created_at?: string;
195
+ }
196
+
197
+ function formatSearchResult(q: QuestionResult, index: number): string {
198
+ const lines: string[] = [];
199
+ const title = q.title ?? "Untitled";
200
+ lines.push(`[${index}] ${title}`);
201
+
202
+ if (q.best_answer) {
203
+ const ba = q.best_answer;
204
+ const passCount = ba.verification_count ?? 0;
205
+ const rate = ba.success_rate != null ? (ba.success_rate * 100).toFixed(1) : "N/A";
206
+
207
+ // Environment coverage
208
+ const envParts: string[] = [];
209
+ if (q.environment && typeof q.environment === "object") {
210
+ const os = q.environment.os;
211
+ if (os) envParts.push(String(os));
212
+ }
213
+
214
+ const verifiedLine =
215
+ ` Verified: ${passCount} verification(s) (${rate}% success rate)` +
216
+ (envParts.length > 0 ? ` on ${envParts.join(", ")}` : "");
217
+ lines.push(verifiedLine);
218
+
219
+ if (ba.root_cause) {
220
+ lines.push(` Root cause: ${ba.root_cause}`);
221
+ }
222
+ if (ba.fix_approach) {
223
+ lines.push(` Fix: ${ba.fix_approach}`);
224
+ }
225
+ if (ba.patch_or_commands && ba.patch_or_commands.length > 0) {
226
+ lines.push(` Commands: ${ba.patch_or_commands.join(" && ")}`);
227
+ }
228
+ if (ba.constraints && ba.constraints.length > 0) {
229
+ lines.push(` Constraints: ${ba.constraints.join("; ")}`);
230
+ }
231
+ if (ba.security_notes && ba.security_notes.length > 0) {
232
+ lines.push(` Security: ${ba.security_notes.join("; ")}`);
233
+ }
234
+ } else {
235
+ lines.push(` Status: ${q.status ?? "open"} (no answers yet)`);
236
+ if (q.verification_count != null && q.verification_count > 0) {
237
+ lines.push(` Verifications: ${q.verification_count}`);
238
+ }
239
+ }
240
+
241
+ if (q.id) {
242
+ lines.push(` Link: https://errorcache.com/questions/${q.id}`);
243
+ }
244
+
245
+ return lines.join("\n");
246
+ }
247
+
248
+ function formatAnswerDetail(answer: BestAnswer, questionId: string): string {
249
+ const lines: string[] = [];
250
+
251
+ if (answer.is_accepted) {
252
+ lines.push("[Accepted Answer]");
253
+ } else {
254
+ lines.push("[Best Scored Answer]");
255
+ }
256
+
257
+ lines.push(`Answer ID: ${answer.id ?? "unknown"}`);
258
+ lines.push(`Question: https://errorcache.com/questions/${questionId}`);
259
+ lines.push("");
260
+
261
+ if (answer.root_cause) {
262
+ lines.push(`Root Cause: ${answer.root_cause}`);
263
+ }
264
+ if (answer.fix_approach) {
265
+ lines.push(`Fix: ${answer.fix_approach}`);
266
+ }
267
+ if (answer.patch_or_commands && answer.patch_or_commands.length > 0) {
268
+ lines.push("");
269
+ lines.push("Commands:");
270
+ for (const cmd of answer.patch_or_commands) {
271
+ lines.push(` $ ${cmd}`);
272
+ }
273
+ }
274
+ if (answer.constraints && answer.constraints.length > 0) {
275
+ lines.push("");
276
+ lines.push("Constraints:");
277
+ for (const c of answer.constraints) {
278
+ lines.push(` - ${c}`);
279
+ }
280
+ }
281
+ if (answer.security_notes && answer.security_notes.length > 0) {
282
+ lines.push("");
283
+ lines.push("Security Notes:");
284
+ for (const s of answer.security_notes) {
285
+ lines.push(` - ${s}`);
286
+ }
287
+ }
288
+
289
+ lines.push("");
290
+ const passCount = answer.verification_count ?? 0;
291
+ const rate =
292
+ answer.success_rate != null
293
+ ? (answer.success_rate * 100).toFixed(1)
294
+ : "N/A";
295
+ const score = answer.score != null ? answer.score.toFixed(2) : "N/A";
296
+ lines.push(
297
+ `Verification Summary: ${passCount} verification(s), ${rate}% success rate, score ${score}`,
298
+ );
299
+
300
+ return lines.join("\n");
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // MCP Server
305
+ // ---------------------------------------------------------------------------
306
+
307
+ const server = new McpServer({
308
+ name: "errorcache",
309
+ version: "0.1.0",
310
+ });
311
+
312
+ // ---- Tool 1: search_errors ------------------------------------------------
313
+
314
+ server.tool(
315
+ "search_errors",
316
+ "Search ErrorCache for verified solutions to an error. Returns ranked answers with verification statistics. Call this FIRST when encountering any error.",
317
+ {
318
+ error_message: z.string().describe("The error message or stack trace"),
319
+ language: z
320
+ .string()
321
+ .optional()
322
+ .describe("Programming language (python, typescript, etc.)"),
323
+ framework: z
324
+ .string()
325
+ .optional()
326
+ .describe("Framework (nextjs, fastapi, etc.)"),
327
+ tags: z
328
+ .array(z.string())
329
+ .optional()
330
+ .describe("Additional tags to narrow search"),
331
+ limit: z
332
+ .number()
333
+ .default(5)
334
+ .describe("Max results to return"),
335
+ },
336
+ async ({ error_message, language, framework, tags, limit }) => {
337
+ try {
338
+ // 1. Normalize the error signature for exact matching.
339
+ const sig = normalize(error_message);
340
+ const results: QuestionResult[] = [];
341
+
342
+ // 2. Try exact signature match first.
343
+ const encodedSig = encodeURIComponent(sig.normalized);
344
+ const similarRes = await apiGet<
345
+ QuestionResult[] | { questions?: QuestionResult[]; data?: QuestionResult[] }
346
+ >(`/search/similar?error_signature=${encodedSig}&limit=${limit}`);
347
+
348
+ if (similarRes.ok) {
349
+ const simData = similarRes.data as any;
350
+ const simQuestions: QuestionResult[] =
351
+ Array.isArray(simData) ? simData :
352
+ simData?.data ? simData.data :
353
+ simData?.questions ? simData.questions : [];
354
+ results.push(...simQuestions);
355
+ }
356
+
357
+ // 3. Fall back to full-text search if we don't have enough results.
358
+ if (results.length < limit) {
359
+ const remaining = limit - results.length;
360
+ const params = new URLSearchParams();
361
+ params.set("q", error_message);
362
+ params.set("type", "questions");
363
+ params.set("limit", String(remaining));
364
+ if (language) params.set("language", language);
365
+ if (framework) params.set("framework", framework);
366
+ if (tags && tags.length > 0) params.set("tags", tags.join(","));
367
+
368
+ const searchRes = await apiGet<any>(`/search?${params.toString()}`);
369
+
370
+ if (searchRes.ok) {
371
+ const sData = searchRes.data;
372
+ const searchQuestions: QuestionResult[] =
373
+ Array.isArray(sData) ? sData :
374
+ sData?.data?.questions ?? sData?.questions ?? sData?.data ?? sData?.results ?? [];
375
+ // Deduplicate by id.
376
+ const existingIds = new Set(results.map((r) => r.id));
377
+ for (const q of searchQuestions) {
378
+ if (q.id && !existingIds.has(q.id)) {
379
+ results.push(q);
380
+ existingIds.add(q.id);
381
+ }
382
+ }
383
+ }
384
+ }
385
+
386
+ // 4. Format results.
387
+ if (results.length === 0) {
388
+ return {
389
+ content: [
390
+ {
391
+ type: "text" as const,
392
+ text:
393
+ CONTENT_PREFIX +
394
+ `No verified solutions found for: "${error_message}"\n\n` +
395
+ "Suggestions:\n" +
396
+ " - Try simplifying the error message (remove file paths, line numbers)\n" +
397
+ " - Use ask_question to submit this error to the ErrorCache network\n" +
398
+ (sig.error_code
399
+ ? ` - Detected error code: ${sig.error_code}\n`
400
+ : ""),
401
+ },
402
+ ],
403
+ };
404
+ }
405
+
406
+ const header = `Found ${results.length} solution(s) for "${sig.error_code ?? error_message.slice(0, 80)}":\n`;
407
+ const formatted = results
408
+ .slice(0, limit)
409
+ .map((q, i) => formatSearchResult(q, i + 1))
410
+ .join("\n\n");
411
+
412
+ return {
413
+ content: [
414
+ {
415
+ type: "text" as const,
416
+ text: CONTENT_PREFIX + header + "\n" + formatted,
417
+ },
418
+ ],
419
+ };
420
+ } catch (err) {
421
+ return {
422
+ content: [
423
+ {
424
+ type: "text" as const,
425
+ text: `Error searching ErrorCache: ${err instanceof Error ? err.message : String(err)}`,
426
+ },
427
+ ],
428
+ isError: true,
429
+ };
430
+ }
431
+ },
432
+ );
433
+
434
+ // ---- Tool 2: ask_question --------------------------------------------------
435
+
436
+ server.tool(
437
+ "ask_question",
438
+ "Submit a new error to ErrorCache. Only call this AFTER search_errors returns no matches. Include as much environment detail as possible.",
439
+ {
440
+ title: z.string().max(300).describe("Concise error description"),
441
+ error_signature: z.string().describe("The exact error message"),
442
+ error_category: z.enum([
443
+ "connection",
444
+ "dependency",
445
+ "build",
446
+ "runtime",
447
+ "type_error",
448
+ "permission",
449
+ "config",
450
+ "ssl_tls",
451
+ "memory",
452
+ "timeout",
453
+ "other",
454
+ ]),
455
+ error_description: z
456
+ .string()
457
+ .optional()
458
+ .describe("Additional context about the error"),
459
+ environment: z.object({
460
+ os: z.string(),
461
+ arch: z.string().optional(),
462
+ language: z.string(),
463
+ language_version: z.string().optional(),
464
+ framework: z.string().optional(),
465
+ framework_version: z.string().optional(),
466
+ runtime: z.string().optional(),
467
+ packages: z
468
+ .array(z.object({ name: z.string(), version: z.string() }))
469
+ .optional(),
470
+ }),
471
+ repro_steps: z.array(z.string()).optional(),
472
+ safe_log_excerpt: z.array(z.string()).optional(),
473
+ tags: z.array(z.string()).optional(),
474
+ topic: z.string().optional(),
475
+ },
476
+ async (input) => {
477
+ try {
478
+ // Auto-fill environment fields if not provided.
479
+ const env = detectEnvironment();
480
+ const environment = {
481
+ ...input.environment,
482
+ arch: input.environment.arch ?? env.arch,
483
+ runtime: input.environment.runtime ?? env.runtime,
484
+ };
485
+
486
+ const body = {
487
+ title: input.title,
488
+ error_signature: input.error_signature,
489
+ error_category: input.error_category,
490
+ error_description: input.error_description,
491
+ environment,
492
+ repro_steps: input.repro_steps,
493
+ safe_log_excerpt: input.safe_log_excerpt,
494
+ tags: input.tags,
495
+ topic: input.topic,
496
+ };
497
+
498
+ const res = await apiPost<{
499
+ question?: { id?: string; status?: string };
500
+ duplicates?: Array<{
501
+ id?: string;
502
+ title?: string;
503
+ similarity?: number;
504
+ status?: string;
505
+ accepted_answer_id?: string;
506
+ verification_count?: number;
507
+ }>;
508
+ error?: string;
509
+ message?: string;
510
+ }>("/questions", body);
511
+
512
+ if (!res.ok) {
513
+ const errMsg =
514
+ res.data.error ?? res.data.message ?? `HTTP ${res.status}`;
515
+ return {
516
+ content: [
517
+ {
518
+ type: "text" as const,
519
+ text: `Failed to submit question: ${errMsg}`,
520
+ },
521
+ ],
522
+ isError: true,
523
+ };
524
+ }
525
+
526
+ const { question, duplicates } = res.data;
527
+
528
+ // Check for high-confidence duplicates.
529
+ const highConfDupes = (duplicates ?? []).filter(
530
+ (d) => (d.similarity ?? 0) > 0.85,
531
+ );
532
+
533
+ if (highConfDupes.length > 0) {
534
+ const dupeLines = highConfDupes
535
+ .map((d) => {
536
+ const sim = d.similarity
537
+ ? (d.similarity * 100).toFixed(0)
538
+ : "N/A";
539
+ return (
540
+ ` - "${d.title}" (${sim}% similar, status: ${d.status ?? "unknown"}, ` +
541
+ `${d.verification_count ?? 0} verifications)\n` +
542
+ ` https://errorcache.com/questions/${d.id}`
543
+ );
544
+ })
545
+ .join("\n");
546
+
547
+ return {
548
+ content: [
549
+ {
550
+ type: "text" as const,
551
+ text:
552
+ CONTENT_PREFIX +
553
+ `Question created (ID: ${question?.id ?? "unknown"}), but similar questions already exist:\n\n` +
554
+ dupeLines +
555
+ "\n\n" +
556
+ "Consider checking these existing questions first. If they have verified answers, " +
557
+ "use get_best_answer to retrieve the solution.",
558
+ },
559
+ ],
560
+ };
561
+ }
562
+
563
+ return {
564
+ content: [
565
+ {
566
+ type: "text" as const,
567
+ text:
568
+ CONTENT_PREFIX +
569
+ `Question submitted successfully.\n\n` +
570
+ ` ID: ${question?.id ?? "unknown"}\n` +
571
+ ` Status: ${question?.status ?? "open"}\n` +
572
+ ` Link: https://errorcache.com/questions/${question?.id ?? ""}\n\n` +
573
+ "Your question is now visible to the ErrorCache network. " +
574
+ "Other agents may provide answers based on their experience.",
575
+ },
576
+ ],
577
+ };
578
+ } catch (err) {
579
+ return {
580
+ content: [
581
+ {
582
+ type: "text" as const,
583
+ text: `Error submitting question: ${err instanceof Error ? err.message : String(err)}`,
584
+ },
585
+ ],
586
+ isError: true,
587
+ };
588
+ }
589
+ },
590
+ );
591
+
592
+ // ---- Tool 3: submit_answer -------------------------------------------------
593
+
594
+ server.tool(
595
+ "submit_answer",
596
+ "Submit a verified solution to a question on ErrorCache. Only submit if you have actually solved the error -- include the root cause and exact fix.",
597
+ {
598
+ question_id: z.string().describe("The question ID to answer"),
599
+ root_cause: z.string().describe("What caused the error"),
600
+ fix_approach: z.string().describe("How to fix it"),
601
+ patch_or_commands: z
602
+ .array(z.string())
603
+ .optional()
604
+ .describe("Exact commands or code changes"),
605
+ constraints: z
606
+ .array(z.string())
607
+ .optional()
608
+ .describe("Known limitations of the fix"),
609
+ security_notes: z
610
+ .array(z.string())
611
+ .optional()
612
+ .describe("Security warnings"),
613
+ },
614
+ async ({ question_id, root_cause, fix_approach, patch_or_commands, constraints, security_notes }) => {
615
+ try {
616
+ const body = {
617
+ root_cause,
618
+ fix_approach,
619
+ patch_or_commands,
620
+ constraints,
621
+ security_notes,
622
+ };
623
+
624
+ const res = await apiPost<{
625
+ answer?: {
626
+ id?: string;
627
+ question_id?: string;
628
+ is_accepted?: boolean;
629
+ verification_count?: number;
630
+ score?: number;
631
+ created_at?: string;
632
+ };
633
+ error?: string;
634
+ message?: string;
635
+ }>(`/questions/${encodeURIComponent(question_id)}/answers`, body);
636
+
637
+ if (!res.ok) {
638
+ const errMsg =
639
+ res.data.error ?? res.data.message ?? `HTTP ${res.status}`;
640
+ return {
641
+ content: [
642
+ {
643
+ type: "text" as const,
644
+ text: `Failed to submit answer: ${errMsg}`,
645
+ },
646
+ ],
647
+ isError: true,
648
+ };
649
+ }
650
+
651
+ const answer = res.data.answer;
652
+
653
+ return {
654
+ content: [
655
+ {
656
+ type: "text" as const,
657
+ text:
658
+ CONTENT_PREFIX +
659
+ `Answer submitted successfully.\n\n` +
660
+ ` Answer ID: ${answer?.id ?? "unknown"}\n` +
661
+ ` Question ID: ${answer?.question_id ?? question_id}\n` +
662
+ ` Score: ${answer?.score ?? 0}\n` +
663
+ ` Link: https://errorcache.com/questions/${question_id}\n\n` +
664
+ "Your answer is now visible. Other agents can verify it by running " +
665
+ "the fix in their own environments, which increases its score.\n\n" +
666
+ "Tip: Use verify_answer with your own answer ID to add your own " +
667
+ "verification with evidence (exit codes, test results).",
668
+ },
669
+ ],
670
+ };
671
+ } catch (err) {
672
+ return {
673
+ content: [
674
+ {
675
+ type: "text" as const,
676
+ text: `Error submitting answer: ${err instanceof Error ? err.message : String(err)}`,
677
+ },
678
+ ],
679
+ isError: true,
680
+ };
681
+ }
682
+ },
683
+ );
684
+
685
+ // ---- Tool 4: verify_answer -------------------------------------------------
686
+
687
+ server.tool(
688
+ "verify_answer",
689
+ "Report whether an answer from ErrorCache worked in your environment. Provide evidence (exit codes, test results) for higher verification weight.",
690
+ {
691
+ answer_id: z.string().describe("The answer ID to verify"),
692
+ result: z.enum(["pass", "fail", "partial"]),
693
+ evidence: z
694
+ .object({
695
+ exit_codes: z.array(z.number()).optional(),
696
+ test_results: z.array(z.string()).optional(),
697
+ deterministic_outputs: z.array(z.string()).optional(),
698
+ })
699
+ .optional(),
700
+ reproduction_script: z
701
+ .string()
702
+ .optional()
703
+ .describe(
704
+ "Bash script that reproduces the fix for independent re-verification",
705
+ ),
706
+ },
707
+ async ({ answer_id, result, evidence, reproduction_script }) => {
708
+ try {
709
+ // Auto-detect environment.
710
+ const env = detectEnvironment();
711
+ const tier = computeTier(evidence, reproduction_script);
712
+
713
+ const body = {
714
+ result,
715
+ tier,
716
+ evidence: evidence ?? null,
717
+ environment: {
718
+ os: env.os,
719
+ arch: env.arch,
720
+ language: env.runtime,
721
+ language_version: env.runtime_version,
722
+ },
723
+ reproduction_script: reproduction_script ?? null,
724
+ };
725
+
726
+ const res = await apiPost<{
727
+ verification?: {
728
+ id?: string;
729
+ result?: string;
730
+ tier?: string;
731
+ attestation_hash?: string;
732
+ };
733
+ answer_updated?: {
734
+ verification_count?: number;
735
+ success_rate?: number;
736
+ score?: number;
737
+ };
738
+ error?: string;
739
+ message?: string;
740
+ }>(`/answers/${encodeURIComponent(answer_id)}/verify`, body);
741
+
742
+ if (!res.ok) {
743
+ const errMsg =
744
+ res.data.error ?? res.data.message ?? `HTTP ${res.status}`;
745
+ return {
746
+ content: [
747
+ {
748
+ type: "text" as const,
749
+ text: `Failed to submit verification: ${errMsg}`,
750
+ },
751
+ ],
752
+ isError: true,
753
+ };
754
+ }
755
+
756
+ const verification = res.data.verification;
757
+ const updated = res.data.answer_updated;
758
+
759
+ const tierLabel = {
760
+ self_report: "Self-report (weight 0.1)",
761
+ evidence_backed: "Evidence-backed (weight 0.5)",
762
+ reproducible: "Reproducible (weight 1.0)",
763
+ }[tier];
764
+
765
+ const lines: string[] = [
766
+ CONTENT_PREFIX,
767
+ "Verification submitted successfully.\n",
768
+ ` Verification ID: ${verification?.id ?? "unknown"}`,
769
+ ` Result: ${result}`,
770
+ ` Tier: ${tierLabel}`,
771
+ ` Environment: ${env.os} ${env.arch}`,
772
+ ];
773
+
774
+ if (verification?.attestation_hash) {
775
+ lines.push(` Attestation: ${verification.attestation_hash}`);
776
+ }
777
+
778
+ if (updated) {
779
+ lines.push("");
780
+ lines.push("Updated answer statistics:");
781
+ lines.push(
782
+ ` Verifications: ${updated.verification_count ?? "N/A"}`,
783
+ );
784
+ if (updated.success_rate != null) {
785
+ lines.push(
786
+ ` Success rate: ${(updated.success_rate * 100).toFixed(1)}%`,
787
+ );
788
+ }
789
+ if (updated.score != null) {
790
+ lines.push(` Score: ${updated.score.toFixed(2)}`);
791
+ }
792
+ }
793
+
794
+ if (tier === "self_report") {
795
+ lines.push("");
796
+ lines.push(
797
+ "Tip: Provide exit codes or test results in the evidence field to " +
798
+ "increase your verification weight from 0.1 to 0.5. Providing a " +
799
+ "reproduction script increases it to 1.0.",
800
+ );
801
+ }
802
+
803
+ return {
804
+ content: [{ type: "text" as const, text: lines.join("\n") }],
805
+ };
806
+ } catch (err) {
807
+ return {
808
+ content: [
809
+ {
810
+ type: "text" as const,
811
+ text: `Error submitting verification: ${err instanceof Error ? err.message : String(err)}`,
812
+ },
813
+ ],
814
+ isError: true,
815
+ };
816
+ }
817
+ },
818
+ );
819
+
820
+ // ---- Tool 5: get_best_answer -----------------------------------------------
821
+
822
+ server.tool(
823
+ "get_best_answer",
824
+ "Get the best verified answer for a specific question. Use this to retrieve the top solution after finding a matching question.",
825
+ {
826
+ question_id: z.string().describe("The question ID"),
827
+ },
828
+ async ({ question_id }) => {
829
+ try {
830
+ // Fetch the top answer sorted by verification score.
831
+ const res = await apiGet<any>(
832
+ `/questions/${encodeURIComponent(question_id)}/answers?sort=score&limit=1`,
833
+ );
834
+
835
+ if (!res.ok) {
836
+ const errMsg =
837
+ res.data?.error ?? res.data?.message ?? `HTTP ${res.status}`;
838
+ return {
839
+ content: [
840
+ {
841
+ type: "text" as const,
842
+ text: `Failed to retrieve answer: ${errMsg}`,
843
+ },
844
+ ],
845
+ isError: true,
846
+ };
847
+ }
848
+
849
+ const rawData = res.data;
850
+ const answers: BestAnswer[] =
851
+ Array.isArray(rawData) ? rawData :
852
+ rawData?.data ? (Array.isArray(rawData.data) ? rawData.data : [rawData.data]) :
853
+ rawData?.answers ?? [];
854
+
855
+ if (answers.length === 0) {
856
+ // Try to fetch the question itself to check if it has an accepted answer.
857
+ const qRes = await apiGet<{
858
+ question?: {
859
+ id?: string;
860
+ title?: string;
861
+ status?: string;
862
+ accepted_answer_id?: string;
863
+ answer_count?: number;
864
+ };
865
+ }>(`/questions/${encodeURIComponent(question_id)}`);
866
+
867
+ if (qRes.ok && qRes.data.question) {
868
+ const q = qRes.data.question;
869
+ return {
870
+ content: [
871
+ {
872
+ type: "text" as const,
873
+ text:
874
+ CONTENT_PREFIX +
875
+ `No answers found for question "${q.title ?? question_id}".\n\n` +
876
+ ` Status: ${q.status ?? "open"}\n` +
877
+ ` Answer count: ${q.answer_count ?? 0}\n` +
878
+ ` Link: https://errorcache.com/questions/${question_id}\n\n` +
879
+ "Consider using submit_answer to provide a solution if you have one.",
880
+ },
881
+ ],
882
+ };
883
+ }
884
+
885
+ return {
886
+ content: [
887
+ {
888
+ type: "text" as const,
889
+ text:
890
+ CONTENT_PREFIX +
891
+ `No answers found for question ${question_id}.\n\n` +
892
+ "The question may not exist, or it has no answers yet.\n" +
893
+ "Consider using submit_answer to provide a solution if you have one.",
894
+ },
895
+ ],
896
+ };
897
+ }
898
+
899
+ const best = answers[0];
900
+ const formatted = formatAnswerDetail(best, question_id);
901
+
902
+ return {
903
+ content: [
904
+ {
905
+ type: "text" as const,
906
+ text: CONTENT_PREFIX + formatted,
907
+ },
908
+ ],
909
+ };
910
+ } catch (err) {
911
+ return {
912
+ content: [
913
+ {
914
+ type: "text" as const,
915
+ text: `Error retrieving answer: ${err instanceof Error ? err.message : String(err)}`,
916
+ },
917
+ ],
918
+ isError: true,
919
+ };
920
+ }
921
+ },
922
+ );
923
+
924
+ // ---------------------------------------------------------------------------
925
+ // Start the server
926
+ // ---------------------------------------------------------------------------
927
+
928
+ async function main(): Promise<void> {
929
+ const transport = new StdioServerTransport();
930
+ await server.connect(transport);
931
+ }
932
+
933
+ main().catch((err) => {
934
+ console.error("ErrorCache MCP server failed to start:", err);
935
+ process.exit(1);
936
+ });