@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.
@@ -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
+ }