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.
Files changed (2) hide show
  1. package/dist/index.js +106 -42
  2. 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 (.rs file) and submits it for a paid security audit.",
217
- "Charges 0.15 USDC on Stellar Testnet via the x402 protocol.",
218
- "Returns a structured audit report with vulnerabilities, warnings, and recommendations."
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("Absolute or relative path to the Soroban .rs contract file to audit")
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 readFile(file_path, "utf8");
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 file "${file_path}": ${message}` }],
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: `File "${file_path}" is empty.` }],
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 (.rs file) and submits it for a paid security audit.",
327
- "Charges 0.15 USDC on Stellar Testnet via the Stripe Machine Payments Protocol (MPP).",
328
- "Returns a structured audit report with vulnerabilities, CWE IDs, severity levels, and fixes."
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("Absolute or relative path to the Soroban .rs contract file to audit")
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 readFile(file_path, "utf8");
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 file "${file_path}": ${message}` }],
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: `File "${file_path}" is empty.` }],
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "MCP server that audits Soroban smart contracts via autonomous x402 / Stripe MPP payments on Stellar Testnet",
6
6
  "keywords": [