#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs";
import path from "node:path";
import { parsePackageJson, parsePackageLock, parseYarnLock, parsePnpmLock } from "./parsers";
import { analyzeDependencies } from "./analyzer";
import { getVulnDetailsBatch } from "./api";
import { Dependency } from "./types";
import { getPackageVersion } from "./version"; // Import the helper
const server = new McpServer({
name: "dependency-checker",
version: getPackageVersion(), // Use the helper function
});
// --- Tools ---
server.registerTool(
"scan_file",
{
description: "Parses a manifest file and checks for vulnerabilities.",
inputSchema: { file_path: z.string() }
},
async ({ file_path }) => {
if (!fs.existsSync(file_path)) return { isError: true, content: [{ type: "text", text: "File not found" }] };
try {
const content = fs.readFileSync(file_path, "utf8");
const filename = path.basename(file_path);
let dependencies: Dependency[] = [];
if (filename === "package.json") dependencies = parsePackageJson(content);
else if (filename === "package-lock.json") dependencies = parsePackageLock(content);
else if (filename === "yarn.lock") dependencies = parseYarnLock(content);
else if (filename === "pnpm-lock.yaml") dependencies = parsePnpmLock(content);
else return { isError: true, content: [{ type: "text", text: "Unsupported file" }] };
if (dependencies.length === 0) return { content: [{ type: "text", text: "No dependencies found." }] };
const results = await analyzeDependencies(dependencies);
const summary = `Scanned ${dependencies.length} dependencies from ${filename}. Found ${results.length} issues.`;
if (results.length === 0) return { content: [{ type: "text", text: `✅ ${summary}` }] };
return { content: [{ type: "text", text: summary }, { type: "text", text: JSON.stringify(results, null, 2) }] };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { isError: true, content: [{ type: "text", text: `Error: ${errorMessage}` }] };
}
}
);
server.registerTool(
"check_vulnerabilities",
{
description: "Checks a list of dependencies.",
inputSchema: {
dependencies: z.array(z.object({ name: z.string(), version: z.string() })),
}
},
async ({ dependencies }) => {
// Zod inferred type matches Dependency interface closely enough for this usage
const results = await analyzeDependencies(dependencies as Dependency[]);
return { content: [{ type: "text", text: results.length ? JSON.stringify(results, null, 2) : "✅ No vulnerabilities." }] };
});
server.registerTool(
"get_vulnerability_details",
{
description: "Get details for a list of vulnerability IDs.",
inputSchema: { vuln_ids: z.array(z.string()) }
},
async ({ vuln_ids }) => {
const details = await getVulnDetailsBatch(vuln_ids);
const results = details.map((d, i) => d || { id: vuln_ids[i], error: "Not found" });
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
});
// --- Prompts ---
server.registerPrompt(
"audit_dependencies",
{
description: "Instructs the agent to perform a comprehensive security audit of the project's dependencies.",
arguments: []
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You are an expert AppSec Engineer specializing in Software Supply Chain Security. Your task is to audit the dependencies of the current project.
STRICT PROTOCOL:
1. **Discovery**:
- Locate manifest files in the root directory. Prioritize lockfiles ('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml') for accurate deep scanning. If unavailable, fall back to 'package.json'.
- Use the 'scan_file' tool on the most relevant file found.
2. **Analysis**:
- For every detected Vulnerability (OSV):
- Analyze the severity (CVSS) and the attack vector.
- Determine if the vulnerability is likely exploitable in a standard context.
- For every detected Risk (Socket):
- Flag "Install Scripts" as CRITICAL requiring manual review.
- Flag "Low Reputation" or "Typosquatting" as HIGH.
3. **Presentation (DIAGNOSTIC ONLY)**:
- You MUST output a **Structured Diagnostic Report** first.
- **Summary**: Total dependencies scanned, count of critical/high issues.
- **Findings**: List the specific vulnerabilities and risks found (ID, Severity, Package).
- **STOP** after presenting the findings. **DO NOT** provide detailed remediation steps or execute commands yet.
4. **User Interaction**:
- End your response by explicitly asking the user how they wish to proceed:
- **[Apply Fixes]**: Should I attempt to automatically fix these issues (e.g., via 'npm update' or file edits)?
- **[Explain Fixes]**: Should I provide a detailed guide with commands for you to run manually?
- **[Dismiss]**: Do nothing further (diagnostic only).`
}
}
]
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
// Keep the process alive. This is important for some environments where
// the event loop might dry up if there are no active listeners.
setInterval(() => {
// No-op
}, 10000);
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error);
});
process.on("unhandledRejection", (reason) => {
console.error("Unhandled Rejection:", reason);
});