auditor-mcp 0.1.4 → 0.1.6
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.js +106 -42
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { config as loadEnv } from "dotenv";
|
|
5
|
-
import { dirname, resolve } from "path";
|
|
5
|
+
import { dirname, resolve, join } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { readFile } from "fs/promises";
|
|
7
|
+
import { readFile, writeFile, mkdir, readdir, stat } from "fs/promises";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { homedir } from "os";
|
|
8
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
12
|
import { wrapFetchWithPayment, x402Client, x402HTTPClient, decodePaymentResponseHeader } from "@x402/fetch";
|
|
@@ -206,6 +208,46 @@ var mppClient = MppxClient.create({
|
|
|
206
208
|
methods: [stellarMpp.charge({ secretKey: STELLAR_SECRET_KEY })],
|
|
207
209
|
polyfill: false
|
|
208
210
|
});
|
|
211
|
+
var REPORTS_DIR = join(homedir(), ".auditor-mcp", "reports");
|
|
212
|
+
async function findRsFiles(dir) {
|
|
213
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
214
|
+
const files = [];
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
const full = join(dir, entry.name);
|
|
217
|
+
if (entry.isDirectory()) {
|
|
218
|
+
files.push(...await findRsFiles(full));
|
|
219
|
+
} else if (entry.isFile() && entry.name.endsWith(".rs")) {
|
|
220
|
+
files.push(full);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return files.sort();
|
|
224
|
+
}
|
|
225
|
+
async function loadContractCode(filePath) {
|
|
226
|
+
const info = await stat(filePath);
|
|
227
|
+
if (info.isDirectory()) {
|
|
228
|
+
const rsFiles = await findRsFiles(filePath);
|
|
229
|
+
if (rsFiles.length === 0)
|
|
230
|
+
throw new Error(`No .rs files found in directory: ${filePath}`);
|
|
231
|
+
const sections = await Promise.all(
|
|
232
|
+
rsFiles.map(async (f) => {
|
|
233
|
+
const content = await readFile(f, "utf8");
|
|
234
|
+
const relative = f.slice(filePath.length).replace(/^\//, "");
|
|
235
|
+
return `// === FILE: ${relative} ===
|
|
236
|
+
|
|
237
|
+
${content}`;
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
return { code: sections.join("\n\n"), filesAudited: rsFiles };
|
|
241
|
+
}
|
|
242
|
+
const code = await readFile(filePath, "utf8");
|
|
243
|
+
return { code, filesAudited: [filePath] };
|
|
244
|
+
}
|
|
245
|
+
async function saveReport(auditId, report) {
|
|
246
|
+
await mkdir(REPORTS_DIR, { recursive: true });
|
|
247
|
+
const reportPath = join(REPORTS_DIR, `${auditId}.json`);
|
|
248
|
+
await writeFile(reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
249
|
+
return reportPath;
|
|
250
|
+
}
|
|
209
251
|
var server = new McpServer({
|
|
210
252
|
name: process.env.MCP_SERVER_NAME || "auditor-mcp",
|
|
211
253
|
version: process.env.MCP_SERVER_VERSION || "0.1.0"
|
|
@@ -213,30 +255,36 @@ var server = new McpServer({
|
|
|
213
255
|
server.tool(
|
|
214
256
|
"audit_soroban_contract",
|
|
215
257
|
[
|
|
216
|
-
"Reads a local Soroban smart contract
|
|
217
|
-
"
|
|
218
|
-
"
|
|
258
|
+
"Reads a local Soroban smart contract and submits it for a paid AI security audit.",
|
|
259
|
+
"Cost: 0.15 USDC per audit, charged autonomously on Stellar Testnet via the x402 protocol.",
|
|
260
|
+
"Accepts a single .rs file OR a project directory (all .rs files are included automatically).",
|
|
261
|
+
"Returns a structured report with CWE IDs, severity levels, fix recommendations,",
|
|
262
|
+
"a unique audit ID, and a downloadable report saved to ~/.auditor-mcp/reports/."
|
|
219
263
|
].join(" "),
|
|
220
264
|
{
|
|
221
|
-
file_path: z.string().describe(
|
|
265
|
+
file_path: z.string().describe(
|
|
266
|
+
"Absolute path to a Soroban .rs file, OR a directory containing Soroban contract source files. When a directory is provided, all .rs files are discovered recursively and audited together."
|
|
267
|
+
)
|
|
222
268
|
},
|
|
223
269
|
async ({ file_path }) => {
|
|
224
270
|
let code;
|
|
271
|
+
let filesAudited;
|
|
225
272
|
try {
|
|
226
|
-
code = await
|
|
273
|
+
({ code, filesAudited } = await loadContractCode(file_path));
|
|
227
274
|
} catch (err) {
|
|
228
275
|
const message = err instanceof Error ? err.message : String(err);
|
|
229
276
|
return {
|
|
230
|
-
content: [{ type: "text", text: `Failed to read
|
|
277
|
+
content: [{ type: "text", text: `Failed to read "${file_path}": ${message}` }],
|
|
231
278
|
isError: true
|
|
232
279
|
};
|
|
233
280
|
}
|
|
234
281
|
if (code.trim().length === 0) {
|
|
235
282
|
return {
|
|
236
|
-
content: [{ type: "text", text: `
|
|
283
|
+
content: [{ type: "text", text: `No contract code found at "${file_path}".` }],
|
|
237
284
|
isError: true
|
|
238
285
|
};
|
|
239
286
|
}
|
|
287
|
+
const auditId = randomUUID();
|
|
240
288
|
let response;
|
|
241
289
|
try {
|
|
242
290
|
response = await fetchWithPayment(AUDIT_GATEWAY_URL, {
|
|
@@ -298,23 +346,28 @@ ${rawBody}` }],
|
|
|
298
346
|
}
|
|
299
347
|
}
|
|
300
348
|
const summary = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"].filter((s) => counts[s]).map((s) => `${s}: ${counts[s]}`).join(" | ");
|
|
349
|
+
const output = {
|
|
350
|
+
auditId,
|
|
351
|
+
file: file_path,
|
|
352
|
+
filesAudited,
|
|
353
|
+
protocol: "x402 / Stellar Testnet",
|
|
354
|
+
walletAddress: signer.address,
|
|
355
|
+
stellarTxUrl,
|
|
356
|
+
model: report?.model,
|
|
357
|
+
summary: summary || "No vulnerabilities found",
|
|
358
|
+
findings,
|
|
359
|
+
reasoning: report?.reasoning ?? null
|
|
360
|
+
};
|
|
361
|
+
let reportFile = null;
|
|
362
|
+
try {
|
|
363
|
+
reportFile = await saveReport(auditId, output);
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
301
366
|
return {
|
|
302
367
|
content: [
|
|
303
368
|
{
|
|
304
369
|
type: "text",
|
|
305
|
-
text: JSON.stringify(
|
|
306
|
-
{
|
|
307
|
-
file: file_path,
|
|
308
|
-
protocol: "x402 / Stellar Testnet",
|
|
309
|
-
walletAddress: signer.address,
|
|
310
|
-
stellarTxUrl,
|
|
311
|
-
model: report?.model,
|
|
312
|
-
summary: summary || "No vulnerabilities found",
|
|
313
|
-
findings
|
|
314
|
-
},
|
|
315
|
-
null,
|
|
316
|
-
2
|
|
317
|
-
)
|
|
370
|
+
text: JSON.stringify({ ...output, reportFile }, null, 2)
|
|
318
371
|
}
|
|
319
372
|
]
|
|
320
373
|
};
|
|
@@ -323,30 +376,36 @@ ${rawBody}` }],
|
|
|
323
376
|
server.tool(
|
|
324
377
|
"audit_soroban_contract_mpp",
|
|
325
378
|
[
|
|
326
|
-
"Reads a local Soroban smart contract
|
|
327
|
-
"
|
|
328
|
-
"
|
|
379
|
+
"Reads a local Soroban smart contract and submits it for a paid AI security audit.",
|
|
380
|
+
"Cost: 0.15 USDC per audit, charged autonomously on Stellar Testnet via the Stripe Machine Payments Protocol (MPP).",
|
|
381
|
+
"Accepts a single .rs file OR a project directory (all .rs files are included automatically).",
|
|
382
|
+
"Returns a structured report with CWE IDs, severity levels, fix recommendations,",
|
|
383
|
+
"a unique audit ID, and a downloadable report saved to ~/.auditor-mcp/reports/."
|
|
329
384
|
].join(" "),
|
|
330
385
|
{
|
|
331
|
-
file_path: z.string().describe(
|
|
386
|
+
file_path: z.string().describe(
|
|
387
|
+
"Absolute path to a Soroban .rs file, OR a directory containing Soroban contract source files. When a directory is provided, all .rs files are discovered recursively and audited together."
|
|
388
|
+
)
|
|
332
389
|
},
|
|
333
390
|
async ({ file_path }) => {
|
|
334
391
|
let code;
|
|
392
|
+
let filesAudited;
|
|
335
393
|
try {
|
|
336
|
-
code = await
|
|
394
|
+
({ code, filesAudited } = await loadContractCode(file_path));
|
|
337
395
|
} catch (err) {
|
|
338
396
|
const message = err instanceof Error ? err.message : String(err);
|
|
339
397
|
return {
|
|
340
|
-
content: [{ type: "text", text: `Failed to read
|
|
398
|
+
content: [{ type: "text", text: `Failed to read "${file_path}": ${message}` }],
|
|
341
399
|
isError: true
|
|
342
400
|
};
|
|
343
401
|
}
|
|
344
402
|
if (code.trim().length === 0) {
|
|
345
403
|
return {
|
|
346
|
-
content: [{ type: "text", text: `
|
|
404
|
+
content: [{ type: "text", text: `No contract code found at "${file_path}".` }],
|
|
347
405
|
isError: true
|
|
348
406
|
};
|
|
349
407
|
}
|
|
408
|
+
const auditId = randomUUID();
|
|
350
409
|
let response;
|
|
351
410
|
try {
|
|
352
411
|
response = await mppClient.fetch(MPP_GATEWAY_URL, {
|
|
@@ -409,23 +468,28 @@ ${rawBody}` }],
|
|
|
409
468
|
}
|
|
410
469
|
}
|
|
411
470
|
const summary = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"].filter((s) => counts[s]).map((s) => `${s}: ${counts[s]}`).join(" | ");
|
|
471
|
+
const output = {
|
|
472
|
+
auditId,
|
|
473
|
+
file: file_path,
|
|
474
|
+
filesAudited,
|
|
475
|
+
protocol: "Stripe MPP / Stellar Testnet",
|
|
476
|
+
walletAddress: signer.address,
|
|
477
|
+
stellarTxUrl: mppStellarTxUrl,
|
|
478
|
+
model: report?.model,
|
|
479
|
+
summary: summary || "No vulnerabilities found",
|
|
480
|
+
findings,
|
|
481
|
+
reasoning: report?.reasoning ?? null
|
|
482
|
+
};
|
|
483
|
+
let reportFile = null;
|
|
484
|
+
try {
|
|
485
|
+
reportFile = await saveReport(auditId, output);
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
412
488
|
return {
|
|
413
489
|
content: [
|
|
414
490
|
{
|
|
415
491
|
type: "text",
|
|
416
|
-
text: JSON.stringify(
|
|
417
|
-
{
|
|
418
|
-
file: file_path,
|
|
419
|
-
protocol: "Stripe MPP / Stellar Testnet",
|
|
420
|
-
walletAddress: signer.address,
|
|
421
|
-
stellarTxUrl: mppStellarTxUrl,
|
|
422
|
-
model: report?.model,
|
|
423
|
-
summary: summary || "No vulnerabilities found",
|
|
424
|
-
findings
|
|
425
|
-
},
|
|
426
|
-
null,
|
|
427
|
-
2
|
|
428
|
-
)
|
|
492
|
+
text: JSON.stringify({ ...output, reportFile }, null, 2)
|
|
429
493
|
}
|
|
430
494
|
]
|
|
431
495
|
};
|