hound-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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/index.js +928 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
|
|
9
|
+
// src/prompts/index.ts
|
|
10
|
+
import { z } from "zod/v4";
|
|
11
|
+
function registerPrompts(server) {
|
|
12
|
+
server.registerPrompt(
|
|
13
|
+
"security_audit",
|
|
14
|
+
{
|
|
15
|
+
description: "Run a full security audit on the current project's dependencies. Scans for vulnerabilities, license issues, and typosquat risks across your entire dependency tree.",
|
|
16
|
+
argsSchema: {
|
|
17
|
+
ecosystem: z.string().optional().describe("Package ecosystem (npm, pypi, cargo, etc). Auto-detected if omitted.")
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
({ ecosystem }) => {
|
|
21
|
+
const ecoNote = ecosystem ? ` The project uses ${ecosystem}.` : "";
|
|
22
|
+
return {
|
|
23
|
+
messages: [
|
|
24
|
+
{
|
|
25
|
+
role: "user",
|
|
26
|
+
content: {
|
|
27
|
+
type: "text",
|
|
28
|
+
text: `Please run a comprehensive security audit on this project's dependencies.${ecoNote}
|
|
29
|
+
|
|
30
|
+
Follow these steps:
|
|
31
|
+
1. Use \`hound_popular\` to check the most commonly used packages in this ecosystem for known vulnerabilities \u2014 this gives a quick baseline.
|
|
32
|
+
2. For any specific packages you can identify in the project, use \`hound_vulns\` to check each one for CVEs and advisories.
|
|
33
|
+
3. Use \`hound_inspect\` on the 3-5 most critical dependencies to check their licenses, OpenSSF Scorecard, and GitHub health.
|
|
34
|
+
4. For any package names that look unusual or unfamiliar, use \`hound_typosquat\` to check for potential typosquatting.
|
|
35
|
+
5. If any vulnerabilities are found, use \`hound_advisories\` to get full details and fix guidance.
|
|
36
|
+
|
|
37
|
+
Summarize findings as:
|
|
38
|
+
- **Critical / High** vulnerabilities that need immediate attention
|
|
39
|
+
- **License risks** (copyleft licenses, unknown licenses)
|
|
40
|
+
- **Health concerns** (abandoned packages, low Scorecard scores)
|
|
41
|
+
- **Recommended actions** with specific version upgrades where available`
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
server.registerPrompt(
|
|
49
|
+
"package_evaluation",
|
|
50
|
+
{
|
|
51
|
+
description: "Evaluate a package before adding it as a dependency. Returns a go/no-go recommendation with security, license, and health analysis.",
|
|
52
|
+
argsSchema: {
|
|
53
|
+
package: z.string().describe("Package name to evaluate (e.g. express, requests, serde)"),
|
|
54
|
+
version: z.string().optional().describe("Specific version to evaluate. Uses latest stable if omitted."),
|
|
55
|
+
ecosystem: z.string().optional().describe("Package ecosystem. Defaults to npm if omitted.")
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
({ package: pkg, version, ecosystem }) => {
|
|
59
|
+
const eco = ecosystem ?? "npm";
|
|
60
|
+
const versionNote = version ? `version ${version} of ` : "";
|
|
61
|
+
return {
|
|
62
|
+
messages: [
|
|
63
|
+
{
|
|
64
|
+
role: "user",
|
|
65
|
+
content: {
|
|
66
|
+
type: "text",
|
|
67
|
+
text: `I'm considering adding ${versionNote}\`${pkg}\` (${eco}) as a dependency. Please evaluate it thoroughly.
|
|
68
|
+
|
|
69
|
+
Steps:
|
|
70
|
+
1. Use \`hound_inspect\` on \`${pkg}\`${version ? `@${version}` : ""} (ecosystem: ${eco}) to get the full health profile \u2014 licenses, vulnerabilities, OpenSSF Scorecard, GitHub stats.
|
|
71
|
+
2. Use \`hound_vulns\` to get the full vulnerability list with fix versions.
|
|
72
|
+
3. Use \`hound_typosquat\` to confirm this is the legitimate package and not a typosquat.
|
|
73
|
+
4. Use \`hound_tree\` to check the transitive dependency count \u2014 packages with hundreds of transitive deps carry more supply chain risk.
|
|
74
|
+
5. If any advisories are listed, use \`hound_advisories\` to get the details.
|
|
75
|
+
|
|
76
|
+
Give me a clear **GO / NO-GO / CONDITIONAL** recommendation with reasoning. If conditional, state exactly what version or conditions would make it acceptable.`
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
server.registerPrompt(
|
|
84
|
+
"pre_release_check",
|
|
85
|
+
{
|
|
86
|
+
description: "Run a pre-release dependency scan before shipping. Checks for vulnerabilities and license issues that could block a release.",
|
|
87
|
+
argsSchema: {
|
|
88
|
+
version: z.string().optional().describe("The version you are about to release, e.g. 1.2.0")
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
({ version }) => {
|
|
92
|
+
const versionNote = version ? ` (version ${version})` : "";
|
|
93
|
+
return {
|
|
94
|
+
messages: [
|
|
95
|
+
{
|
|
96
|
+
role: "user",
|
|
97
|
+
content: {
|
|
98
|
+
type: "text",
|
|
99
|
+
text: `I'm about to release this project${versionNote}. Run a pre-release dependency check.
|
|
100
|
+
|
|
101
|
+
Steps:
|
|
102
|
+
1. Use \`hound_popular\` to scan all key packages in this project's ecosystem for known vulnerabilities.
|
|
103
|
+
2. For any packages identified as critical to this project, use \`hound_vulns\` to check for vulnerabilities.
|
|
104
|
+
3. Use \`hound_inspect\` on the top 5 dependencies by importance to verify licenses are compatible and no advisories are outstanding.
|
|
105
|
+
4. Flag any HIGH or CRITICAL severity vulnerabilities as **release blockers**.
|
|
106
|
+
5. Flag any copyleft licenses (GPL, AGPL, LGPL) that may conflict with the project's MIT license as **license blockers**.
|
|
107
|
+
|
|
108
|
+
Output a release checklist:
|
|
109
|
+
- \u2705 or \u274C Vulnerabilities (CRITICAL/HIGH)
|
|
110
|
+
- \u2705 or \u274C License compatibility
|
|
111
|
+
- \u2705 or \u274C No abandoned dependencies (last published >2 years ago)
|
|
112
|
+
|
|
113
|
+
End with a clear **SAFE TO RELEASE** or **BLOCKED \u2014 fix these issues first** verdict.`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/tools/advisories.ts
|
|
123
|
+
import { z as z2 } from "zod/v4";
|
|
124
|
+
|
|
125
|
+
// src/api/depsdev.ts
|
|
126
|
+
var BASE_URL = "https://api.deps.dev/v3";
|
|
127
|
+
var ECOSYSTEM_MAP = {
|
|
128
|
+
npm: "npm",
|
|
129
|
+
pypi: "pypi",
|
|
130
|
+
go: "go",
|
|
131
|
+
maven: "maven",
|
|
132
|
+
cargo: "cargo",
|
|
133
|
+
nuget: "nuget",
|
|
134
|
+
rubygems: "rubygems"
|
|
135
|
+
};
|
|
136
|
+
async function get(path) {
|
|
137
|
+
const url = `${BASE_URL}${path}`;
|
|
138
|
+
const res = await fetch(url, {
|
|
139
|
+
headers: { "User-Agent": "hound-mcp/0.1.0" }
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
const body = await res.text().catch(() => "");
|
|
143
|
+
throw new DepsDevError(res.status, url, body);
|
|
144
|
+
}
|
|
145
|
+
return res.json();
|
|
146
|
+
}
|
|
147
|
+
var DepsDevError = class extends Error {
|
|
148
|
+
constructor(status, url, body) {
|
|
149
|
+
super(`deps.dev API error ${status} for ${url}`);
|
|
150
|
+
this.status = status;
|
|
151
|
+
this.url = url;
|
|
152
|
+
this.body = body;
|
|
153
|
+
this.name = "DepsDevError";
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
async function getVersion(ecosystem, name, version) {
|
|
157
|
+
const sys = ECOSYSTEM_MAP[ecosystem];
|
|
158
|
+
return get(
|
|
159
|
+
`/systems/${sys}/packages/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
async function getPackage(ecosystem, name) {
|
|
163
|
+
const sys = ECOSYSTEM_MAP[ecosystem];
|
|
164
|
+
return get(`/systems/${sys}/packages/${encodeURIComponent(name)}`);
|
|
165
|
+
}
|
|
166
|
+
async function getDependencies(ecosystem, name, version) {
|
|
167
|
+
const sys = ECOSYSTEM_MAP[ecosystem];
|
|
168
|
+
return get(
|
|
169
|
+
`/systems/${sys}/packages/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}:dependencies`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
async function getProject(projectId) {
|
|
173
|
+
const encoded = projectId.replaceAll("/", "%2F");
|
|
174
|
+
return get(`/projects/${encoded}`);
|
|
175
|
+
}
|
|
176
|
+
async function getAdvisory(advisoryId) {
|
|
177
|
+
return get(`/advisories/${encodeURIComponent(advisoryId)}`);
|
|
178
|
+
}
|
|
179
|
+
function extractProjectId(version) {
|
|
180
|
+
const related = version.relatedProjects ?? [];
|
|
181
|
+
const sourceRepo = related.find(
|
|
182
|
+
(r) => r.relationType === "SOURCE_REPO" || r.relationType === "ISSUE_TRACKER"
|
|
183
|
+
);
|
|
184
|
+
return sourceRepo?.projectKey.id ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/api/osv.ts
|
|
188
|
+
var BASE_URL2 = "https://api.osv.dev/v1";
|
|
189
|
+
var ECOSYSTEM_MAP2 = {
|
|
190
|
+
npm: "npm",
|
|
191
|
+
pypi: "PyPI",
|
|
192
|
+
go: "Go",
|
|
193
|
+
maven: "Maven",
|
|
194
|
+
cargo: "crates.io",
|
|
195
|
+
nuget: "NuGet",
|
|
196
|
+
rubygems: "RubyGems"
|
|
197
|
+
};
|
|
198
|
+
async function post(path, body) {
|
|
199
|
+
const url = `${BASE_URL2}${path}`;
|
|
200
|
+
const res = await fetch(url, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"Content-Type": "application/json",
|
|
204
|
+
"User-Agent": "hound-mcp/0.1.0"
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify(body)
|
|
207
|
+
});
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
const text = await res.text().catch(() => "");
|
|
210
|
+
throw new OsvError(res.status, url, text);
|
|
211
|
+
}
|
|
212
|
+
return res.json();
|
|
213
|
+
}
|
|
214
|
+
var OsvError = class extends Error {
|
|
215
|
+
constructor(status, url, body) {
|
|
216
|
+
super(`OSV API error ${status} for ${url}`);
|
|
217
|
+
this.status = status;
|
|
218
|
+
this.url = url;
|
|
219
|
+
this.body = body;
|
|
220
|
+
this.name = "OsvError";
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
async function queryVulns(ecosystem, name, version) {
|
|
224
|
+
const response = await post("/query", {
|
|
225
|
+
version,
|
|
226
|
+
package: {
|
|
227
|
+
name,
|
|
228
|
+
ecosystem: ECOSYSTEM_MAP2[ecosystem]
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
return response.vulns ?? [];
|
|
232
|
+
}
|
|
233
|
+
async function queryVulnsBatch(packages) {
|
|
234
|
+
if (packages.length === 0) return [];
|
|
235
|
+
const response = await post("/querybatch", {
|
|
236
|
+
queries: packages.map((pkg) => ({
|
|
237
|
+
version: pkg.version,
|
|
238
|
+
package: {
|
|
239
|
+
name: pkg.name,
|
|
240
|
+
ecosystem: ECOSYSTEM_MAP2[pkg.ecosystem]
|
|
241
|
+
}
|
|
242
|
+
}))
|
|
243
|
+
});
|
|
244
|
+
return response.results.map((result) => result.vulns ?? []);
|
|
245
|
+
}
|
|
246
|
+
async function getVuln(vulnId) {
|
|
247
|
+
const url = `${BASE_URL2}/vulns/${encodeURIComponent(vulnId)}`;
|
|
248
|
+
const res = await fetch(url, {
|
|
249
|
+
headers: { "User-Agent": "hound-mcp/0.1.0" }
|
|
250
|
+
});
|
|
251
|
+
if (!res.ok) {
|
|
252
|
+
const text = await res.text().catch(() => "");
|
|
253
|
+
throw new OsvError(res.status, url, text);
|
|
254
|
+
}
|
|
255
|
+
return res.json();
|
|
256
|
+
}
|
|
257
|
+
function extractSeverity(vuln) {
|
|
258
|
+
const dbSeverity = vuln.database_specific?.severity?.toUpperCase();
|
|
259
|
+
if (dbSeverity === "CRITICAL") return "CRITICAL";
|
|
260
|
+
if (dbSeverity === "HIGH") return "HIGH";
|
|
261
|
+
if (dbSeverity === "MODERATE") return "MODERATE";
|
|
262
|
+
if (dbSeverity === "LOW") return "LOW";
|
|
263
|
+
const cvssScore = extractCvssScore(vuln);
|
|
264
|
+
if (cvssScore === null) return "UNKNOWN";
|
|
265
|
+
if (cvssScore >= 9) return "CRITICAL";
|
|
266
|
+
if (cvssScore >= 7) return "HIGH";
|
|
267
|
+
if (cvssScore >= 4) return "MODERATE";
|
|
268
|
+
return "LOW";
|
|
269
|
+
}
|
|
270
|
+
function extractCvssScore(vuln) {
|
|
271
|
+
const entries = vuln.severity ?? [];
|
|
272
|
+
const preferred = ["CVSS_V3", "CVSS_V4", "CVSS_V2"];
|
|
273
|
+
for (const type of preferred) {
|
|
274
|
+
const entry = entries.find((e) => e.type === type);
|
|
275
|
+
if (entry) {
|
|
276
|
+
return parseCvssScore(entry.score);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
function extractFixVersions(vuln, ecosystem) {
|
|
282
|
+
const osvEcosystem = ECOSYSTEM_MAP2[ecosystem];
|
|
283
|
+
const fixed = /* @__PURE__ */ new Set();
|
|
284
|
+
for (const affected of vuln.affected) {
|
|
285
|
+
if (affected.package.ecosystem.toLowerCase() !== osvEcosystem.toLowerCase()) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
for (const range of affected.ranges) {
|
|
289
|
+
for (const event of range.events) {
|
|
290
|
+
if ("fixed" in event && event.fixed) {
|
|
291
|
+
fixed.add(event.fixed);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return [...fixed];
|
|
297
|
+
}
|
|
298
|
+
function parseCvssScore(vector) {
|
|
299
|
+
void vector;
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/tools/advisories.ts
|
|
304
|
+
function register(server) {
|
|
305
|
+
return server.registerTool(
|
|
306
|
+
"hound_advisories",
|
|
307
|
+
{
|
|
308
|
+
description: "Get full details for a security advisory by ID (GHSA, CVE, or OSV ID). Returns title, severity, affected versions, fix versions, and references.",
|
|
309
|
+
inputSchema: {
|
|
310
|
+
id: z2.string().describe("Advisory ID \u2014 e.g. GHSA-rv95-896h-c2vc, CVE-2024-29041, or any OSV ID")
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
async ({ id }) => {
|
|
314
|
+
const [osvResult, depsdevResult] = await Promise.allSettled([getVuln(id), getAdvisory(id)]);
|
|
315
|
+
const lines = [];
|
|
316
|
+
if (osvResult.status === "fulfilled") {
|
|
317
|
+
const vuln = osvResult.value;
|
|
318
|
+
lines.push(`\u{1F514} Advisory: ${vuln.id}`);
|
|
319
|
+
lines.push("\u2550".repeat(50));
|
|
320
|
+
lines.push("");
|
|
321
|
+
lines.push(`\u{1F4CB} Summary: ${vuln.summary}`);
|
|
322
|
+
lines.push(`\u{1F4C5} Published: ${vuln.published.slice(0, 10)}`);
|
|
323
|
+
lines.push(`\u{1F504} Last modified: ${vuln.modified.slice(0, 10)}`);
|
|
324
|
+
if (vuln.aliases && vuln.aliases.length > 0) {
|
|
325
|
+
lines.push(`\u{1F517} Also known as: ${vuln.aliases.join(", ")}`);
|
|
326
|
+
}
|
|
327
|
+
const dbSeverity = vuln.database_specific?.severity;
|
|
328
|
+
if (dbSeverity) {
|
|
329
|
+
lines.push(`\u26A0\uFE0F Severity: ${dbSeverity}`);
|
|
330
|
+
}
|
|
331
|
+
if (vuln.database_specific?.cwe_ids && vuln.database_specific.cwe_ids.length > 0) {
|
|
332
|
+
lines.push(`\u{1F3F7}\uFE0F CWE: ${vuln.database_specific.cwe_ids.join(", ")}`);
|
|
333
|
+
}
|
|
334
|
+
if (vuln.affected.length > 0) {
|
|
335
|
+
lines.push("");
|
|
336
|
+
lines.push("\u{1F4E6} Affected packages");
|
|
337
|
+
lines.push("\u2500".repeat(30));
|
|
338
|
+
for (const affected of vuln.affected) {
|
|
339
|
+
lines.push(` ${affected.package.ecosystem}: ${affected.package.name}`);
|
|
340
|
+
for (const range of affected.ranges) {
|
|
341
|
+
if (range.type === "SEMVER") {
|
|
342
|
+
const introduced = range.events.find((e) => "introduced" in e);
|
|
343
|
+
const fixed = range.events.find((e) => "fixed" in e);
|
|
344
|
+
const introStr = introduced && "introduced" in introduced ? introduced.introduced : "0";
|
|
345
|
+
if (fixed && "fixed" in fixed) {
|
|
346
|
+
lines.push(` Affected: >= ${introStr}, fixed in ${fixed.fixed}`);
|
|
347
|
+
} else {
|
|
348
|
+
lines.push(` Affected: >= ${introStr} (no fix available)`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (vuln.details) {
|
|
355
|
+
lines.push("");
|
|
356
|
+
lines.push("\u{1F4DD} Details");
|
|
357
|
+
lines.push("\u2500".repeat(30));
|
|
358
|
+
const details = vuln.details.length > 500 ? `${vuln.details.slice(0, 500)}...` : vuln.details;
|
|
359
|
+
lines.push(details);
|
|
360
|
+
}
|
|
361
|
+
if (vuln.references && vuln.references.length > 0) {
|
|
362
|
+
lines.push("");
|
|
363
|
+
lines.push("\u{1F517} References");
|
|
364
|
+
lines.push("\u2500".repeat(30));
|
|
365
|
+
for (const ref of vuln.references.slice(0, 5)) {
|
|
366
|
+
lines.push(` ${ref.url}`);
|
|
367
|
+
}
|
|
368
|
+
if (vuln.references.length > 5) {
|
|
369
|
+
lines.push(` ... and ${vuln.references.length - 5} more`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
lines.push("");
|
|
373
|
+
lines.push(`Source: https://osv.dev/vulnerability/${id}`);
|
|
374
|
+
} else if (depsdevResult.status === "fulfilled") {
|
|
375
|
+
const advisory = depsdevResult.value;
|
|
376
|
+
lines.push(`\u{1F514} Advisory: ${advisory.advisoryKey.id}`);
|
|
377
|
+
lines.push("\u2550".repeat(50));
|
|
378
|
+
lines.push("");
|
|
379
|
+
lines.push(`\u{1F4CB} Title: ${advisory.title}`);
|
|
380
|
+
if (advisory.aliases.length > 0) {
|
|
381
|
+
lines.push(`\u{1F517} Also known as: ${advisory.aliases.join(", ")}`);
|
|
382
|
+
}
|
|
383
|
+
lines.push(`\u26A0\uFE0F CVSS v3 Score: ${advisory.cvss3Score}`);
|
|
384
|
+
lines.push(`\u{1F4D0} CVSS Vector: ${advisory.cvss3Vector}`);
|
|
385
|
+
lines.push("");
|
|
386
|
+
lines.push(`Source: ${advisory.url}`);
|
|
387
|
+
} else {
|
|
388
|
+
return {
|
|
389
|
+
content: [
|
|
390
|
+
{
|
|
391
|
+
type: "text",
|
|
392
|
+
text: `Could not find advisory "${id}". Check the ID format (e.g. GHSA-xxxx-xxxx-xxxx or CVE-YYYY-NNNNN).`
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/tools/inspect.ts
|
|
405
|
+
import { z as z3 } from "zod/v4";
|
|
406
|
+
var ECOSYSTEM_VALUES = ["npm", "pypi", "go", "maven", "cargo", "nuget", "rubygems"];
|
|
407
|
+
function register2(server) {
|
|
408
|
+
return server.registerTool(
|
|
409
|
+
"hound_inspect",
|
|
410
|
+
{
|
|
411
|
+
description: "Get a comprehensive profile of a package version: licenses, vulnerabilities, OpenSSF scorecard, GitHub stats, and dependency count \u2014 all in one call.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
name: z3.string().describe("Package name"),
|
|
414
|
+
version: z3.string().describe("Package version"),
|
|
415
|
+
ecosystem: z3.enum(ECOSYSTEM_VALUES).default("npm").describe("Package ecosystem (default: npm)")
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
async ({ name, version, ecosystem }) => {
|
|
419
|
+
const eco = ecosystem;
|
|
420
|
+
const [versionData, vulns] = await Promise.allSettled([
|
|
421
|
+
getVersion(eco, name, version),
|
|
422
|
+
queryVulns(eco, name, version)
|
|
423
|
+
]);
|
|
424
|
+
if (versionData.status === "rejected") {
|
|
425
|
+
return {
|
|
426
|
+
content: [
|
|
427
|
+
{
|
|
428
|
+
type: "text",
|
|
429
|
+
text: `Could not find ${name}@${version} in ${ecosystem}. Check the package name and version.`
|
|
430
|
+
}
|
|
431
|
+
]
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const pkg = versionData.value;
|
|
435
|
+
const vulnList = vulns.status === "fulfilled" ? vulns.value : [];
|
|
436
|
+
const projectId = extractProjectId(pkg);
|
|
437
|
+
const projectData = projectId !== null ? await getProject(projectId).catch(() => null) : null;
|
|
438
|
+
const lines = [`\u{1F4E6} ${name}@${version} (${ecosystem})`, "\u2550".repeat(50), ""];
|
|
439
|
+
const published = pkg.publishedAt.slice(0, 10);
|
|
440
|
+
const daysSince = Math.floor(
|
|
441
|
+
(Date.now() - new Date(pkg.publishedAt).getTime()) / (1e3 * 60 * 60 * 24)
|
|
442
|
+
);
|
|
443
|
+
lines.push(`\u{1F4C5} Published: ${published} (${daysSince} days ago)`);
|
|
444
|
+
const licenses = pkg.licenses && pkg.licenses.length > 0 ? pkg.licenses.join(", ") : "Unknown";
|
|
445
|
+
lines.push(`\u{1F4C4} License: ${licenses}`);
|
|
446
|
+
if (vulnList.length === 0) {
|
|
447
|
+
lines.push(`\u{1F6E1}\uFE0F Vulnerabilities: None known`);
|
|
448
|
+
} else {
|
|
449
|
+
const counts = {};
|
|
450
|
+
for (const v of vulnList) {
|
|
451
|
+
const sev = extractSeverity(v);
|
|
452
|
+
counts[sev] = (counts[sev] ?? 0) + 1;
|
|
453
|
+
}
|
|
454
|
+
const summary = Object.entries(counts).map(([s, n]) => `${n} ${s.toLowerCase()}`).join(", ");
|
|
455
|
+
lines.push(`\u26A0\uFE0F Vulnerabilities: ${vulnList.length} (${summary})`);
|
|
456
|
+
}
|
|
457
|
+
const advisoryCount = pkg.advisoryKeys?.length ?? 0;
|
|
458
|
+
if (advisoryCount > 0) {
|
|
459
|
+
const ids = pkg.advisoryKeys?.map((a) => a.id).join(", ");
|
|
460
|
+
lines.push(`\u{1F514} Advisories: ${ids}`);
|
|
461
|
+
}
|
|
462
|
+
if (projectData !== null) {
|
|
463
|
+
lines.push("");
|
|
464
|
+
lines.push("\u{1F4CA} Project Health");
|
|
465
|
+
lines.push("\u2500".repeat(30));
|
|
466
|
+
lines.push(`\u2B50 Stars: ${projectData.starsCount.toLocaleString()}`);
|
|
467
|
+
lines.push(`\u{1F374} Forks: ${projectData.forksCount.toLocaleString()}`);
|
|
468
|
+
lines.push(`\u{1F41B} Open issues: ${projectData.openIssuesCount.toLocaleString()}`);
|
|
469
|
+
if (projectData.scorecard !== null) {
|
|
470
|
+
const score = projectData.scorecard.overallScore.toFixed(1);
|
|
471
|
+
const grade = scorecardGrade(projectData.scorecard.overallScore);
|
|
472
|
+
lines.push(`\u{1F3C6} OpenSSF Scorecard: ${score}/10 (${grade})`);
|
|
473
|
+
const weak = [...projectData.scorecard.checks].sort((a, b) => a.score - b.score).slice(0, 3);
|
|
474
|
+
if (weak.length > 0) {
|
|
475
|
+
lines.push(
|
|
476
|
+
` Lowest checks: ${weak.map((c) => `${c.name} (${c.score}/10)`).join(", ")}`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const homepage = pkg.links?.find((l) => l.label === "HOMEPAGE");
|
|
482
|
+
const sourceRepo = pkg.links?.find((l) => l.label === "SOURCE_REPO");
|
|
483
|
+
if (homepage ?? sourceRepo) {
|
|
484
|
+
lines.push("");
|
|
485
|
+
if (homepage) lines.push(`\u{1F310} Homepage: ${homepage.url}`);
|
|
486
|
+
if (sourceRepo) lines.push(`\u{1F4BB} Source: ${sourceRepo.url}`);
|
|
487
|
+
}
|
|
488
|
+
lines.push("");
|
|
489
|
+
lines.push(
|
|
490
|
+
`\u{1F517} deps.dev: https://deps.dev/${ecosystem}/packages/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}`
|
|
491
|
+
);
|
|
492
|
+
return {
|
|
493
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
function scorecardGrade(score) {
|
|
499
|
+
if (score >= 9) return "A";
|
|
500
|
+
if (score >= 7) return "B";
|
|
501
|
+
if (score >= 5) return "C";
|
|
502
|
+
if (score >= 3) return "D";
|
|
503
|
+
return "F";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/tools/popular.ts
|
|
507
|
+
import { z as z4 } from "zod/v4";
|
|
508
|
+
var ECOSYSTEM_VALUES2 = ["npm", "pypi", "go", "maven", "cargo", "nuget", "rubygems"];
|
|
509
|
+
var POPULAR_DEFAULTS = {
|
|
510
|
+
npm: [
|
|
511
|
+
"express",
|
|
512
|
+
"lodash",
|
|
513
|
+
"axios",
|
|
514
|
+
"react",
|
|
515
|
+
"typescript",
|
|
516
|
+
"webpack",
|
|
517
|
+
"eslint",
|
|
518
|
+
"jest",
|
|
519
|
+
"next",
|
|
520
|
+
"vue"
|
|
521
|
+
],
|
|
522
|
+
pypi: [
|
|
523
|
+
"requests",
|
|
524
|
+
"numpy",
|
|
525
|
+
"pandas",
|
|
526
|
+
"flask",
|
|
527
|
+
"django",
|
|
528
|
+
"fastapi",
|
|
529
|
+
"sqlalchemy",
|
|
530
|
+
"pytest",
|
|
531
|
+
"boto3",
|
|
532
|
+
"pydantic"
|
|
533
|
+
],
|
|
534
|
+
go: [
|
|
535
|
+
"github.com/gin-gonic/gin",
|
|
536
|
+
"github.com/gorilla/mux",
|
|
537
|
+
"github.com/stretchr/testify",
|
|
538
|
+
"github.com/spf13/cobra",
|
|
539
|
+
"go.uber.org/zap"
|
|
540
|
+
],
|
|
541
|
+
maven: [
|
|
542
|
+
"com.google.guava:guava",
|
|
543
|
+
"org.springframework:spring-core",
|
|
544
|
+
"org.apache.commons:commons-lang3",
|
|
545
|
+
"com.fasterxml.jackson.core:jackson-databind",
|
|
546
|
+
"org.slf4j:slf4j-api"
|
|
547
|
+
],
|
|
548
|
+
cargo: ["serde", "tokio", "reqwest", "clap", "anyhow", "log", "rand", "regex"],
|
|
549
|
+
nuget: [
|
|
550
|
+
"Newtonsoft.Json",
|
|
551
|
+
"Microsoft.Extensions.Logging",
|
|
552
|
+
"Serilog",
|
|
553
|
+
"AutoMapper",
|
|
554
|
+
"FluentValidation"
|
|
555
|
+
],
|
|
556
|
+
rubygems: ["rails", "devise", "rspec-core", "activerecord", "sidekiq"]
|
|
557
|
+
};
|
|
558
|
+
function register3(server) {
|
|
559
|
+
return server.registerTool(
|
|
560
|
+
"hound_popular",
|
|
561
|
+
{
|
|
562
|
+
description: "Scan a list of popular (or user-specified) packages for known vulnerabilities. Quickly surface which widely-used packages in an ecosystem have open security issues.",
|
|
563
|
+
inputSchema: {
|
|
564
|
+
ecosystem: z4.enum(ECOSYSTEM_VALUES2).default("npm").describe("Package ecosystem (default: npm)"),
|
|
565
|
+
packages: z4.array(z4.string()).optional().describe(
|
|
566
|
+
"Specific package names to check. If omitted, uses a curated list of popular packages for the ecosystem."
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
async ({ ecosystem, packages }) => {
|
|
571
|
+
const eco = ecosystem;
|
|
572
|
+
const names = packages ?? POPULAR_DEFAULTS[ecosystem] ?? [];
|
|
573
|
+
if (names.length === 0) {
|
|
574
|
+
return {
|
|
575
|
+
content: [
|
|
576
|
+
{
|
|
577
|
+
type: "text",
|
|
578
|
+
text: `No packages specified and no defaults available for ${ecosystem}.`
|
|
579
|
+
}
|
|
580
|
+
]
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const packageResults = await Promise.allSettled(
|
|
584
|
+
names.map(async (name) => {
|
|
585
|
+
const pkg = await getPackage(eco, name);
|
|
586
|
+
const defaultVersion = pkg.versions.find((v) => v.isDefault) ?? pkg.versions[0];
|
|
587
|
+
return {
|
|
588
|
+
name,
|
|
589
|
+
version: defaultVersion?.versionKey.version ?? "unknown"
|
|
590
|
+
};
|
|
591
|
+
})
|
|
592
|
+
);
|
|
593
|
+
const resolved = packageResults.map((r, i) => ({
|
|
594
|
+
name: names[i] ?? "",
|
|
595
|
+
version: r.status === "fulfilled" ? r.value.version : null
|
|
596
|
+
})).filter((p) => p.version !== null);
|
|
597
|
+
const failed = packageResults.filter((r) => r.status === "rejected").length;
|
|
598
|
+
const vulnResults = await queryVulnsBatch(
|
|
599
|
+
resolved.map((p) => ({ ecosystem: eco, name: p.name, version: p.version }))
|
|
600
|
+
);
|
|
601
|
+
const lines = [
|
|
602
|
+
`\u{1F50D} Vulnerability scan: ${ecosystem} popular packages`,
|
|
603
|
+
"\u2550".repeat(50),
|
|
604
|
+
`Checked ${resolved.length} packages`,
|
|
605
|
+
""
|
|
606
|
+
];
|
|
607
|
+
const withVulns = resolved.map((pkg, i) => ({ ...pkg, vulns: vulnResults[i] ?? [] })).sort((a, b) => b.vulns.length - a.vulns.length);
|
|
608
|
+
const vulnerableCount = withVulns.filter((p) => p.vulns.length > 0).length;
|
|
609
|
+
if (vulnerableCount === 0) {
|
|
610
|
+
lines.push(`\u2705 All ${resolved.length} packages are clean \u2014 no known vulnerabilities.`);
|
|
611
|
+
lines.push("");
|
|
612
|
+
} else {
|
|
613
|
+
lines.push(`\u26A0\uFE0F ${vulnerableCount} of ${resolved.length} packages have vulnerabilities:`);
|
|
614
|
+
lines.push("");
|
|
615
|
+
}
|
|
616
|
+
for (const pkg of withVulns) {
|
|
617
|
+
if (pkg.vulns.length === 0) {
|
|
618
|
+
lines.push(` \u2705 ${pkg.name}@${pkg.version}`);
|
|
619
|
+
} else {
|
|
620
|
+
lines.push(` \u26A0\uFE0F ${pkg.name}@${pkg.version} \u2014 ${pkg.vulns.length} vuln(s)`);
|
|
621
|
+
for (const v of pkg.vulns.slice(0, 3)) {
|
|
622
|
+
lines.push(` ${v.id}: ${v.summary}`);
|
|
623
|
+
}
|
|
624
|
+
if (pkg.vulns.length > 3) {
|
|
625
|
+
lines.push(` ... and ${pkg.vulns.length - 3} more`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (failed > 0) {
|
|
630
|
+
lines.push("");
|
|
631
|
+
lines.push(`\u2139\uFE0F ${failed} package(s) could not be resolved in ${ecosystem}.`);
|
|
632
|
+
}
|
|
633
|
+
lines.push("");
|
|
634
|
+
lines.push("Source: OSV.dev + deps.dev");
|
|
635
|
+
return {
|
|
636
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/tools/tree.ts
|
|
643
|
+
import { z as z5 } from "zod/v4";
|
|
644
|
+
var ECOSYSTEM_VALUES3 = ["npm", "pypi", "go", "maven", "cargo", "nuget", "rubygems"];
|
|
645
|
+
function register4(server) {
|
|
646
|
+
return server.registerTool(
|
|
647
|
+
"hound_tree",
|
|
648
|
+
{
|
|
649
|
+
description: "Show the full resolved dependency tree for a package version, including all transitive dependencies with their depth and relation type.",
|
|
650
|
+
inputSchema: {
|
|
651
|
+
name: z5.string().describe("Package name"),
|
|
652
|
+
version: z5.string().describe("Package version"),
|
|
653
|
+
ecosystem: z5.enum(ECOSYSTEM_VALUES3).default("npm").describe("Package ecosystem (default: npm)"),
|
|
654
|
+
maxDepth: z5.number().int().min(1).max(10).default(3).describe("Maximum depth to display (default: 3, max: 10)")
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
async ({ name, version, ecosystem, maxDepth }) => {
|
|
658
|
+
let deps;
|
|
659
|
+
try {
|
|
660
|
+
deps = await getDependencies(ecosystem, name, version);
|
|
661
|
+
} catch {
|
|
662
|
+
return {
|
|
663
|
+
content: [
|
|
664
|
+
{
|
|
665
|
+
type: "text",
|
|
666
|
+
text: `Could not fetch dependency tree for ${name}@${version}. Check the package name and version.`
|
|
667
|
+
}
|
|
668
|
+
]
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
const nodes = deps.nodes;
|
|
672
|
+
const edges = deps.edges;
|
|
673
|
+
const directCount = nodes.filter((n) => n.relationType === "DIRECT").length;
|
|
674
|
+
const indirectCount = nodes.filter((n) => n.relationType === "INDIRECT").length;
|
|
675
|
+
const totalCount = directCount + indirectCount;
|
|
676
|
+
const children = /* @__PURE__ */ new Map();
|
|
677
|
+
for (const edge of edges) {
|
|
678
|
+
if (!children.has(edge.fromNode)) children.set(edge.fromNode, []);
|
|
679
|
+
children.get(edge.fromNode)?.push(edge.toNode);
|
|
680
|
+
}
|
|
681
|
+
const lines = [
|
|
682
|
+
`\u{1F333} Dependency tree for ${name}@${version} (${ecosystem})`,
|
|
683
|
+
"\u2550".repeat(50),
|
|
684
|
+
`${totalCount} total dependencies (${directCount} direct, ${indirectCount} transitive)`,
|
|
685
|
+
""
|
|
686
|
+
];
|
|
687
|
+
const rootNode = nodes[0];
|
|
688
|
+
if (rootNode) {
|
|
689
|
+
renderNode(lines, nodes, children, 0, 0, maxDepth, /* @__PURE__ */ new Set());
|
|
690
|
+
}
|
|
691
|
+
if (maxDepth < 10 && indirectCount > 0) {
|
|
692
|
+
lines.push("");
|
|
693
|
+
lines.push(`\u2139\uFE0F Showing up to depth ${maxDepth}. Use maxDepth to see deeper.`);
|
|
694
|
+
}
|
|
695
|
+
lines.push("");
|
|
696
|
+
lines.push(`Source: deps.dev`);
|
|
697
|
+
return {
|
|
698
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
function renderNode(lines, nodes, children, nodeIndex, depth, maxDepth, visited) {
|
|
704
|
+
if (visited.has(nodeIndex)) return;
|
|
705
|
+
visited.add(nodeIndex);
|
|
706
|
+
const node = nodes[nodeIndex];
|
|
707
|
+
if (!node) return;
|
|
708
|
+
const indent = " ".repeat(depth);
|
|
709
|
+
const isRoot = depth === 0;
|
|
710
|
+
const prefix = isRoot ? "" : "\u251C\u2500\u2500 ";
|
|
711
|
+
const errorSuffix = node.errors.length > 0 ? " \u26A0\uFE0F" : "";
|
|
712
|
+
lines.push(`${indent}${prefix}${node.key.name}@${node.key.version}${errorSuffix}`);
|
|
713
|
+
if (depth >= maxDepth) {
|
|
714
|
+
const childCount = children.get(nodeIndex)?.length ?? 0;
|
|
715
|
+
if (childCount > 0) {
|
|
716
|
+
lines.push(`${indent} \u2514\u2500\u2500 ... (${childCount} more)`);
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
for (const childIdx of children.get(nodeIndex) ?? []) {
|
|
721
|
+
renderNode(lines, nodes, children, childIdx, depth + 1, maxDepth, visited);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/tools/typosquat.ts
|
|
726
|
+
import { z as z6 } from "zod/v4";
|
|
727
|
+
var ECOSYSTEM_VALUES4 = ["npm", "pypi", "go", "maven", "cargo", "nuget", "rubygems"];
|
|
728
|
+
function generateTypos(name) {
|
|
729
|
+
const variants = /* @__PURE__ */ new Set();
|
|
730
|
+
for (let i = 0; i < name.length; i++) {
|
|
731
|
+
variants.add(name.slice(0, i) + name.slice(i + 1));
|
|
732
|
+
}
|
|
733
|
+
for (let i = 0; i < name.length - 1; i++) {
|
|
734
|
+
const a = name.charAt(i);
|
|
735
|
+
const b = name.charAt(i + 1);
|
|
736
|
+
variants.add(name.slice(0, i) + b + a + name.slice(i + 2));
|
|
737
|
+
}
|
|
738
|
+
if (name.includes("-")) {
|
|
739
|
+
variants.add(name.replaceAll("-", "_"));
|
|
740
|
+
variants.add(name.replaceAll("-", ""));
|
|
741
|
+
}
|
|
742
|
+
if (name.includes("_")) {
|
|
743
|
+
variants.add(name.replaceAll("_", "-"));
|
|
744
|
+
variants.add(name.replaceAll("_", ""));
|
|
745
|
+
}
|
|
746
|
+
variants.add(`node-${name}`);
|
|
747
|
+
variants.add(`${name}-js`);
|
|
748
|
+
variants.add(`${name}js`);
|
|
749
|
+
variants.add(`${name}-node`);
|
|
750
|
+
variants.delete(name);
|
|
751
|
+
return [...variants];
|
|
752
|
+
}
|
|
753
|
+
function register5(server) {
|
|
754
|
+
return server.registerTool(
|
|
755
|
+
"hound_typosquat",
|
|
756
|
+
{
|
|
757
|
+
description: "Check if a package name looks like a typosquat of a popular package. Generates likely typo variants and checks which ones exist in the registry.",
|
|
758
|
+
inputSchema: {
|
|
759
|
+
name: z6.string().describe("Package name to check"),
|
|
760
|
+
ecosystem: z6.enum(ECOSYSTEM_VALUES4).default("npm").describe("Package ecosystem (default: npm)")
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
async ({ name, ecosystem }) => {
|
|
764
|
+
const eco = ecosystem;
|
|
765
|
+
const variants = generateTypos(name);
|
|
766
|
+
const results = await Promise.allSettled(
|
|
767
|
+
variants.map(async (variant) => {
|
|
768
|
+
const pkg = await getPackage(eco, variant);
|
|
769
|
+
return { name: variant, versionCount: pkg.versions.length };
|
|
770
|
+
})
|
|
771
|
+
);
|
|
772
|
+
const existing = results.filter((r) => r.status === "fulfilled").map((r) => r.value).sort((a, b) => b.versionCount - a.versionCount);
|
|
773
|
+
const targetExists = await getPackage(eco, name).then((p) => p.versions.length).catch(() => null);
|
|
774
|
+
const lines = [`\u{1F50D} Typosquat check: ${name} (${ecosystem})`, "\u2550".repeat(50), ""];
|
|
775
|
+
if (targetExists === null) {
|
|
776
|
+
lines.push(`\u26A0\uFE0F "${name}" does not exist in ${ecosystem}.`);
|
|
777
|
+
lines.push(`This package name itself may be available \u2014 or it could be a typo.`);
|
|
778
|
+
} else {
|
|
779
|
+
lines.push(`\u2705 "${name}" exists (${targetExists} published versions)`);
|
|
780
|
+
}
|
|
781
|
+
lines.push("");
|
|
782
|
+
if (existing.length === 0) {
|
|
783
|
+
lines.push(`\u2705 No typosquat variants found in ${ecosystem}.`);
|
|
784
|
+
lines.push(`None of the ${variants.length} generated variants exist.`);
|
|
785
|
+
} else {
|
|
786
|
+
lines.push(`\u26A0\uFE0F Found ${existing.length} similar package(s) that exist in ${ecosystem}:`);
|
|
787
|
+
lines.push("");
|
|
788
|
+
for (const pkg of existing) {
|
|
789
|
+
lines.push(` \u{1F4E6} ${pkg.name} (${pkg.versionCount} versions)`);
|
|
790
|
+
}
|
|
791
|
+
lines.push("");
|
|
792
|
+
lines.push(`If you meant to install "${name}", double-check you typed it correctly.`);
|
|
793
|
+
lines.push(`If you are the author of "${name}", consider reserving these names.`);
|
|
794
|
+
}
|
|
795
|
+
lines.push("");
|
|
796
|
+
lines.push(
|
|
797
|
+
`\u2139\uFE0F Checked ${variants.length} typo variants (omission, transposition, hyphen, affixes)`
|
|
798
|
+
);
|
|
799
|
+
return {
|
|
800
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/tools/vulns.ts
|
|
807
|
+
import { z as z7 } from "zod/v4";
|
|
808
|
+
var ECOSYSTEM_VALUES5 = ["npm", "pypi", "go", "maven", "cargo", "nuget", "rubygems"];
|
|
809
|
+
var SEVERITY_ICON = {
|
|
810
|
+
CRITICAL: "\u{1F534}",
|
|
811
|
+
HIGH: "\u{1F7E0}",
|
|
812
|
+
MODERATE: "\u{1F7E1}",
|
|
813
|
+
LOW: "\u{1F535}",
|
|
814
|
+
UNKNOWN: "\u26AA"
|
|
815
|
+
};
|
|
816
|
+
function register6(server) {
|
|
817
|
+
return server.registerTool(
|
|
818
|
+
"hound_vulns",
|
|
819
|
+
{
|
|
820
|
+
description: "List all known vulnerabilities for a specific package version, grouped by severity with fix versions and advisory links.",
|
|
821
|
+
inputSchema: {
|
|
822
|
+
name: z7.string().describe("Package name (e.g. express, lodash)"),
|
|
823
|
+
version: z7.string().describe("Package version (e.g. 4.18.2)"),
|
|
824
|
+
ecosystem: z7.enum(ECOSYSTEM_VALUES5).default("npm").describe("Package ecosystem (default: npm)")
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
async ({ name, version, ecosystem }) => {
|
|
828
|
+
let vulns;
|
|
829
|
+
try {
|
|
830
|
+
vulns = await queryVulns(ecosystem, name, version);
|
|
831
|
+
} catch {
|
|
832
|
+
return {
|
|
833
|
+
content: [
|
|
834
|
+
{
|
|
835
|
+
type: "text",
|
|
836
|
+
text: `Failed to query vulnerabilities for ${name}@${version}. The OSV API may be temporarily unavailable.`
|
|
837
|
+
}
|
|
838
|
+
]
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
if (vulns.length === 0) {
|
|
842
|
+
return {
|
|
843
|
+
content: [
|
|
844
|
+
{
|
|
845
|
+
type: "text",
|
|
846
|
+
text: `\u2705 No known vulnerabilities found for ${name}@${version} (${ecosystem}).
|
|
847
|
+
|
|
848
|
+
This checks the OSV database which covers GitHub Advisory Database, NVD, and more.`
|
|
849
|
+
}
|
|
850
|
+
]
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
const bySeverity = {
|
|
854
|
+
CRITICAL: [],
|
|
855
|
+
HIGH: [],
|
|
856
|
+
MODERATE: [],
|
|
857
|
+
LOW: [],
|
|
858
|
+
UNKNOWN: []
|
|
859
|
+
};
|
|
860
|
+
for (const vuln of vulns) {
|
|
861
|
+
const sev = extractSeverity(vuln);
|
|
862
|
+
bySeverity[sev]?.push(vuln);
|
|
863
|
+
}
|
|
864
|
+
const lines = [
|
|
865
|
+
`\u{1F50D} Vulnerabilities in ${name}@${version} (${ecosystem})`,
|
|
866
|
+
"\u2500".repeat(50),
|
|
867
|
+
`Found ${vulns.length} vulnerability${vulns.length === 1 ? "" : "ies"}`,
|
|
868
|
+
""
|
|
869
|
+
];
|
|
870
|
+
for (const severity of ["CRITICAL", "HIGH", "MODERATE", "LOW", "UNKNOWN"]) {
|
|
871
|
+
const group = bySeverity[severity] ?? [];
|
|
872
|
+
if (group.length === 0) continue;
|
|
873
|
+
const icon = SEVERITY_ICON[severity] ?? "\u26AA";
|
|
874
|
+
lines.push(`${icon} ${severity} (${group.length})`);
|
|
875
|
+
lines.push("\u2500".repeat(30));
|
|
876
|
+
for (const vuln of group) {
|
|
877
|
+
lines.push(` ${vuln.id}`);
|
|
878
|
+
lines.push(` ${vuln.summary}`);
|
|
879
|
+
const fixes = extractFixVersions(vuln, ecosystem);
|
|
880
|
+
if (fixes.length > 0) {
|
|
881
|
+
lines.push(` Fix: upgrade to ${fixes.join(" or ")}`);
|
|
882
|
+
} else {
|
|
883
|
+
lines.push(` Fix: no patched version available`);
|
|
884
|
+
}
|
|
885
|
+
if (vuln.aliases && vuln.aliases.length > 0) {
|
|
886
|
+
lines.push(` Also known as: ${vuln.aliases.join(", ")}`);
|
|
887
|
+
}
|
|
888
|
+
lines.push(` Published: ${vuln.published.slice(0, 10)}`);
|
|
889
|
+
lines.push("");
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
lines.push(`Source: https://osv.dev`);
|
|
893
|
+
return {
|
|
894
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/server.ts
|
|
901
|
+
var SERVER_NAME = "hound-mcp";
|
|
902
|
+
var SERVER_VERSION = "0.1.0";
|
|
903
|
+
function createServer() {
|
|
904
|
+
const server = new McpServer({
|
|
905
|
+
name: SERVER_NAME,
|
|
906
|
+
version: SERVER_VERSION
|
|
907
|
+
});
|
|
908
|
+
register6(server);
|
|
909
|
+
register2(server);
|
|
910
|
+
register4(server);
|
|
911
|
+
register5(server);
|
|
912
|
+
register(server);
|
|
913
|
+
register3(server);
|
|
914
|
+
registerPrompts(server);
|
|
915
|
+
return server;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/index.ts
|
|
919
|
+
async function main() {
|
|
920
|
+
const server = createServer();
|
|
921
|
+
const transport = new StdioServerTransport();
|
|
922
|
+
await server.connect(transport);
|
|
923
|
+
}
|
|
924
|
+
main().catch((err) => {
|
|
925
|
+
console.error("Fatal error:", err);
|
|
926
|
+
process.exit(1);
|
|
927
|
+
});
|
|
928
|
+
//# sourceMappingURL=index.js.map
|