dep-oracle 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-VHQCTVCZ.js +6443 -0
- package/dist/chunk-VHQCTVCZ.js.map +1 -0
- package/dist/cli/index.js +77 -6488
- package/dist/cli/index.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/server-TKLM7YIF.js +608 -0
- package/dist/server-TKLM7YIF.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BlastRadiusCalculator,
|
|
4
|
+
CacheManager,
|
|
5
|
+
CollectorOrchestrator,
|
|
6
|
+
MigrationAdvisor,
|
|
7
|
+
NpmParser,
|
|
8
|
+
PythonParser,
|
|
9
|
+
TrustScoreEngine,
|
|
10
|
+
TyposquatDetector,
|
|
11
|
+
ZombieDetector
|
|
12
|
+
} from "./chunk-VHQCTVCZ.js";
|
|
13
|
+
|
|
14
|
+
// src/mcp/server.ts
|
|
15
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { readFileSync } from "fs";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
import { dirname, join as join2 } from "path";
|
|
20
|
+
|
|
21
|
+
// src/mcp/tools.ts
|
|
22
|
+
import { resolve } from "path";
|
|
23
|
+
import {
|
|
24
|
+
ListToolsRequestSchema,
|
|
25
|
+
CallToolRequestSchema
|
|
26
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
27
|
+
|
|
28
|
+
// src/utils/graph.ts
|
|
29
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
30
|
+
import { join, extname } from "path";
|
|
31
|
+
var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
32
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
33
|
+
"node_modules",
|
|
34
|
+
".git",
|
|
35
|
+
"dist",
|
|
36
|
+
"build",
|
|
37
|
+
"out",
|
|
38
|
+
".next",
|
|
39
|
+
".nuxt",
|
|
40
|
+
"coverage",
|
|
41
|
+
"__pycache__",
|
|
42
|
+
".venv",
|
|
43
|
+
"venv"
|
|
44
|
+
]);
|
|
45
|
+
var IMPORT_PATTERNS = [
|
|
46
|
+
// ESM: import ... from "pkg" / export ... from "pkg"
|
|
47
|
+
/(?:import|export)\s+.*?\s+from\s+["']([^"']+)["']/g,
|
|
48
|
+
// ESM: import "pkg" (side-effect import)
|
|
49
|
+
/import\s+["']([^"']+)["']/g,
|
|
50
|
+
// CJS: require("pkg")
|
|
51
|
+
/require\s*\(\s*["']([^"']+)["']\s*\)/g,
|
|
52
|
+
// Dynamic: import("pkg")
|
|
53
|
+
/import\s*\(\s*["']([^"']+)["']\s*\)/g
|
|
54
|
+
];
|
|
55
|
+
function extractPackageName(specifier) {
|
|
56
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) return null;
|
|
57
|
+
if (specifier.startsWith("node:")) return null;
|
|
58
|
+
if (specifier.startsWith("@")) {
|
|
59
|
+
const parts = specifier.split("/");
|
|
60
|
+
if (parts.length < 2) return null;
|
|
61
|
+
return `${parts[0]}/${parts[1]}`;
|
|
62
|
+
}
|
|
63
|
+
return specifier.split("/")[0];
|
|
64
|
+
}
|
|
65
|
+
function extractImports(content) {
|
|
66
|
+
const packages = /* @__PURE__ */ new Set();
|
|
67
|
+
for (const pattern of IMPORT_PATTERNS) {
|
|
68
|
+
pattern.lastIndex = 0;
|
|
69
|
+
let match;
|
|
70
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
71
|
+
const specifier = match[1];
|
|
72
|
+
const pkg = extractPackageName(specifier);
|
|
73
|
+
if (pkg) {
|
|
74
|
+
packages.add(pkg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return packages;
|
|
79
|
+
}
|
|
80
|
+
async function collectSourceFiles(dir) {
|
|
81
|
+
const files = [];
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = await readdir(dir);
|
|
85
|
+
} catch {
|
|
86
|
+
return files;
|
|
87
|
+
}
|
|
88
|
+
const tasks = [];
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (IGNORED_DIRS.has(entry)) continue;
|
|
91
|
+
const fullPath = join(dir, entry);
|
|
92
|
+
tasks.push(
|
|
93
|
+
(async () => {
|
|
94
|
+
try {
|
|
95
|
+
const info = await stat(fullPath);
|
|
96
|
+
if (info.isDirectory()) {
|
|
97
|
+
const nested = await collectSourceFiles(fullPath);
|
|
98
|
+
files.push(...nested);
|
|
99
|
+
} else if (info.isFile() && SCANNABLE_EXTENSIONS.has(extname(entry))) {
|
|
100
|
+
files.push(fullPath);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
})()
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
await Promise.all(tasks);
|
|
108
|
+
return files;
|
|
109
|
+
}
|
|
110
|
+
async function buildImportGraph(dir) {
|
|
111
|
+
const graph = /* @__PURE__ */ new Map();
|
|
112
|
+
const sourceFiles = await collectSourceFiles(dir);
|
|
113
|
+
const results = await Promise.all(
|
|
114
|
+
sourceFiles.map(async (filePath) => {
|
|
115
|
+
try {
|
|
116
|
+
const content = await readFile(filePath, "utf-8");
|
|
117
|
+
const packages = extractImports(content);
|
|
118
|
+
return { filePath, packages };
|
|
119
|
+
} catch {
|
|
120
|
+
return { filePath, packages: /* @__PURE__ */ new Set() };
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
for (const { filePath, packages } of results) {
|
|
125
|
+
for (const pkg of packages) {
|
|
126
|
+
let fileSet = graph.get(pkg);
|
|
127
|
+
if (!fileSet) {
|
|
128
|
+
fileSet = /* @__PURE__ */ new Set();
|
|
129
|
+
graph.set(pkg, fileSet);
|
|
130
|
+
}
|
|
131
|
+
fileSet.add(filePath);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return graph;
|
|
135
|
+
}
|
|
136
|
+
function getBlastRadius(packageName, graph) {
|
|
137
|
+
const files = graph.get(packageName);
|
|
138
|
+
return files ? files.size : 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/reporters/json.ts
|
|
142
|
+
var JsonReporter = class {
|
|
143
|
+
/**
|
|
144
|
+
* Convert the scan result into a pretty-printed JSON string.
|
|
145
|
+
*
|
|
146
|
+
* Map objects (e.g. DependencyTree.nodes) are converted to plain
|
|
147
|
+
* objects so that they survive JSON serialization.
|
|
148
|
+
*/
|
|
149
|
+
report(result) {
|
|
150
|
+
return JSON.stringify(this.toSerializable(result), null, 2);
|
|
151
|
+
}
|
|
152
|
+
// -------------------------------------------------------------------------
|
|
153
|
+
// Internals
|
|
154
|
+
// -------------------------------------------------------------------------
|
|
155
|
+
/**
|
|
156
|
+
* Walk the value recursively and convert Map instances into plain objects
|
|
157
|
+
* so JSON.stringify can handle them.
|
|
158
|
+
*/
|
|
159
|
+
toSerializable(value) {
|
|
160
|
+
if (value === null || value === void 0) {
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
if (value instanceof Map) {
|
|
164
|
+
const obj = {};
|
|
165
|
+
for (const [k, v] of value.entries()) {
|
|
166
|
+
obj[String(k)] = this.toSerializable(v);
|
|
167
|
+
}
|
|
168
|
+
return obj;
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(value)) {
|
|
171
|
+
return value.map((item) => this.toSerializable(item));
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === "object") {
|
|
174
|
+
const result = {};
|
|
175
|
+
for (const [k, v] of Object.entries(value)) {
|
|
176
|
+
result[k] = this.toSerializable(v);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/mcp/tools.ts
|
|
185
|
+
var VALID_PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9._-~]*\/)?[a-z0-9-~][a-z0-9._-~]*$/i;
|
|
186
|
+
function validateDir(input) {
|
|
187
|
+
const dir = resolve(String(input ?? process.cwd()));
|
|
188
|
+
const normalized = dir.replace(/\\/g, "/");
|
|
189
|
+
if (normalized.includes("/../") || normalized.endsWith("/..")) {
|
|
190
|
+
throw new Error("Path traversal not allowed");
|
|
191
|
+
}
|
|
192
|
+
return dir;
|
|
193
|
+
}
|
|
194
|
+
function validateOutputPath(output, baseDir) {
|
|
195
|
+
const resolved = resolve(output);
|
|
196
|
+
const base = resolve(baseDir);
|
|
197
|
+
if (!resolved.startsWith(base)) {
|
|
198
|
+
throw new Error("Output path must be within the project directory");
|
|
199
|
+
}
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
function validatePackageName(name) {
|
|
203
|
+
const trimmed = name.trim();
|
|
204
|
+
if (!trimmed) throw new Error("Package name is required");
|
|
205
|
+
if (trimmed.length > 214) throw new Error("Package name too long");
|
|
206
|
+
if (!VALID_PACKAGE_NAME.test(trimmed)) {
|
|
207
|
+
throw new Error(`Invalid package name: "${trimmed}"`);
|
|
208
|
+
}
|
|
209
|
+
return trimmed;
|
|
210
|
+
}
|
|
211
|
+
var cache = new CacheManager();
|
|
212
|
+
var orchestrator = new CollectorOrchestrator(cache, {
|
|
213
|
+
githubToken: process.env.GITHUB_TOKEN
|
|
214
|
+
});
|
|
215
|
+
var trustEngine = new TrustScoreEngine();
|
|
216
|
+
var zombieDetector = new ZombieDetector();
|
|
217
|
+
var blastRadiusCalc = new BlastRadiusCalculator();
|
|
218
|
+
var migrationAdvisor = new MigrationAdvisor();
|
|
219
|
+
var typosquatDetector = new TyposquatDetector();
|
|
220
|
+
var jsonReporter = new JsonReporter();
|
|
221
|
+
var parsers = [new NpmParser(), new PythonParser()];
|
|
222
|
+
var TOOLS = [
|
|
223
|
+
{
|
|
224
|
+
name: "dep_oracle_scan",
|
|
225
|
+
description: "Perform a full dependency security scan on a project directory. Parses the manifest (package.json, requirements.txt, etc.), collects data from registries and GitHub, computes trust scores for every dependency, detects zombies, typosquats, and calculates blast radius. Returns a complete ScanResult JSON with per-package trust reports and an overall project score.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
dir: {
|
|
230
|
+
type: "string",
|
|
231
|
+
description: "Absolute path to the project directory to scan. Defaults to the current working directory if omitted."
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
required: []
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: "dep_oracle_trust_score",
|
|
239
|
+
description: "Calculate the trust score for a single npm package. Queries the npm registry, GitHub, OSV vulnerability database, and other sources to produce a weighted score (0-100) across 6 dimensions: security, maintainer, activity, popularity, funding, and license. Returns a TrustReport JSON with the overall score, per-dimension metrics, zombie status, and alternative suggestions.",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
package: {
|
|
244
|
+
type: "string",
|
|
245
|
+
description: 'npm package name (e.g. "express", "@scope/pkg").'
|
|
246
|
+
},
|
|
247
|
+
version: {
|
|
248
|
+
type: "string",
|
|
249
|
+
description: 'Specific version to analyze (e.g. "4.18.2"). If omitted, the latest version is used.'
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
required: ["package"]
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "dep_oracle_blast_radius",
|
|
257
|
+
description: "Analyze the blast radius (import impact) of a package within a project. Scans all JS/TS source files to find how many files import the given package. Returns the count of affected files, their paths, and the percentage of the codebase impacted. Useful for understanding the risk if a dependency is compromised or needs replacement.",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
type: "object",
|
|
260
|
+
properties: {
|
|
261
|
+
package: {
|
|
262
|
+
type: "string",
|
|
263
|
+
description: "Package name to check import usage for."
|
|
264
|
+
},
|
|
265
|
+
dir: {
|
|
266
|
+
type: "string",
|
|
267
|
+
description: "Absolute path to the project directory. Defaults to cwd if omitted."
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
required: ["package"]
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: "dep_oracle_zombies",
|
|
275
|
+
description: "Detect zombie (abandoned/unmaintained) dependencies in a project. Parses the manifest, queries registry and GitHub for each dependency, and flags packages that show signs of abandonment: deprecated, no commits in 12+ months, no active maintainers, etc. Returns an array of zombie packages with severity levels and reasons.",
|
|
276
|
+
inputSchema: {
|
|
277
|
+
type: "object",
|
|
278
|
+
properties: {
|
|
279
|
+
dir: {
|
|
280
|
+
type: "string",
|
|
281
|
+
description: "Absolute path to the project directory. Defaults to cwd if omitted."
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
required: []
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "dep_oracle_suggest_migration",
|
|
289
|
+
description: "Get migration suggestions for a given package. Looks up the package in a curated knowledge base of common replacements and returns alternatives with descriptions and difficulty ratings. Useful for replacing deprecated, abandoned, or low-trust packages (e.g. moment -> dayjs, request -> got, lodash -> es-toolkit).",
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
package: {
|
|
294
|
+
type: "string",
|
|
295
|
+
description: "Package name to find migration alternatives for."
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
required: ["package"]
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "dep_oracle_typosquat_check",
|
|
303
|
+
description: "Check whether a package name is a potential typosquat of a popular package. Uses Levenshtein distance, homoglyph detection, and pattern analysis to identify suspicious package names that closely resemble well-known packages. Returns a risk assessment with similar package names and the edit distance.",
|
|
304
|
+
inputSchema: {
|
|
305
|
+
type: "object",
|
|
306
|
+
properties: {
|
|
307
|
+
package: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "Package name to check for typosquatting risk."
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
required: ["package"]
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "dep_oracle_compare",
|
|
317
|
+
description: "Compare two packages side-by-side by computing trust scores for both. Returns the full trust report for each package including scores, metrics, zombie status, and trend direction. Useful for evaluating alternatives or choosing between competing libraries.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
packageA: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description: "First package name to compare."
|
|
324
|
+
},
|
|
325
|
+
packageB: {
|
|
326
|
+
type: "string",
|
|
327
|
+
description: "Second package name to compare."
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
required: ["packageA", "packageB"]
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "dep_oracle_report",
|
|
335
|
+
description: "Generate a JSON report for a project. Runs a full scan and outputs the results as formatted JSON. Optionally writes the report to a file. Returns the JSON content or the path to the generated file.",
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: {
|
|
339
|
+
dir: {
|
|
340
|
+
type: "string",
|
|
341
|
+
description: "Absolute path to the project directory. Defaults to cwd if omitted."
|
|
342
|
+
},
|
|
343
|
+
output: {
|
|
344
|
+
type: "string",
|
|
345
|
+
description: "Absolute path to write the report file. If omitted, the report content is returned directly."
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
required: []
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
];
|
|
352
|
+
function registerTools(server2) {
|
|
353
|
+
server2.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
354
|
+
tools: TOOLS
|
|
355
|
+
}));
|
|
356
|
+
server2.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
357
|
+
const { name, arguments: args } = request.params;
|
|
358
|
+
try {
|
|
359
|
+
switch (name) {
|
|
360
|
+
case "dep_oracle_scan":
|
|
361
|
+
return await handleScan(args);
|
|
362
|
+
case "dep_oracle_trust_score":
|
|
363
|
+
return await handleTrustScore(args);
|
|
364
|
+
case "dep_oracle_blast_radius":
|
|
365
|
+
return await handleBlastRadius(args);
|
|
366
|
+
case "dep_oracle_zombies":
|
|
367
|
+
return await handleZombies(args);
|
|
368
|
+
case "dep_oracle_suggest_migration":
|
|
369
|
+
return await handleSuggestMigration(args);
|
|
370
|
+
case "dep_oracle_typosquat_check":
|
|
371
|
+
return await handleTyposquatCheck(args);
|
|
372
|
+
case "dep_oracle_compare":
|
|
373
|
+
return await handleCompare(args);
|
|
374
|
+
case "dep_oracle_report":
|
|
375
|
+
return await handleReport(args);
|
|
376
|
+
default:
|
|
377
|
+
return errorResponse(`Unknown tool: ${name}`);
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
381
|
+
return errorResponse(`Tool "${name}" failed: ${message}`);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function successResponse(data) {
|
|
386
|
+
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
387
|
+
return {
|
|
388
|
+
content: [{ type: "text", text }]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function errorResponse(message) {
|
|
392
|
+
return {
|
|
393
|
+
content: [{ type: "text", text: message }],
|
|
394
|
+
isError: true
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async function parseProject(dir) {
|
|
398
|
+
for (const parser of parsers) {
|
|
399
|
+
if (await parser.detect(dir)) {
|
|
400
|
+
return parser.parse(dir);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
async function buildTrustReport(packageName, version, blastRadius = 0) {
|
|
406
|
+
const results = await orchestrator.collectAll(packageName, version);
|
|
407
|
+
const trustResult = trustEngine.calculate(results);
|
|
408
|
+
const zombie = zombieDetector.detect(
|
|
409
|
+
results.registry.data,
|
|
410
|
+
results.github.data
|
|
411
|
+
);
|
|
412
|
+
const typosquat = typosquatDetector.check(packageName);
|
|
413
|
+
const alternatives = migrationAdvisor.suggest(
|
|
414
|
+
packageName,
|
|
415
|
+
zombie.isZombie ? "zombie dependency" : "low trust score"
|
|
416
|
+
);
|
|
417
|
+
const trend = results.popularity.data?.trend ?? "stable";
|
|
418
|
+
return {
|
|
419
|
+
package: packageName,
|
|
420
|
+
version,
|
|
421
|
+
trustScore: trustResult.trustScore,
|
|
422
|
+
metrics: trustResult.metrics,
|
|
423
|
+
isZombie: zombie.isZombie,
|
|
424
|
+
blastRadius,
|
|
425
|
+
alternatives: alternatives.map((a) => a.alternative),
|
|
426
|
+
trend,
|
|
427
|
+
typosquatRisk: typosquat.isRisky ? 1 : 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function handleScan(args) {
|
|
431
|
+
const dir = validateDir(args?.dir);
|
|
432
|
+
const tree = await parseProject(dir);
|
|
433
|
+
if (!tree) {
|
|
434
|
+
return errorResponse(
|
|
435
|
+
`No supported manifest file found in "${dir}". Supported: package.json, requirements.txt, pyproject.toml, Pipfile.`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
const importGraph = await buildImportGraph(dir);
|
|
439
|
+
const directNodes = Array.from(tree.nodes.values()).filter((n) => n.isDirect);
|
|
440
|
+
const reports = [];
|
|
441
|
+
for (const node of directNodes) {
|
|
442
|
+
const blastRadius = getBlastRadius(node.name, importGraph);
|
|
443
|
+
const report = await buildTrustReport(node.name, node.version, blastRadius);
|
|
444
|
+
reports.push(report);
|
|
445
|
+
}
|
|
446
|
+
const overallScore = reports.length > 0 ? Math.round(
|
|
447
|
+
reports.reduce((sum, r) => sum + r.trustScore, 0) / reports.length
|
|
448
|
+
) : 0;
|
|
449
|
+
const zombieCount = reports.filter((r) => r.isZombie).length;
|
|
450
|
+
const criticalCount = reports.filter((r) => r.trustScore < 50).length;
|
|
451
|
+
const summary = `Scanned ${reports.length} direct dependencies. Overall trust score: ${overallScore}/100. ${zombieCount} zombie(s) detected. ${criticalCount} package(s) below trust threshold.`;
|
|
452
|
+
const scanResult = {
|
|
453
|
+
tree,
|
|
454
|
+
reports,
|
|
455
|
+
overallScore,
|
|
456
|
+
summary
|
|
457
|
+
};
|
|
458
|
+
const serialized = jsonReporter.report(scanResult);
|
|
459
|
+
return successResponse(serialized);
|
|
460
|
+
}
|
|
461
|
+
async function handleTrustScore(args) {
|
|
462
|
+
const packageName = validatePackageName(String(args?.package ?? ""));
|
|
463
|
+
const version = String(args?.version ?? "latest");
|
|
464
|
+
const report = await buildTrustReport(packageName, version);
|
|
465
|
+
return successResponse(report);
|
|
466
|
+
}
|
|
467
|
+
async function handleBlastRadius(args) {
|
|
468
|
+
const packageName = validatePackageName(String(args?.package ?? ""));
|
|
469
|
+
const dir = validateDir(args?.dir);
|
|
470
|
+
const result = await blastRadiusCalc.calculate(packageName, dir);
|
|
471
|
+
return successResponse(result);
|
|
472
|
+
}
|
|
473
|
+
async function handleZombies(args) {
|
|
474
|
+
const dir = validateDir(args?.dir);
|
|
475
|
+
const tree = await parseProject(dir);
|
|
476
|
+
if (!tree) {
|
|
477
|
+
return errorResponse(
|
|
478
|
+
`No supported manifest file found in "${dir}".`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const directNodes = Array.from(tree.nodes.values()).filter((n) => n.isDirect);
|
|
482
|
+
const zombies = [];
|
|
483
|
+
for (const node of directNodes) {
|
|
484
|
+
const results = await orchestrator.collectAll(node.name, node.version);
|
|
485
|
+
const zombie = zombieDetector.detect(
|
|
486
|
+
results.registry.data,
|
|
487
|
+
results.github.data
|
|
488
|
+
);
|
|
489
|
+
if (zombie.isZombie) {
|
|
490
|
+
zombies.push({
|
|
491
|
+
package: node.name,
|
|
492
|
+
version: node.version,
|
|
493
|
+
severity: zombie.severity,
|
|
494
|
+
reason: zombie.reason,
|
|
495
|
+
lastActivity: zombie.lastActivity?.toISOString() ?? null
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (zombies.length === 0) {
|
|
500
|
+
return successResponse({
|
|
501
|
+
message: "No zombie dependencies detected.",
|
|
502
|
+
zombies: []
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return successResponse({
|
|
506
|
+
message: `Found ${zombies.length} zombie dependency(ies).`,
|
|
507
|
+
zombies
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async function handleSuggestMigration(args) {
|
|
511
|
+
const packageName = validatePackageName(String(args?.package ?? ""));
|
|
512
|
+
const suggestions = migrationAdvisor.suggest(packageName, "manual query");
|
|
513
|
+
if (suggestions.length === 0) {
|
|
514
|
+
return successResponse({
|
|
515
|
+
package: packageName,
|
|
516
|
+
message: `No migration suggestions found for "${packageName}". This package may not have known alternatives in our database.`,
|
|
517
|
+
suggestions: []
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return successResponse({
|
|
521
|
+
package: packageName,
|
|
522
|
+
suggestions
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async function handleTyposquatCheck(args) {
|
|
526
|
+
const packageName = validatePackageName(String(args?.package ?? ""));
|
|
527
|
+
const result = typosquatDetector.check(packageName);
|
|
528
|
+
return successResponse({
|
|
529
|
+
package: packageName,
|
|
530
|
+
...result
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
async function handleCompare(args) {
|
|
534
|
+
const packageA = validatePackageName(String(args?.packageA ?? ""));
|
|
535
|
+
const packageB = validatePackageName(String(args?.packageB ?? ""));
|
|
536
|
+
const [reportA, reportB] = await Promise.all([
|
|
537
|
+
buildTrustReport(packageA, "latest"),
|
|
538
|
+
buildTrustReport(packageB, "latest")
|
|
539
|
+
]);
|
|
540
|
+
return successResponse({
|
|
541
|
+
comparison: {
|
|
542
|
+
packageA: reportA,
|
|
543
|
+
packageB: reportB,
|
|
544
|
+
winner: reportA.trustScore > reportB.trustScore ? packageA : reportA.trustScore < reportB.trustScore ? packageB : "tie",
|
|
545
|
+
scoreDifference: Math.abs(reportA.trustScore - reportB.trustScore)
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
async function handleReport(args) {
|
|
550
|
+
const dir = validateDir(args?.dir);
|
|
551
|
+
const output = args?.output ? validateOutputPath(String(args.output), dir) : null;
|
|
552
|
+
const tree = await parseProject(dir);
|
|
553
|
+
if (!tree) {
|
|
554
|
+
return errorResponse(
|
|
555
|
+
`No supported manifest file found in "${dir}".`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const importGraph = await buildImportGraph(dir);
|
|
559
|
+
const directNodes = Array.from(tree.nodes.values()).filter((n) => n.isDirect);
|
|
560
|
+
const reports = [];
|
|
561
|
+
for (const node of directNodes) {
|
|
562
|
+
const blastRadius = getBlastRadius(node.name, importGraph);
|
|
563
|
+
const report = await buildTrustReport(node.name, node.version, blastRadius);
|
|
564
|
+
reports.push(report);
|
|
565
|
+
}
|
|
566
|
+
const overallScore = reports.length > 0 ? Math.round(
|
|
567
|
+
reports.reduce((sum, r) => sum + r.trustScore, 0) / reports.length
|
|
568
|
+
) : 0;
|
|
569
|
+
const zombieCount = reports.filter((r) => r.isZombie).length;
|
|
570
|
+
const criticalCount = reports.filter((r) => r.trustScore < 50).length;
|
|
571
|
+
const summary = `Scanned ${reports.length} direct dependencies. Overall trust score: ${overallScore}/100. ${zombieCount} zombie(s) detected. ${criticalCount} package(s) below trust threshold.`;
|
|
572
|
+
const scanResult = {
|
|
573
|
+
tree,
|
|
574
|
+
reports,
|
|
575
|
+
overallScore,
|
|
576
|
+
summary
|
|
577
|
+
};
|
|
578
|
+
const jsonContent = jsonReporter.report(scanResult);
|
|
579
|
+
if (output) {
|
|
580
|
+
const { writeFile } = await import("fs/promises");
|
|
581
|
+
await writeFile(output, jsonContent, "utf-8");
|
|
582
|
+
return successResponse({
|
|
583
|
+
message: `Report written to ${output}`,
|
|
584
|
+
path: output
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return successResponse(jsonContent);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/mcp/server.ts
|
|
591
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
592
|
+
var __dirname2 = dirname(__filename2);
|
|
593
|
+
var pkgPath = join2(__dirname2, "..", "..", "package.json");
|
|
594
|
+
var pkgVersion = (() => {
|
|
595
|
+
try {
|
|
596
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
597
|
+
} catch {
|
|
598
|
+
return "1.2.1";
|
|
599
|
+
}
|
|
600
|
+
})();
|
|
601
|
+
var server = new Server(
|
|
602
|
+
{ name: "dep-oracle", version: pkgVersion },
|
|
603
|
+
{ capabilities: { tools: {} } }
|
|
604
|
+
);
|
|
605
|
+
registerTools(server);
|
|
606
|
+
var transport = new StdioServerTransport();
|
|
607
|
+
await server.connect(transport);
|
|
608
|
+
//# sourceMappingURL=server-TKLM7YIF.js.map
|