🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@graneth/mcp-server

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@graneth/mcp-server - npm Package Compare versions

Comparing version
0.6.0
to
0.7.0
+219
-63
dist/index.js
#!/usr/bin/env node
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { createInterface } from "readline";
// src/protocol.ts
var LATEST_PROTOCOL_VERSION = "2025-11-25";
var SUPPORTED_PROTOCOL_VERSIONS = [
LATEST_PROTOCOL_VERSION,
"2025-06-18",
"2025-03-26",
"2024-11-05",
"2024-10-07"
];
var ErrorCode = {
ParseError: -32700,
InvalidRequest: -32600,
MethodNotFound: -32601,
InvalidParams: -32602,
InternalError: -32603
};
var InvalidParams = class extends Error {
};
function ok(id, result) {
return { jsonrpc: "2.0", id, result };
}
function fail(id, code, message) {
return { jsonrpc: "2.0", id, error: { code, message } };
}
function createServer(opts) {
const byName = new Map(opts.tools.map((t) => [t.name, t]));
async function callTool(id, params) {
const name = params?.name;
const tool = typeof name === "string" ? byName.get(name) : void 0;
if (!tool) {
return fail(id, ErrorCode.InvalidParams, `Unknown tool: ${String(name)}`);
}
let input;
try {
input = tool.parse(params?.arguments);
} catch (err) {
const message = err instanceof InvalidParams ? err.message : `Invalid arguments: ${String(err)}`;
return fail(id, ErrorCode.InvalidParams, message);
}
try {
const result = await tool.handler(input);
return ok(id, result);
} catch (err) {
return fail(id, ErrorCode.InternalError, `Tool "${tool.name}" failed: ${String(err)}`);
}
}
async function handleMessage(msg) {
const req = msg ?? {};
const id = req.id ?? null;
const method = req.method;
if (typeof method !== "string") {
return fail(id, ErrorCode.InvalidRequest, "Missing or invalid `method`");
}
if (method.startsWith("notifications/")) return null;
switch (method) {
case "initialize": {
const requested = req.params?.protocolVersion;
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL_VERSION;
return ok(id, {
protocolVersion,
capabilities: { tools: {} },
serverInfo: { name: opts.name, version: opts.version }
});
}
case "ping":
return ok(id, {});
case "tools/list":
return ok(id, {
tools: opts.tools.map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema
}))
});
case "tools/call":
return callTool(id, req.params);
default:
return fail(id, ErrorCode.MethodNotFound, `Method not found: ${method}`);
}
}
return { handleMessage };
}
// ../core-checks/src/secrets.ts

@@ -1216,13 +1297,58 @@ function shannonEntropy(str) {

// src/index.ts
var VERSION = "0.6.0";
var VERSION = "0.7.0";
function requireObject(args) {
if (!args || typeof args !== "object" || Array.isArray(args)) {
throw new InvalidParams("arguments must be an object");
}
return args;
}
function requireString(v, field, min, max) {
if (typeof v !== "string") throw new InvalidParams(`\`${field}\` must be a string`);
if (v.length < min) throw new InvalidParams(`\`${field}\` must be at least ${min} character(s)`);
if (v.length > max) throw new InvalidParams(`\`${field}\` must be at most ${max} characters`);
return v;
}
function optionalString(v, field, min, max) {
if (v === void 0 || v === null) return void 0;
return requireString(v, field, min, max);
}
var PRE_FLIGHT_CHECK_DESCRIPTION = "Security pre-flight check for local file changes BEFORE committing. Run this whenever you are about to suggest `git commit`, `git push`, or open a pull request \u2014 especially when changes add package imports or dependencies (package.json / requirements.txt / Cargo.toml / go.mod / Gemfile / composer.json / import statements) or could contain secrets. Detects AI-hallucinated (non-existent) packages by live-checking six registries (npm, PyPI, crates.io, RubyGems, Go module proxy, Packagist) plus a bundled public threat-feed snapshot, which keeps catching a known hallucinated name even after an attacker registers it; risk-scores dependencies an AI agent introduced by compounding metadata signals (new + install-scripts + low-adoption + no-provenance = an attack shape no single check flags); and finds hardcoded credentials via pattern + entropy analysis. Always free, no account required. Returns CLEAR, REVIEW_REQUIRED, or BLOCKED with specific findings and remediation.";
var PRE_FLIGHT_CHECK_SCHEMA = {
files: z.array(
z.object({
path: z.string().min(1).max(1024).describe("Relative file path"),
content: z.string().max(2e5).describe("Full file content to check")
})
).min(1).max(50).describe("Files staged for commit (path + content)"),
context: z.string().max(500).optional().describe("What these changes do (helps triage)")
var PRE_FLIGHT_CHECK_INPUT_SCHEMA = {
type: "object",
properties: {
files: {
type: "array",
minItems: 1,
maxItems: 50,
description: "Files staged for commit (path + content)",
items: {
type: "object",
properties: {
path: { type: "string", minLength: 1, maxLength: 1024, description: "Relative file path" },
content: { type: "string", maxLength: 2e5, description: "Full file content to check" }
},
required: ["path", "content"],
additionalProperties: false
}
},
context: { type: "string", maxLength: 500, description: "What these changes do (helps triage)" }
},
required: ["files"],
additionalProperties: false
};
function parsePreFlight(args) {
const o = requireObject(args);
if (!Array.isArray(o.files)) throw new InvalidParams("`files` must be an array");
if (o.files.length < 1) throw new InvalidParams("`files` must contain at least 1 file");
if (o.files.length > 50) throw new InvalidParams("`files` must contain at most 50 files");
const files = o.files.map((raw, i) => {
if (!raw || typeof raw !== "object") throw new InvalidParams(`files[${i}] must be an object`);
const f = raw;
return {
path: requireString(f.path, `files[${i}].path`, 1, 1024),
content: requireString(f.content, `files[${i}].content`, 0, 2e5)
};
});
return { files, context: optionalString(o.context, "context", 0, 500) };
}
var VERDICT_ACTION = {

@@ -1262,20 +1388,44 @@ BLOCKED: "Do NOT commit these changes. Fix all CRITICAL findings before proceeding. Use each finding's `recommendation` for remediation.",

}
async function handlePreFlightCheckTool({ files, context }) {
try {
const result = await runPreFlightCheck(files, context);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
} catch (err) {
return {
content: [{ type: "text", text: `Pre-flight check failed: ${String(err)}` }],
isError: true
};
var preFlightCheckTool = {
name: "pre_flight_check",
description: PRE_FLIGHT_CHECK_DESCRIPTION,
inputSchema: PRE_FLIGHT_CHECK_INPUT_SCHEMA,
parse: parsePreFlight,
async handler({ files, context }) {
try {
const result = await runPreFlightCheck(files, context);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
} catch (err) {
return {
content: [{ type: "text", text: `Pre-flight check failed: ${String(err)}` }],
isError: true
};
}
}
}
};
var REPORT_URL = "https://graneth.com/api/threat-feed/report";
var REPORTABLE_ECOSYSTEMS = ["npm", "pypi", "crates", "gems"];
var REPORT_HALLUCINATION_DESCRIPTION = "Report an AI-hallucinated package name to Graneth's public threat feed, so every user's pre-flight catches it \u2014 permanently, even if an attacker registers the name later. IMPORTANT: only call this AFTER the human has explicitly agreed to report the name; ask them first. Sends exactly one package name + ecosystem to graneth.com (never file contents). The server re-verifies non-existence against the live registry before accepting. Reportable ecosystems: npm (unscoped), pypi, crates, gems \u2014 namespaced names (npm @scope, go, composer) are rejected to keep internal package names out of a public feed. Optional `reporter` is a public attribution handle shown on the feed entry.";
var REPORT_HALLUCINATION_SCHEMA = {
package: z.string().min(1).max(214).describe("The hallucinated package name (as caught by pre_flight_check)"),
ecosystem: z.enum(["npm", "pypi", "crates", "gems"]).describe("Registry the name was checked against"),
reporter: z.string().min(2).max(30).optional().describe("Optional public attribution handle ([a-zA-Z0-9_-])")
var REPORT_HALLUCINATION_INPUT_SCHEMA = {
type: "object",
properties: {
package: { type: "string", minLength: 1, maxLength: 214, description: "The hallucinated package name (as caught by pre_flight_check)" },
ecosystem: { type: "string", enum: [...REPORTABLE_ECOSYSTEMS], description: "Registry the name was checked against" },
reporter: { type: "string", minLength: 2, maxLength: 30, description: "Optional public attribution handle ([a-zA-Z0-9_-])" }
},
required: ["package", "ecosystem"],
additionalProperties: false
};
function parseReport(args) {
const o = requireObject(args);
const eco = o.ecosystem;
if (typeof eco !== "string" || !REPORTABLE_ECOSYSTEMS.includes(eco)) {
throw new InvalidParams(`\`ecosystem\` must be one of: ${REPORTABLE_ECOSYSTEMS.join(", ")}`);
}
return {
package: requireString(o.package, "package", 1, 214),
ecosystem: eco,
reporter: optionalString(o.reporter, "reporter", 2, 30)
};
}
var REPORT_OUTCOME_TEXT = {

@@ -1287,43 +1437,49 @@ accepted: (b) => `Reported. "${b.entry?.name}" is in the public threat feed now (community tier)${b.entry?.reporter ? `, attributed to "${b.entry.reporter}"` : ""}. It stays caught for every Graneth user even if someone registers the name later. Feed: https://graneth.com/api/threat-feed`,

};
async function handleReportHallucinationTool({ package: pkg, ecosystem, reporter }) {
try {
const res = await fetch(REPORT_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ package: pkg, ecosystem, reporter }),
signal: AbortSignal.timeout(15e3)
});
const body = await res.json().catch(() => ({}));
const render = REPORT_OUTCOME_TEXT[body.status];
const text = render ? render(body) : `Unexpected response (HTTP ${res.status}) \u2014 nothing was stored.`;
return { content: [{ type: "text", text }] };
} catch (err) {
return {
content: [{ type: "text", text: `Report failed before reaching the feed: ${String(err)}. Nothing was stored.` }],
isError: true
};
var reportHallucinationTool = {
name: "report_hallucination",
description: REPORT_HALLUCINATION_DESCRIPTION,
inputSchema: REPORT_HALLUCINATION_INPUT_SCHEMA,
parse: parseReport,
async handler({ package: pkg, ecosystem, reporter }) {
try {
const res = await fetch(REPORT_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ package: pkg, ecosystem, reporter }),
signal: AbortSignal.timeout(15e3)
});
const body = await res.json().catch(() => ({}));
const render = REPORT_OUTCOME_TEXT[body.status];
const text = render ? render(body) : `Unexpected response (HTTP ${res.status}) \u2014 nothing was stored.`;
return { content: [{ type: "text", text }] };
} catch (err) {
return {
content: [{ type: "text", text: `Report failed before reaching the feed: ${String(err)}. Nothing was stored.` }],
isError: true
};
}
}
}
function buildServer() {
const mcp = new McpServer({ name: "graneth", version: VERSION });
mcp.tool(
"pre_flight_check",
PRE_FLIGHT_CHECK_DESCRIPTION,
PRE_FLIGHT_CHECK_SCHEMA,
handlePreFlightCheckTool
);
mcp.tool(
"report_hallucination",
REPORT_HALLUCINATION_DESCRIPTION,
REPORT_HALLUCINATION_SCHEMA,
handleReportHallucinationTool
);
return mcp;
}
};
async function main() {
const server = buildServer();
const transport = new StdioServerTransport();
await server.connect(transport);
const server = createServer({
name: "graneth",
version: VERSION,
tools: [preFlightCheckTool, reportHallucinationTool]
});
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
process.stderr.write(`[graneth-mcp-server] v${VERSION} ready on stdio (pre_flight_check, report_hallucination)
`);
for await (const line of rl) {
const trimmed = line.trim();
if (!trimmed) continue;
let msg;
try {
msg = JSON.parse(trimmed);
} catch {
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: ErrorCode.ParseError, message: "Parse error" } }) + "\n");
continue;
}
const response = await server.handleMessage(msg);
if (response !== null) process.stdout.write(JSON.stringify(response) + "\n");
}
}

@@ -1330,0 +1486,0 @@ main().catch((err) => {

{
"name": "@graneth/mcp-server",
"version": "0.6.0",
"version": "0.7.0",
"mcpName": "com.graneth/mcp-server",

@@ -31,6 +31,2 @@ "description": "Account-free MCP server: catch AI-hallucinated packages (npm, PyPI, crates.io, RubyGems, Go, Packagist), risk-score the dependencies an AI agent introduces, and find hardcoded secrets before you commit. Exposes the free pre_flight_check tool over stdio.",

"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^4.4.3"
},
"devDependencies": {

@@ -37,0 +33,0 @@ "tsup": "^8.5.1"

@@ -7,2 +7,6 @@ # @graneth/mcp-server

**Zero runtime dependencies** (verify: `npm view @graneth/mcp-server dependencies`). A
supply-chain security tool should be auditable in full — so the MCP/JSON-RPC layer is a
small hand-rolled core, not a framework, and `npm audit` on a fresh install is clean.
It exposes a single, always-free tool over stdio:

@@ -9,0 +13,0 @@