@standardbeagle/errlookup-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/README.md +36 -0
- package/dist/index.js +452 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @standardbeagle/errlookup-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that answers **"what is this error and how do I fix it"** for open-source libraries, from a locally cached static dataset published at [errors.standardbeagle.com](https://errors.standardbeagle.com).
|
|
4
|
+
|
|
5
|
+
No backend: on first use it downloads versioned JSON from the CDN, caches under `~/.cache/errlookup/`, and serves every query offline-capable from disk.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"errlookup": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@standardbeagle/errlookup-mcp"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Tools
|
|
21
|
+
|
|
22
|
+
- `search_error` — pass a raw runtime error message; tiered matching (exact code → message pattern → fuzzy) returns scored matches with citations.
|
|
23
|
+
- `get_error` — full record for one error: explanation, ordered solutions, example fix, defensive patterns, source permalink.
|
|
24
|
+
- `list_repos` — analyzed repositories and their error counts.
|
|
25
|
+
- `refresh_dataset` — force a freshness check against the published manifest.
|
|
26
|
+
|
|
27
|
+
## Config (env)
|
|
28
|
+
|
|
29
|
+
| var | default | |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `ERRLOOKUP_BASE_URL` | `https://errors.standardbeagle.com` | dataset origin |
|
|
32
|
+
| `ERRLOOKUP_CACHE_DIR` | `$XDG_CACHE_HOME/errlookup` | cache location |
|
|
33
|
+
| `ERRLOOKUP_TTL_SECONDS` | `300` | manifest poll interval |
|
|
34
|
+
| `ERRLOOKUP_OFFLINE` | unset | `1` = never fetch, cache only |
|
|
35
|
+
|
|
36
|
+
Data is AI-assisted analysis of public repositories, pinned to the analyzed commit SHA — every result links its source and its JSON twin.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/server.ts
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import {
|
|
8
|
+
CallToolRequestSchema,
|
|
9
|
+
ListToolsRequestSchema
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
|
|
12
|
+
// src/cache.ts
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join, dirname, resolve } from "node:path";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
|
|
18
|
+
// src/base-url.ts
|
|
19
|
+
var DEFAULT_BASE_URL = "https://errors.standardbeagle.com";
|
|
20
|
+
function siteBaseUrl(env = process.env) {
|
|
21
|
+
return (env.ERRLOOKUP_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
22
|
+
}
|
|
23
|
+
function siteErrorUrl(repo, slug) {
|
|
24
|
+
const [owner, name] = repo.split("/");
|
|
25
|
+
return `${siteBaseUrl()}/${owner}/${name}/${slug}/`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/cache.ts
|
|
29
|
+
function defaultCacheConfig(env = process.env) {
|
|
30
|
+
const xdg = env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
31
|
+
return {
|
|
32
|
+
baseUrl: siteBaseUrl(env),
|
|
33
|
+
cacheDir: env.ERRLOOKUP_CACHE_DIR ?? join(xdg, "errlookup"),
|
|
34
|
+
ttlSeconds: Number.parseInt(env.ERRLOOKUP_TTL_SECONDS ?? "300", 10),
|
|
35
|
+
offline: env.ERRLOOKUP_OFFLINE === "1"
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function fileExists(p) {
|
|
39
|
+
return existsSync(p);
|
|
40
|
+
}
|
|
41
|
+
function readJson(p) {
|
|
42
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
43
|
+
}
|
|
44
|
+
function atomicWrite(p, content) {
|
|
45
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
46
|
+
const tmp = `${p}.tmp-${process.pid}`;
|
|
47
|
+
writeFileSync(tmp, content, "utf8");
|
|
48
|
+
renameSync(tmp, p);
|
|
49
|
+
}
|
|
50
|
+
function sha256(content) {
|
|
51
|
+
return createHash("sha256").update(content).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
var CacheStore = class {
|
|
54
|
+
constructor(cfg) {
|
|
55
|
+
this.cfg = cfg;
|
|
56
|
+
}
|
|
57
|
+
get dir() {
|
|
58
|
+
return this.cfg.cacheDir;
|
|
59
|
+
}
|
|
60
|
+
manifestPath() {
|
|
61
|
+
return join(this.cfg.cacheDir, "manifest.json");
|
|
62
|
+
}
|
|
63
|
+
indexPath() {
|
|
64
|
+
return join(this.cfg.cacheDir, "index.json");
|
|
65
|
+
}
|
|
66
|
+
repoPath(repo) {
|
|
67
|
+
const [owner, name] = repo.split("/");
|
|
68
|
+
return join(this.cfg.cacheDir, "repos", `${owner}`, `${name}.json`);
|
|
69
|
+
}
|
|
70
|
+
hasManifest() {
|
|
71
|
+
return fileExists(this.manifestPath());
|
|
72
|
+
}
|
|
73
|
+
readManifest() {
|
|
74
|
+
return this.hasManifest() ? readJson(this.manifestPath()) : null;
|
|
75
|
+
}
|
|
76
|
+
readIndex() {
|
|
77
|
+
return fileExists(this.indexPath()) ? readJson(this.indexPath()) : null;
|
|
78
|
+
}
|
|
79
|
+
readRepo(repo) {
|
|
80
|
+
const p = this.repoPath(repo);
|
|
81
|
+
return fileExists(p) ? readJson(p) : null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Download a file, verify its sha256 against the manifest, write atomically.
|
|
85
|
+
* Returns false (and keeps the old cache) if the hash mismatches — corrupt
|
|
86
|
+
* downloads are rejected, never served (§8 mcp suite).
|
|
87
|
+
*/
|
|
88
|
+
async fetchVerified(urlPath, dest, expectedSha) {
|
|
89
|
+
const url = `${this.cfg.baseUrl}${urlPath}`;
|
|
90
|
+
const res = await fetch(url);
|
|
91
|
+
if (!res.ok) throw new Error(`GET ${urlPath} \u2192 ${res.status}`);
|
|
92
|
+
const text = await res.text();
|
|
93
|
+
if (expectedSha && sha256(text) !== expectedSha) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
atomicWrite(dest, text);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/sync.ts
|
|
102
|
+
async function syncDataset(store, ttlOk) {
|
|
103
|
+
const cachedManifest = store.readManifest();
|
|
104
|
+
const cachedIndex = store.readIndex();
|
|
105
|
+
if (ttlOk) {
|
|
106
|
+
return {
|
|
107
|
+
updated: false,
|
|
108
|
+
datasetVersion: cachedManifest?.datasetVersion ?? null,
|
|
109
|
+
errorCount: cachedIndex?.errors.length ?? 0,
|
|
110
|
+
stale: false
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
let manifest;
|
|
114
|
+
try {
|
|
115
|
+
const ok = await store.fetchVerified("/data/manifest.json", store.manifestPath());
|
|
116
|
+
if (!ok) {
|
|
117
|
+
return staleOrError(store, cachedManifest, cachedIndex);
|
|
118
|
+
}
|
|
119
|
+
manifest = store.readManifest();
|
|
120
|
+
} catch {
|
|
121
|
+
return staleOrError(store, cachedManifest, cachedIndex);
|
|
122
|
+
}
|
|
123
|
+
if (cachedManifest && manifest.datasetVersion === cachedManifest.datasetVersion && cachedIndex) {
|
|
124
|
+
return { updated: false, datasetVersion: manifest.datasetVersion, errorCount: cachedIndex.errors.length, stale: false };
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const ok = await store.fetchVerified(
|
|
128
|
+
"/data/index.json",
|
|
129
|
+
store.indexPath(),
|
|
130
|
+
manifest.files.index?.sha256
|
|
131
|
+
);
|
|
132
|
+
if (!ok) {
|
|
133
|
+
return staleOrError(store, cachedManifest, cachedIndex);
|
|
134
|
+
}
|
|
135
|
+
const idx = store.readIndex();
|
|
136
|
+
return { updated: true, datasetVersion: manifest.datasetVersion, errorCount: idx.errors.length, stale: false };
|
|
137
|
+
} catch {
|
|
138
|
+
return staleOrError(store, cachedManifest, cachedIndex);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function staleOrError(store, cachedManifest, cachedIndex) {
|
|
142
|
+
if (cachedManifest && cachedIndex) {
|
|
143
|
+
return {
|
|
144
|
+
updated: false,
|
|
145
|
+
datasetVersion: cachedManifest.datasetVersion,
|
|
146
|
+
errorCount: cachedIndex.errors.length,
|
|
147
|
+
stale: true
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { updated: false, datasetVersion: null, errorCount: 0, stale: true };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/search.ts
|
|
154
|
+
var PATTERN_BUDGET_MS = 50;
|
|
155
|
+
var FUZZY_THRESHOLD = 0.4;
|
|
156
|
+
function extractCodeTokens(input) {
|
|
157
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
158
|
+
for (const m of input.matchAll(/\b[A-Z][A-Z0-9_]{2,}\b/g)) tokens.add(m[0]);
|
|
159
|
+
for (const m of input.matchAll(/\bE[A-Z]+\b/g)) tokens.add(m[0]);
|
|
160
|
+
return tokens;
|
|
161
|
+
}
|
|
162
|
+
function tierExactCode(input, errors) {
|
|
163
|
+
const tokens = extractCodeTokens(input);
|
|
164
|
+
if (tokens.size === 0) return [];
|
|
165
|
+
return errors.filter((e) => e.code !== null && tokens.has(e.code));
|
|
166
|
+
}
|
|
167
|
+
function tierPattern(input, errors) {
|
|
168
|
+
const start = Date.now();
|
|
169
|
+
const hits = [];
|
|
170
|
+
for (const e of errors) {
|
|
171
|
+
if (e.pattern.length > 500) continue;
|
|
172
|
+
if (Date.now() - start > PATTERN_BUDGET_MS) break;
|
|
173
|
+
try {
|
|
174
|
+
if (new RegExp(e.pattern).test(input)) hits.push(e);
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return hits;
|
|
179
|
+
}
|
|
180
|
+
function normalizeForFuzzy(s) {
|
|
181
|
+
return s.toLowerCase().replace(/\/[\w./-]+/g, " ").replace(/0x[0-9a-f]+/g, " ").replace(/\b\d+\b/g, " ").replace(/'[^']*'|"[^"]*"/g, " ").replace(/[^a-z ]+/g, " ").replace(/\s+/g, " ").trim();
|
|
182
|
+
}
|
|
183
|
+
function buildIdf(errors) {
|
|
184
|
+
const df = /* @__PURE__ */ new Map();
|
|
185
|
+
const N = errors.length || 1;
|
|
186
|
+
for (const e of errors) {
|
|
187
|
+
const seen = new Set(normalizeForFuzzy(e.msg).split(" ").filter(Boolean));
|
|
188
|
+
for (const t of seen) df.set(t, (df.get(t) ?? 0) + 1);
|
|
189
|
+
}
|
|
190
|
+
const idf = /* @__PURE__ */ new Map();
|
|
191
|
+
for (const [t, d] of df) idf.set(t, Math.log((N + 1) / (d + 1)) + 1);
|
|
192
|
+
return idf;
|
|
193
|
+
}
|
|
194
|
+
function tierFuzzy(input, errors, idf) {
|
|
195
|
+
const inTokens = new Set(normalizeForFuzzy(input).split(" ").filter(Boolean));
|
|
196
|
+
if (inTokens.size === 0) return [];
|
|
197
|
+
const scored = [];
|
|
198
|
+
for (const e of errors) {
|
|
199
|
+
const eTokens = new Set(normalizeForFuzzy(e.msg).split(" ").filter(Boolean));
|
|
200
|
+
if (eTokens.size === 0) continue;
|
|
201
|
+
let inter = 0;
|
|
202
|
+
let union = 0;
|
|
203
|
+
const all = /* @__PURE__ */ new Set([...inTokens, ...eTokens]);
|
|
204
|
+
for (const t of all) {
|
|
205
|
+
const w = idf.get(t) ?? 1;
|
|
206
|
+
const inA = inTokens.has(t);
|
|
207
|
+
const inB = eTokens.has(t);
|
|
208
|
+
if (inA && inB) inter += w;
|
|
209
|
+
union += w;
|
|
210
|
+
}
|
|
211
|
+
const score = union > 0 ? inter / union : 0;
|
|
212
|
+
if (score >= FUZZY_THRESHOLD) scored.push({ entry: e, score });
|
|
213
|
+
}
|
|
214
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
215
|
+
}
|
|
216
|
+
function toHit(e, score, matchType) {
|
|
217
|
+
return {
|
|
218
|
+
id: e.id,
|
|
219
|
+
repo: e.repo,
|
|
220
|
+
code: e.code,
|
|
221
|
+
message: e.msg,
|
|
222
|
+
score,
|
|
223
|
+
matchType,
|
|
224
|
+
url: siteErrorUrl(e.repo, e.slug)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function searchErrors(input, errors, opts = {}) {
|
|
228
|
+
const limit = opts.limit ?? 5;
|
|
229
|
+
const pool = opts.repo ? errors.filter((e) => e.repo === opts.repo) : errors;
|
|
230
|
+
if (pool.length === 0) return [];
|
|
231
|
+
const code = tierExactCode(input, pool);
|
|
232
|
+
if (code.length > 0) {
|
|
233
|
+
return dedupe(code.map((e) => toHit(e, 1, "exact-code"))).slice(0, limit);
|
|
234
|
+
}
|
|
235
|
+
const pat = tierPattern(input, pool);
|
|
236
|
+
if (pat.length > 0) {
|
|
237
|
+
return dedupe(pat.map((e) => toHit(e, 0.9, "pattern"))).slice(0, limit);
|
|
238
|
+
}
|
|
239
|
+
const idf = buildIdf(pool);
|
|
240
|
+
const fuzzy = tierFuzzy(input, pool, idf);
|
|
241
|
+
if (fuzzy.length > 0) {
|
|
242
|
+
return dedupe(fuzzy.map((f) => toHit(f.entry, round(f.score), "fuzzy"))).slice(0, limit);
|
|
243
|
+
}
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
function dedupe(hits) {
|
|
247
|
+
const seen = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const h of hits) if (!seen.has(h.id)) seen.set(h.id, h);
|
|
249
|
+
return [...seen.values()];
|
|
250
|
+
}
|
|
251
|
+
function round(n) {
|
|
252
|
+
return Math.round(n * 1e3) / 1e3;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/tools.ts
|
|
256
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
257
|
+
async function ensureFresh(ctx) {
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
const ttlOk = now - ctx.lastSyncAt < ctx.ttlSeconds * 1e3;
|
|
260
|
+
return syncDataset(ctx.store, ttlOk);
|
|
261
|
+
}
|
|
262
|
+
function manifestVersion(ctx) {
|
|
263
|
+
return ctx.store.readManifest()?.datasetVersion ?? null;
|
|
264
|
+
}
|
|
265
|
+
async function toolSearchError(ctx, args) {
|
|
266
|
+
await ensureFresh(ctx);
|
|
267
|
+
const idx = ctx.store.readIndex();
|
|
268
|
+
if (!idx) {
|
|
269
|
+
throw new Error("No dataset cached and network unavailable. Run `refresh_dataset` while online.");
|
|
270
|
+
}
|
|
271
|
+
const matches = searchErrors(args.message, idx.errors, { repo: args.repo, limit: args.limit });
|
|
272
|
+
return { matches, datasetVersion: idx.datasetVersion, stale: false };
|
|
273
|
+
}
|
|
274
|
+
async function loadRepoRecords(ctx, repo) {
|
|
275
|
+
let cached = ctx.store.readRepo(repo);
|
|
276
|
+
if (!cached) {
|
|
277
|
+
const manifest = ctx.store.readManifest();
|
|
278
|
+
if (!manifest) throw new Error("No manifest cached; refresh while online.");
|
|
279
|
+
await ctx.store.fetchVerified(`/data/repos/${repo.split("/").join("/")}.json`, ctx.store.repoPath(repo));
|
|
280
|
+
cached = ctx.store.readRepo(repo);
|
|
281
|
+
}
|
|
282
|
+
return cached ?? [];
|
|
283
|
+
}
|
|
284
|
+
async function toolGetError(ctx, args) {
|
|
285
|
+
await ensureFresh(ctx);
|
|
286
|
+
const idx = ctx.store.readIndex();
|
|
287
|
+
if (!idx) throw new Error("No dataset cached and network unavailable.");
|
|
288
|
+
let match;
|
|
289
|
+
if (args.id) {
|
|
290
|
+
match = idx.errors.find((e) => e.id === args.id);
|
|
291
|
+
} else if (args.repo && args.slug) {
|
|
292
|
+
match = idx.errors.find((e) => e.repo === args.repo && e.slug === args.slug);
|
|
293
|
+
}
|
|
294
|
+
if (!match) throw new Error("Error not found in index.");
|
|
295
|
+
const url = siteErrorUrl(match.repo, match.slug);
|
|
296
|
+
const records = await loadRepoRecords(ctx, match.repo);
|
|
297
|
+
const full = records.find((r) => r.id === match.id);
|
|
298
|
+
if (!full) throw new Error(`Record ${match.id} missing from repo file.`);
|
|
299
|
+
return { markdown: renderMarkdown(full), url, datasetVersion: idx.datasetVersion };
|
|
300
|
+
}
|
|
301
|
+
async function toolListRepos(ctx) {
|
|
302
|
+
await ensureFresh(ctx);
|
|
303
|
+
const reposPath = `${ctx.store.dir}/repos.json`;
|
|
304
|
+
if (!existsSync2(reposPath)) {
|
|
305
|
+
try {
|
|
306
|
+
await ctx.store.fetchVerified("/data/repos.json", reposPath);
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
let repos = [];
|
|
311
|
+
try {
|
|
312
|
+
repos = JSON.parse(readFileSync2(reposPath, "utf8"));
|
|
313
|
+
} catch {
|
|
314
|
+
const idx = ctx.store.readIndex();
|
|
315
|
+
if (idx) {
|
|
316
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
317
|
+
for (const e of idx.errors) byRepo.set(e.repo, (byRepo.get(e.repo) ?? 0) + 1);
|
|
318
|
+
repos = [...byRepo.entries()].map(([repo, errorCount]) => ({ repo, description: null, errorCount }));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { repos, datasetVersion: manifestVersion(ctx) };
|
|
322
|
+
}
|
|
323
|
+
async function toolRefreshDataset(ctx) {
|
|
324
|
+
const r = await syncDataset(ctx.store, false);
|
|
325
|
+
if (r.datasetVersion) ctx.lastSyncAt = Date.now();
|
|
326
|
+
return { updated: r.updated, datasetVersion: r.datasetVersion, errors: r.errorCount };
|
|
327
|
+
}
|
|
328
|
+
function renderMarkdown(e) {
|
|
329
|
+
const lines = [];
|
|
330
|
+
lines.push(`# ${e.errorCode ?? e.errorMessage.slice(0, 80)}`);
|
|
331
|
+
lines.push("");
|
|
332
|
+
lines.push(`**Repo:** ${e.repo}`);
|
|
333
|
+
lines.push(`**Message:** \`${e.errorMessage}\``);
|
|
334
|
+
if (e.errorClass) lines.push(`**Class:** ${e.errorClass}`);
|
|
335
|
+
lines.push(`**Severity:** ${e.severity}`);
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push("## What it means");
|
|
338
|
+
lines.push(e.documentation);
|
|
339
|
+
lines.push("");
|
|
340
|
+
if (e.solutions.length > 0) {
|
|
341
|
+
lines.push("## Solutions");
|
|
342
|
+
for (let i = 0; i < e.solutions.length; i++) lines.push(`${i + 1}. ${e.solutions[i]}`);
|
|
343
|
+
lines.push("");
|
|
344
|
+
}
|
|
345
|
+
if (e.exampleFix) {
|
|
346
|
+
lines.push("## Example fix");
|
|
347
|
+
lines.push("```");
|
|
348
|
+
lines.push(e.exampleFix);
|
|
349
|
+
lines.push("```");
|
|
350
|
+
lines.push("");
|
|
351
|
+
}
|
|
352
|
+
if (e.handlingStrategy || e.tryCatchPattern || e.preventionTips.length > 0) {
|
|
353
|
+
lines.push("## Defensive pattern");
|
|
354
|
+
if (e.handlingStrategy) lines.push(`Strategy: ${e.handlingStrategy}`);
|
|
355
|
+
if (e.tryCatchPattern) {
|
|
356
|
+
lines.push("```");
|
|
357
|
+
lines.push(e.tryCatchPattern);
|
|
358
|
+
lines.push("```");
|
|
359
|
+
}
|
|
360
|
+
if (e.preventionTips.length > 0) lines.push(`Prevention: ${e.preventionTips.join("; ")}`);
|
|
361
|
+
lines.push("");
|
|
362
|
+
}
|
|
363
|
+
lines.push(`Source: ${e.githubUrl}`);
|
|
364
|
+
lines.push(`Analyzed: ${e.repo}@${e.analyzedSha.slice(0, 10)} on ${e.analyzedAt.slice(0, 10)}`);
|
|
365
|
+
return lines.join("\n");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/server.ts
|
|
369
|
+
var TOOLS = [
|
|
370
|
+
{
|
|
371
|
+
name: "search_error",
|
|
372
|
+
description: "Look up what a runtime error from an open-source library means and how to fix it. Call this when you encounter an error message from a library. Pass the raw message verbatim; error codes (SCREAMING_SNAKE / E[A-Z]+) are matched exactly first.",
|
|
373
|
+
inputSchema: {
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: {
|
|
376
|
+
message: { type: "string", description: "The raw error message verbatim." },
|
|
377
|
+
repo: { type: "string", description: "Optional 'owner/name' filter." },
|
|
378
|
+
limit: { type: "number", description: "Max results (default 5)." }
|
|
379
|
+
},
|
|
380
|
+
required: ["message"]
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "get_error",
|
|
385
|
+
description: "Fetch the full record for one error by id, or by repo+slug. Returns markdown documentation, solutions, and a defensive pattern ready to apply. Cite the returned URL.",
|
|
386
|
+
inputSchema: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {
|
|
389
|
+
id: { type: "string" },
|
|
390
|
+
repo: { type: "string" },
|
|
391
|
+
slug: { type: "string" }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: "list_repos",
|
|
397
|
+
description: "List the open-source repos covered by the ErrLookup dataset.",
|
|
398
|
+
inputSchema: { type: "object", properties: {} }
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: "refresh_dataset",
|
|
402
|
+
description: "Force a freshness check against the ErrLookup CDN. Returns the current dataset version.",
|
|
403
|
+
inputSchema: { type: "object", properties: {} }
|
|
404
|
+
}
|
|
405
|
+
];
|
|
406
|
+
function createContext() {
|
|
407
|
+
const cfg = defaultCacheConfig();
|
|
408
|
+
const store = new CacheStore(cfg);
|
|
409
|
+
return { store, lastSyncAt: 0, ttlSeconds: cfg.ttlSeconds };
|
|
410
|
+
}
|
|
411
|
+
async function runServer(ctx = createContext()) {
|
|
412
|
+
const server = new Server(
|
|
413
|
+
{ name: "errlookup", version: "0.1.0" },
|
|
414
|
+
{ capabilities: { tools: {} } }
|
|
415
|
+
);
|
|
416
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: TOOLS }));
|
|
417
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
418
|
+
const { name, arguments: args } = req.params;
|
|
419
|
+
try {
|
|
420
|
+
switch (name) {
|
|
421
|
+
case "search_error": {
|
|
422
|
+
const r = await toolSearchError(ctx, args ?? {});
|
|
423
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }] };
|
|
424
|
+
}
|
|
425
|
+
case "get_error": {
|
|
426
|
+
const r = await toolGetError(ctx, args ?? {});
|
|
427
|
+
return { content: [{ type: "text", text: r.markdown }, { type: "text", text: `Source: ${r.url}` }] };
|
|
428
|
+
}
|
|
429
|
+
case "list_repos": {
|
|
430
|
+
const r = await toolListRepos(ctx);
|
|
431
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }] };
|
|
432
|
+
}
|
|
433
|
+
case "refresh_dataset": {
|
|
434
|
+
const r = await toolRefreshDataset(ctx);
|
|
435
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }] };
|
|
436
|
+
}
|
|
437
|
+
default:
|
|
438
|
+
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
|
|
439
|
+
}
|
|
440
|
+
} catch (e) {
|
|
441
|
+
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
const transport = new StdioServerTransport();
|
|
445
|
+
await server.connect(transport);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/index.ts
|
|
449
|
+
runServer().catch((e) => {
|
|
450
|
+
console.error(e.stack ?? e.message);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@standardbeagle/errlookup-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ErrLookup MCP server — answers 'what is this error and how do I fix it' from a cached static dataset (errors.standardbeagle.com).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"errlookup-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:@modelcontextprotocol/sdk --banner:js=\"#!/usr/bin/env node\"",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"clean": "rm -rf dist"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.4"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.10.0",
|
|
22
|
+
"tsx": "^4.19.2",
|
|
23
|
+
"typescript": "^5.7.2",
|
|
24
|
+
"vitest": "^2.1.8",
|
|
25
|
+
"esbuild": "^0.25.0",
|
|
26
|
+
"@errlookup/schema": "workspace:*"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/standardbeagle/err-lookup.git",
|
|
38
|
+
"directory": "packages/mcp"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|