@gaurdi/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/dist/index.d.ts +11 -0
- package/dist/index.js +334 -0
- package/package.json +45 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Gaurdi MCP server — supply-chain security tools for AI coding agents.
|
|
4
|
+
*
|
|
5
|
+
* Exposes four tools over the Model Context Protocol (stdio transport):
|
|
6
|
+
* 1. gaurdi_check_package — check a single npm package
|
|
7
|
+
* 2. gaurdi_pre_install_check — parse an install command and check packages
|
|
8
|
+
* 3. gaurdi_suggest_alternative — suggest safer alternatives for a package
|
|
9
|
+
* 4. gaurdi_scan_lockfile — scan an entire lockfile for risks
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Gaurdi MCP server — supply-chain security tools for AI coding agents.
|
|
4
|
+
*
|
|
5
|
+
* Exposes four tools over the Model Context Protocol (stdio transport):
|
|
6
|
+
* 1. gaurdi_check_package — check a single npm package
|
|
7
|
+
* 2. gaurdi_pre_install_check — parse an install command and check packages
|
|
8
|
+
* 3. gaurdi_suggest_alternative — suggest safer alternatives for a package
|
|
9
|
+
* 4. gaurdi_scan_lockfile — scan an entire lockfile for risks
|
|
10
|
+
*/
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Configuration
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const API_URL = process.env.GAURDI_API_URL ?? "https://guardiancode-production.up.railway.app";
|
|
21
|
+
/** Request timeout in milliseconds for all outgoing HTTP calls. */
|
|
22
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
23
|
+
function loadAuthToken() {
|
|
24
|
+
const authPath = join(homedir(), ".gaurdi", "auth.json");
|
|
25
|
+
if (!existsSync(authPath))
|
|
26
|
+
return undefined;
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(readFileSync(authPath, "utf-8"));
|
|
29
|
+
return data.token;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const AUTH_TOKEN = loadAuthToken();
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// HTTP helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
async function apiGet(path) {
|
|
40
|
+
const headers = { Accept: "application/json" };
|
|
41
|
+
if (AUTH_TOKEN)
|
|
42
|
+
headers["Authorization"] = `Bearer ${AUTH_TOKEN}`;
|
|
43
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
44
|
+
headers,
|
|
45
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const body = await res.text();
|
|
49
|
+
throw new Error(`GET ${path} failed (${res.status}): ${body}`);
|
|
50
|
+
}
|
|
51
|
+
return (await res.json());
|
|
52
|
+
}
|
|
53
|
+
async function apiPost(path, body) {
|
|
54
|
+
const headers = {
|
|
55
|
+
Accept: "application/json",
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
};
|
|
58
|
+
if (AUTH_TOKEN)
|
|
59
|
+
headers["Authorization"] = `Bearer ${AUTH_TOKEN}`;
|
|
60
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers,
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const text = await res.text();
|
|
68
|
+
throw new Error(`POST ${path} failed (${res.status}): ${text}`);
|
|
69
|
+
}
|
|
70
|
+
return (await res.json());
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Tool implementations
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
async function checkPackage(ecosystem, name, version) {
|
|
76
|
+
const qs = version ? `?version=${encodeURIComponent(version)}` : "";
|
|
77
|
+
return apiGet(`/v1/package/${encodeURIComponent(ecosystem)}/${encodeURIComponent(name)}${qs}`);
|
|
78
|
+
}
|
|
79
|
+
function formatPackageScore(score) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push(`Package: ${score.package.name}@${score.package.version}`);
|
|
82
|
+
lines.push(`Risk: ${score.risk} (score ${score.score}/100)`);
|
|
83
|
+
lines.push(`Provenance: ${score.provenance}`);
|
|
84
|
+
if (score.signals.length > 0) {
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push("Signals:");
|
|
87
|
+
for (const s of score.signals) {
|
|
88
|
+
lines.push(` [${s.risk.toUpperCase()}] ${s.title}: ${s.description}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (score.alternatives.length > 0) {
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push("Suggested alternatives:");
|
|
94
|
+
for (const a of score.alternatives) {
|
|
95
|
+
const dl = a.weekly_downloads
|
|
96
|
+
? ` (${a.weekly_downloads.toLocaleString()} weekly downloads)`
|
|
97
|
+
: "";
|
|
98
|
+
lines.push(` - ${a.name}${dl}${a.reason ? ": " + a.reason : ""}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
/** Parse packages from a shell install command (npm install X Y, pip install X Y). */
|
|
104
|
+
function parseInstallCommand(command) {
|
|
105
|
+
const trimmed = command.trim();
|
|
106
|
+
// npm install / npm i / npm add / yarn add / pnpm add
|
|
107
|
+
const npmRe = /^(?:npm\s+(?:install|i|add)|yarn\s+add|pnpm\s+(?:add|install))\s+(.+)/i;
|
|
108
|
+
const npmMatch = trimmed.match(npmRe);
|
|
109
|
+
if (npmMatch) {
|
|
110
|
+
const args = npmMatch[1]
|
|
111
|
+
.split(/\s+/)
|
|
112
|
+
.filter((a) => !a.startsWith("-")); // strip flags like --save-dev
|
|
113
|
+
const packages = args.map((a) => a.replace(/@[\^~]?[\d.*]+$/, "")); // strip version suffixes
|
|
114
|
+
return { ecosystem: "npm", packages: packages.filter(Boolean) };
|
|
115
|
+
}
|
|
116
|
+
// pip install / pip3 install
|
|
117
|
+
const pipRe = /^pip3?\s+install\s+(.+)/i;
|
|
118
|
+
const pipMatch = trimmed.match(pipRe);
|
|
119
|
+
if (pipMatch) {
|
|
120
|
+
const args = pipMatch[1]
|
|
121
|
+
.split(/\s+/)
|
|
122
|
+
.filter((a) => !a.startsWith("-") && !a.startsWith("--"));
|
|
123
|
+
const packages = args.map((a) => a.replace(/[>=<!\[].*$/, ""));
|
|
124
|
+
return { ecosystem: "pypi", packages: packages.filter(Boolean) };
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// MCP Server setup
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
const server = new Server({ name: "gaurdi", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
132
|
+
// --- List tools -----------------------------------------------------------
|
|
133
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
134
|
+
tools: [
|
|
135
|
+
{
|
|
136
|
+
name: "gaurdi_check_package",
|
|
137
|
+
description: "Check a single package for supply-chain risks. Returns risk score, signals, provenance, and alternatives.",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: {
|
|
141
|
+
ecosystem: {
|
|
142
|
+
type: "string",
|
|
143
|
+
description: 'Package ecosystem (currently only "npm" supported).',
|
|
144
|
+
default: "npm",
|
|
145
|
+
},
|
|
146
|
+
name: { type: "string", description: "Package name, e.g. 'lodash'." },
|
|
147
|
+
version: {
|
|
148
|
+
type: "string",
|
|
149
|
+
description: "Specific version to check. Omit for latest.",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
required: ["name"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "gaurdi_pre_install_check",
|
|
157
|
+
description: 'Parse a package-install command (e.g. "npm install foo bar") and check each package for supply-chain risks before running it.',
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
command: {
|
|
162
|
+
type: "string",
|
|
163
|
+
description: 'The install command to check, e.g. "npm install express lodash".',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
required: ["command"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "gaurdi_suggest_alternative",
|
|
171
|
+
description: "Check a package and return suggested safer alternatives if the package is risky.",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: {
|
|
175
|
+
ecosystem: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: 'Package ecosystem (default "npm").',
|
|
178
|
+
default: "npm",
|
|
179
|
+
},
|
|
180
|
+
name: { type: "string", description: "Package name to find alternatives for." },
|
|
181
|
+
},
|
|
182
|
+
required: ["name"],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "gaurdi_scan_lockfile",
|
|
187
|
+
description: "Scan an entire lockfile (package-lock.json) for supply-chain risks across all dependencies.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
file_path: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "Absolute path to the lockfile to scan.",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
required: ["file_path"],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
}));
|
|
201
|
+
// --- Call tool -------------------------------------------------------------
|
|
202
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
203
|
+
const { name, arguments: args } = request.params;
|
|
204
|
+
try {
|
|
205
|
+
switch (name) {
|
|
206
|
+
// ---- gaurdi_check_package ------------------------------------
|
|
207
|
+
case "gaurdi_check_package": {
|
|
208
|
+
const ecosystem = args?.ecosystem ?? "npm";
|
|
209
|
+
const pkgName = args?.name;
|
|
210
|
+
const version = args?.version;
|
|
211
|
+
if (!pkgName)
|
|
212
|
+
throw new Error("Missing required argument: name");
|
|
213
|
+
const score = await checkPackage(ecosystem, pkgName, version);
|
|
214
|
+
return { content: [{ type: "text", text: formatPackageScore(score) }] };
|
|
215
|
+
}
|
|
216
|
+
// ---- gaurdi_pre_install_check --------------------------------
|
|
217
|
+
case "gaurdi_pre_install_check": {
|
|
218
|
+
const command = args?.command;
|
|
219
|
+
if (!command)
|
|
220
|
+
throw new Error("Missing required argument: command");
|
|
221
|
+
const parsed = parseInstallCommand(command);
|
|
222
|
+
if (!parsed || parsed.packages.length === 0) {
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: "text",
|
|
227
|
+
text: `Could not parse install command: ${command}\nSupported formats: npm install <pkg>, yarn add <pkg>, pip install <pkg>`,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// Check all packages in parallel
|
|
233
|
+
const settledResults = await Promise.allSettled(parsed.packages.map((pkg) => checkPackage(parsed.ecosystem, pkg)));
|
|
234
|
+
const results = [];
|
|
235
|
+
let blocked = false;
|
|
236
|
+
for (let i = 0; i < parsed.packages.length; i++) {
|
|
237
|
+
const pkg = parsed.packages[i];
|
|
238
|
+
const settled = settledResults[i];
|
|
239
|
+
if (settled.status === "fulfilled") {
|
|
240
|
+
const score = settled.value;
|
|
241
|
+
results.push(formatPackageScore(score));
|
|
242
|
+
if (score.score >= 60) {
|
|
243
|
+
blocked = true;
|
|
244
|
+
results.push(`\n** BLOCKED: ${pkg} has risk score ${score.score}/100 (>= 60 threshold) **`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
results.push(`${pkg}: error checking — ${settled.reason}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const verdict = blocked
|
|
252
|
+
? "VERDICT: One or more packages exceed the risk threshold. Do NOT run this command."
|
|
253
|
+
: "VERDICT: All packages pass. Safe to proceed.";
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text", text: results.join("\n\n---\n\n") + "\n\n" + verdict }],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// ---- gaurdi_suggest_alternative ------------------------------
|
|
259
|
+
case "gaurdi_suggest_alternative": {
|
|
260
|
+
const ecosystem = args?.ecosystem ?? "npm";
|
|
261
|
+
const pkgName = args?.name;
|
|
262
|
+
if (!pkgName)
|
|
263
|
+
throw new Error("Missing required argument: name");
|
|
264
|
+
const score = await checkPackage(ecosystem, pkgName);
|
|
265
|
+
const lines = [];
|
|
266
|
+
lines.push(`${score.package.name}@${score.package.version}: risk=${score.risk} score=${score.score}/100`);
|
|
267
|
+
if (score.alternatives.length === 0) {
|
|
268
|
+
lines.push("\nNo alternatives suggested. The package may be safe or unique.");
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
lines.push("\nSuggested alternatives:");
|
|
272
|
+
for (const a of score.alternatives) {
|
|
273
|
+
const dl = a.weekly_downloads
|
|
274
|
+
? ` (${a.weekly_downloads.toLocaleString()} weekly downloads)`
|
|
275
|
+
: "";
|
|
276
|
+
lines.push(` - ${a.name}${dl}${a.reason ? ": " + a.reason : ""}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
280
|
+
}
|
|
281
|
+
// ---- gaurdi_scan_lockfile ------------------------------------
|
|
282
|
+
case "gaurdi_scan_lockfile": {
|
|
283
|
+
const filePath = args?.file_path;
|
|
284
|
+
if (!filePath)
|
|
285
|
+
throw new Error("Missing required argument: file_path");
|
|
286
|
+
let content;
|
|
287
|
+
try {
|
|
288
|
+
content = readFileSync(filePath, "utf-8");
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
throw new Error(`Cannot read lockfile at ${filePath}: ${err}`);
|
|
292
|
+
}
|
|
293
|
+
const result = await apiPost("/v1/scan", {
|
|
294
|
+
lockfile_content: content,
|
|
295
|
+
});
|
|
296
|
+
const lines = [];
|
|
297
|
+
lines.push(`Scanned ${result.total_packages} packages in ${result.duration_ms}ms`);
|
|
298
|
+
lines.push(`Overall risk: ${result.overall_risk}`);
|
|
299
|
+
lines.push(`Flagged packages: ${result.flagged_packages}`);
|
|
300
|
+
if (result.packages.length > 0) {
|
|
301
|
+
lines.push("");
|
|
302
|
+
// Show only flagged packages (risk > None/Info)
|
|
303
|
+
const flagged = result.packages.filter((p) => p.risk !== "None" && p.risk !== "Info");
|
|
304
|
+
for (const pkg of flagged) {
|
|
305
|
+
lines.push("---");
|
|
306
|
+
lines.push(formatPackageScore(pkg));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
310
|
+
}
|
|
311
|
+
default:
|
|
312
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
319
|
+
isError: true,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Main
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
async function main() {
|
|
327
|
+
const transport = new StdioServerTransport();
|
|
328
|
+
await server.connect(transport);
|
|
329
|
+
console.error("Gaurdi MCP server running on stdio");
|
|
330
|
+
}
|
|
331
|
+
main().catch((err) => {
|
|
332
|
+
console.error("Fatal:", err);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gaurdi/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Gaurdi MCP server — supply chain security for AI coding tools. Checks every package before your AI agent installs it.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gaurdi-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"supply-chain",
|
|
18
|
+
"security",
|
|
19
|
+
"npm",
|
|
20
|
+
"ai-agent",
|
|
21
|
+
"gaurdi"
|
|
22
|
+
],
|
|
23
|
+
"author": "Gaurdi (https://gaurdi.com)",
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/mithunnarayana89/guardianCode.git",
|
|
28
|
+
"directory": "mcp-server"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://gaurdi.com",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^25.5.2",
|
|
43
|
+
"typescript": "^5.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|