mobbdev 1.0.106 → 1.0.108

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.mjs +923 -396
  2. package/package.json +8 -8
package/dist/index.mjs CHANGED
@@ -540,6 +540,9 @@ var FixDetailsFragmentDoc = `
540
540
  vulnerability_report_issue_tag_value
541
541
  }
542
542
  }
543
+ sharedState {
544
+ id
545
+ }
543
546
  patchAndQuestions {
544
547
  __typename
545
548
  ... on FixData {
@@ -1062,19 +1065,33 @@ var AutoPrAnalysisDocument = `
1062
1065
  var GetLatestReportByRepoUrlDocument = `
1063
1066
  query GetLatestReportByRepoUrl($repoUrl: String!, $filters: fix_bool_exp = {}, $limit: Int!, $offset: Int!) {
1064
1067
  fixReport(
1065
- where: {repo: {originalUrl: {_eq: $repoUrl}}}
1068
+ where: {_and: [{repo: {originalUrl: {_eq: $repoUrl}}}, {state: {_eq: Finished}}]}
1066
1069
  order_by: {createdOn: desc}
1067
1070
  limit: 1
1068
1071
  ) {
1069
1072
  ...FixReportSummaryFields
1070
1073
  }
1074
+ expiredReport: fixReport(
1075
+ where: {_and: [{repo: {originalUrl: {_eq: $repoUrl}}}, {state: {_eq: Expired}}]}
1076
+ order_by: {createdOn: desc}
1077
+ limit: 1
1078
+ ) {
1079
+ id
1080
+ expirationOn
1081
+ }
1071
1082
  }
1072
1083
  ${FixReportSummaryFieldsFragmentDoc}`;
1073
1084
  var GetReportFixesDocument = `
1074
1085
  query GetReportFixes($reportId: uuid!, $filters: fix_bool_exp = {}, $limit: Int!, $offset: Int!) {
1075
- fixReport(where: {id: {_eq: $reportId}}) {
1086
+ fixReport(where: {_and: [{id: {_eq: $reportId}}, {state: {_eq: Finished}}]}) {
1076
1087
  ...FixReportSummaryFields
1077
1088
  }
1089
+ expiredReport: fixReport(
1090
+ where: {_and: [{id: {_eq: $reportId}}, {state: {_eq: Expired}}]}
1091
+ ) {
1092
+ id
1093
+ expirationOn
1094
+ }
1078
1095
  }
1079
1096
  ${FixReportSummaryFieldsFragmentDoc}`;
1080
1097
  var defaultWrapper = (action, _operationName, _operationType, _variables) => action();
@@ -4430,9 +4447,15 @@ import { z as z15 } from "zod";
4430
4447
  var EnvVariablesZod = z15.object({
4431
4448
  GITLAB_API_TOKEN: z15.string().optional(),
4432
4449
  GITHUB_API_TOKEN: z15.string().optional(),
4433
- GIT_PROXY_HOST: z15.string().optional().default("http://tinyproxy:8888")
4450
+ GIT_PROXY_HOST: z15.string().optional().default("http://tinyproxy:8888"),
4451
+ MAX_UPLOAD_FILE_SIZE_MB: z15.coerce.number().gt(0).default(5)
4434
4452
  });
4435
- var { GITLAB_API_TOKEN, GITHUB_API_TOKEN, GIT_PROXY_HOST } = EnvVariablesZod.parse(process.env);
4453
+ var {
4454
+ GITLAB_API_TOKEN,
4455
+ GITHUB_API_TOKEN,
4456
+ GIT_PROXY_HOST,
4457
+ MAX_UPLOAD_FILE_SIZE_MB
4458
+ } = EnvVariablesZod.parse(process.env);
4436
4459
 
4437
4460
  // src/features/analysis/scm/ado/validation.ts
4438
4461
  import { z as z16 } from "zod";
@@ -5062,6 +5085,18 @@ import { setTimeout as setTimeout2 } from "timers/promises";
5062
5085
  import * as path2 from "path";
5063
5086
  import { simpleGit } from "simple-git";
5064
5087
 
5088
+ // src/mcp/core/configs.ts
5089
+ var MCP_DEFAULT_API_URL = "https://api.mobb.ai/v1/graphql";
5090
+ var MCP_API_KEY_HEADER_NAME = "x-mobb-key";
5091
+ var MCP_LOGIN_MAX_WAIT = 10 * 60 * 1e3;
5092
+ var MCP_LOGIN_CHECK_DELAY = 1 * 1e3;
5093
+ var MCP_VUL_REPORT_DIGEST_TIMEOUT_MS = 5 * 60 * 1e3;
5094
+ var MCP_MAX_FILE_SIZE = MAX_UPLOAD_FILE_SIZE_MB * 1024 * 1024;
5095
+ var MCP_PERIODIC_CHECK_INTERVAL = 15 * 60 * 1e3;
5096
+ var MCP_DEFAULT_MAX_FILES_TO_SCAN = 10;
5097
+ var MCP_REPORT_ID_EXPIRATION_MS = 2 * 60 * 60 * 1e3;
5098
+ var MCP_TOOLS_BROWSER_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
5099
+
5065
5100
  // src/features/analysis/scm/FileUtils.ts
5066
5101
  import fs2 from "fs";
5067
5102
  import { isBinary } from "istextorbinary";
@@ -5069,6 +5104,9 @@ import path from "path";
5069
5104
  var EXCLUDED_FILE_PATTERNS = [
5070
5105
  // ... (copy the full array from FilePacking.ts)
5071
5106
  ".json",
5107
+ ".snap",
5108
+ ".env.vault",
5109
+ ".env",
5072
5110
  ".yaml",
5073
5111
  ".yml",
5074
5112
  ".toml",
@@ -5220,16 +5258,24 @@ var FileUtils = class {
5220
5258
  }
5221
5259
  static shouldPackFile(filepath, maxFileSize = 1024 * 1024 * 5) {
5222
5260
  const absoluteFilepath = path.resolve(filepath);
5223
- if (this.isExcludedFileType(filepath)) return false;
5224
- if (!fs2.existsSync(absoluteFilepath)) return false;
5225
- if (fs2.lstatSync(absoluteFilepath).size > maxFileSize) return false;
5261
+ if (this.isExcludedFileType(filepath)) {
5262
+ return false;
5263
+ }
5264
+ if (!fs2.existsSync(absoluteFilepath)) {
5265
+ return false;
5266
+ }
5267
+ if (fs2.lstatSync(absoluteFilepath).size > maxFileSize) {
5268
+ return false;
5269
+ }
5226
5270
  let data;
5227
5271
  try {
5228
5272
  data = fs2.readFileSync(absoluteFilepath);
5229
5273
  } catch {
5230
5274
  return false;
5231
5275
  }
5232
- if (isBinary(null, data)) return false;
5276
+ if (isBinary(null, data)) {
5277
+ return false;
5278
+ }
5233
5279
  return true;
5234
5280
  }
5235
5281
  static getAllFiles(dir, rootDir) {
@@ -5239,7 +5285,7 @@ var FileUtils = class {
5239
5285
  if (relativeDepth > 20) {
5240
5286
  return [];
5241
5287
  }
5242
- if (results.length > 1e5) {
5288
+ if (results.length > 1e3) {
5243
5289
  return [];
5244
5290
  }
5245
5291
  try {
@@ -5270,10 +5316,14 @@ var FileUtils = class {
5270
5316
  }
5271
5317
  return results;
5272
5318
  }
5273
- static getLastChangedFiles(dir, maxFileSize = 1024 * 1024 * 5, count = 10) {
5319
+ static getLastChangedFiles({
5320
+ dir,
5321
+ maxFileSize,
5322
+ maxFiles = MCP_DEFAULT_MAX_FILES_TO_SCAN
5323
+ }) {
5274
5324
  if (!fs2.existsSync(dir) || !fs2.lstatSync(dir).isDirectory()) return [];
5275
5325
  const files = this.getAllFiles(dir);
5276
- return files.filter((file) => this.shouldPackFile(file.fullPath, maxFileSize)).sort((a, b) => b.time - a.time).slice(0, count).map((file) => file.relativePath);
5326
+ return files.filter((file) => this.shouldPackFile(file.fullPath, maxFileSize)).sort((a, b) => b.time - a.time).slice(0, maxFiles).map((file) => file.relativePath);
5277
5327
  }
5278
5328
  };
5279
5329
 
@@ -5322,7 +5372,10 @@ var GitService = class {
5322
5372
  gitRoot,
5323
5373
  this.repositoryPath
5324
5374
  );
5325
- const files = status.files.map((file) => {
5375
+ const deletedFiles = status.files.filter((file) => file.index === "D" || file.working_dir === "D").map((file) => file.path);
5376
+ const files = status.files.filter((file) => {
5377
+ return !(file.index === "D" || file.working_dir === "D");
5378
+ }).map((file) => {
5326
5379
  const gitRelativePath = file.path;
5327
5380
  if (relativePathFromGitRoot === "") {
5328
5381
  return gitRelativePath;
@@ -5339,11 +5392,13 @@ var GitService = class {
5339
5392
  fileCount: files.length,
5340
5393
  files: files.slice(0, 10),
5341
5394
  // Log first 10 files to avoid spam
5395
+ deletedFileCount: deletedFiles.length,
5396
+ deletedFiles: deletedFiles.slice(0, 10),
5342
5397
  gitRoot,
5343
5398
  workingDir: this.repositoryPath,
5344
5399
  relativePathFromGitRoot
5345
5400
  });
5346
- return { files, status };
5401
+ return { files, deletedFiles, status };
5347
5402
  } catch (error) {
5348
5403
  const errorMessage = `Failed to get git status: ${error.message}`;
5349
5404
  this.log(errorMessage, "error", { error });
@@ -5468,11 +5523,13 @@ var GitService = class {
5468
5523
  }
5469
5524
  }
5470
5525
  /**
5471
- * Gets the 10 most recently changed files based on commit history
5526
+ * Gets the maxFiles most recently changed files based on commit history
5472
5527
  */
5473
- async getRecentlyChangedFiles() {
5528
+ async getRecentlyChangedFiles({
5529
+ maxFiles = MCP_DEFAULT_MAX_FILES_TO_SCAN
5530
+ }) {
5474
5531
  this.log(
5475
- "Getting the 10 most recently changed files from commit history",
5532
+ `Getting the ${maxFiles} most recently changed files from commit history`,
5476
5533
  "debug"
5477
5534
  );
5478
5535
  try {
@@ -5485,19 +5542,18 @@ var GitService = class {
5485
5542
  const files = [];
5486
5543
  let commitsProcessed = 0;
5487
5544
  const logResult = await this.git.log({
5488
- maxCount: 100,
5489
- // Get last 100 commits - should be enough to find 10 unique files
5545
+ maxCount: maxFiles * 5,
5546
+ // 5 times the max files to scan to ensure we find enough files
5490
5547
  format: {
5491
5548
  hash: "%H",
5492
5549
  date: "%ai",
5493
5550
  message: "%s",
5494
5551
  //the field name author_name can't follow the naming convention as we are using the git log command
5495
- // eslint-disable-next-line @typescript-eslint/naming-convention
5496
5552
  author_name: "%an"
5497
5553
  }
5498
5554
  });
5499
5555
  for (const commit of logResult.all) {
5500
- if (files.length >= 10) {
5556
+ if (files.length >= maxFiles) {
5501
5557
  break;
5502
5558
  }
5503
5559
  commitsProcessed++;
@@ -5509,7 +5565,7 @@ var GitService = class {
5509
5565
  ]);
5510
5566
  const commitFiles = filesOutput.split("\n").filter((file) => file.trim() !== "");
5511
5567
  for (const file of commitFiles) {
5512
- if (files.length >= 10) {
5568
+ if (files.length >= maxFiles) {
5513
5569
  break;
5514
5570
  }
5515
5571
  const gitRelativePath = file.trim();
@@ -5527,7 +5583,7 @@ var GitService = class {
5527
5583
  );
5528
5584
  }
5529
5585
  this.log(`Considering file: ${adjustedPath}`, "debug");
5530
- if (!fileSet.has(adjustedPath) && FileUtils.shouldPackFile(path2.join(gitRoot, gitRelativePath))) {
5586
+ if (!fileSet.has(adjustedPath) && FileUtils.shouldPackFile(path2.join(gitRoot, gitRelativePath)) && !adjustedPath.startsWith("..")) {
5531
5587
  fileSet.add(adjustedPath);
5532
5588
  files.push(adjustedPath);
5533
5589
  }
@@ -5542,8 +5598,8 @@ var GitService = class {
5542
5598
  fileCount: files.length,
5543
5599
  commitsProcessed,
5544
5600
  totalCommitsAvailable: logResult.all.length,
5545
- files: files.slice(0, 10),
5546
- // Log the files (should be all of them since we limit to 10)
5601
+ files: files.slice(0, maxFiles),
5602
+ // Log the files (should be all of them since we limit to maxFiles)
5547
5603
  gitRoot,
5548
5604
  workingDir: this.repositoryPath,
5549
5605
  relativePathFromGitRoot
@@ -10046,8 +10102,8 @@ async function forkSnyk(args, { display }) {
10046
10102
  }
10047
10103
  async function getSnykReport(reportPath, repoRoot, { skipPrompts = false }) {
10048
10104
  debug15("get snyk report start %s %s", reportPath, repoRoot);
10049
- const config5 = await forkSnyk(["config"], { display: false });
10050
- const { message: configMessage } = config5;
10105
+ const config4 = await forkSnyk(["config"], { display: false });
10106
+ const { message: configMessage } = config4;
10051
10107
  if (!configMessage.includes("api: ")) {
10052
10108
  const snykLoginSpinner = createSpinner3().start();
10053
10109
  if (!skipPrompts) {
@@ -10059,7 +10115,7 @@ async function getSnykReport(reportPath, repoRoot, { skipPrompts = false }) {
10059
10115
  snykLoginSpinner.update({
10060
10116
  text: "\u{1F513} Waiting for Snyk login to complete"
10061
10117
  });
10062
- debug15("no token in the config %s", config5);
10118
+ debug15("no token in the config %s", config4);
10063
10119
  await forkSnyk(["auth"], { display: true });
10064
10120
  snykLoginSpinner.success({ text: "\u{1F513} Login to Snyk Successful" });
10065
10121
  }
@@ -11222,7 +11278,8 @@ var Logger = class {
11222
11278
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11223
11279
  level: logEntry.level,
11224
11280
  message: logEntry.message,
11225
- data: logEntry.data
11281
+ data: logEntry.data,
11282
+ version: packageJson.version
11226
11283
  };
11227
11284
  const controller = new AbortController();
11228
11285
  const timeoutId = setTimeout(() => {
@@ -11275,11 +11332,7 @@ import { GraphQLClient as GraphQLClient2 } from "graphql-request";
11275
11332
  import open4 from "open";
11276
11333
  import { v4 as uuidv42 } from "uuid";
11277
11334
 
11278
- // src/mcp/constants.ts
11279
- var DEFAULT_API_URL2 = "https://api.mobb.ai/v1/graphql";
11280
- var API_KEY_HEADER_NAME2 = "x-mobb-key";
11281
-
11282
- // src/mcp/tools/fixVulnerabilities/errors/VulnerabilityFixErrors.ts
11335
+ // src/mcp/core/Errors.ts
11283
11336
  var ApiConnectionError = class extends Error {
11284
11337
  constructor(message = "Failed to connect to the API") {
11285
11338
  super(message);
@@ -11342,20 +11395,17 @@ var FailedToGetApiTokenError = class extends Error {
11342
11395
  };
11343
11396
 
11344
11397
  // src/mcp/services/McpGQLClient.ts
11345
- var LOGIN_MAX_WAIT2 = 10 * 1e3;
11346
- var LOGIN_CHECK_DELAY2 = 1 * 1e3;
11347
- var config4 = new Configstore3(packageJson.name, { apiToken: "" });
11348
- var BROWSER_COOLDOWN_MS = 5e3;
11349
- var lastBrowserOpenTime = 0;
11398
+ var mobbConfigStore = new Configstore3(packageJson.name, { apiToken: "" });
11350
11399
  var McpGQLClient = class {
11351
11400
  constructor(args) {
11352
11401
  __publicField(this, "client");
11353
11402
  __publicField(this, "clientSdk");
11354
11403
  __publicField(this, "_auth");
11355
11404
  this._auth = args;
11356
- const API_URL2 = process.env["API_URL"] || DEFAULT_API_URL2;
11405
+ const API_URL2 = process.env["API_URL"] || MCP_DEFAULT_API_URL;
11406
+ logDebug("creating graphql client", { API_URL: API_URL2, args });
11357
11407
  this.client = new GraphQLClient2(API_URL2, {
11358
- headers: args.type === "apiKey" ? { [API_KEY_HEADER_NAME2]: args.apiKey || "" } : {
11408
+ headers: args.type === "apiKey" ? { [MCP_API_KEY_HEADER_NAME]: args.apiKey || "" } : {
11359
11409
  Authorization: `Bearer ${args.token}`
11360
11410
  },
11361
11411
  requestMiddleware: (request) => {
@@ -11373,10 +11423,10 @@ var McpGQLClient = class {
11373
11423
  }
11374
11424
  getErrorContext() {
11375
11425
  return {
11376
- endpoint: process.env["API_URL"] || DEFAULT_API_URL2,
11426
+ endpoint: process.env["API_URL"] || MCP_DEFAULT_API_URL,
11377
11427
  apiKey: this._auth.type === "apiKey" ? this._auth.apiKey : "",
11378
11428
  headers: {
11379
- [API_KEY_HEADER_NAME2]: this._auth.type === "apiKey" ? "[REDACTED]" : "undefined",
11429
+ [MCP_API_KEY_HEADER_NAME]: this._auth.type === "apiKey" ? "[REDACTED]" : "undefined",
11380
11430
  "x-hasura-request-id": "[DYNAMIC]"
11381
11431
  }
11382
11432
  };
@@ -11385,11 +11435,13 @@ var McpGQLClient = class {
11385
11435
  try {
11386
11436
  logDebug("GraphQL: Calling Me query for connection verification");
11387
11437
  const result = await this.clientSdk.Me();
11388
- logInfo("GraphQL: Me query successful", { result });
11438
+ logDebug("GraphQL: Me query successful", { result });
11389
11439
  return true;
11390
11440
  } catch (e) {
11391
- if (e?.toString().includes("FetchError")) {
11392
- logError("verify connection failed %o", e);
11441
+ const error = e;
11442
+ logDebug(`verify connection failed ${error.toString()}`);
11443
+ if (error?.toString().includes("FetchError")) {
11444
+ logError("verify connection failed", { error });
11393
11445
  return false;
11394
11446
  }
11395
11447
  }
@@ -11561,7 +11613,7 @@ var McpGQLClient = class {
11561
11613
  try {
11562
11614
  const res = await this.clientSdk.CreateCliLogin(variables, {
11563
11615
  // We may have outdated API key in the config storage. Avoid using it for the login request.
11564
- [API_KEY_HEADER_NAME2]: ""
11616
+ [MCP_API_KEY_HEADER_NAME]: ""
11565
11617
  });
11566
11618
  const loginId = res.insert_cli_login_one?.id || "";
11567
11619
  if (!loginId) {
@@ -11578,7 +11630,7 @@ var McpGQLClient = class {
11578
11630
  try {
11579
11631
  const res = await this.clientSdk.GetEncryptedApiToken(variables, {
11580
11632
  // We may have outdated API key in the config storage. Avoid using it for the login request.
11581
- [API_KEY_HEADER_NAME2]: ""
11633
+ [MCP_API_KEY_HEADER_NAME]: ""
11582
11634
  });
11583
11635
  return res?.cli_login_by_pk?.encryptedApiToken || null;
11584
11636
  } catch (e) {
@@ -11606,7 +11658,10 @@ var McpGQLClient = class {
11606
11658
  result: res,
11607
11659
  reportCount: res.fixReport?.length || 0
11608
11660
  });
11609
- return res.fixReport?.[0] || null;
11661
+ return {
11662
+ fixReport: res.fixReport?.[0] || null,
11663
+ expiredReport: res.expiredReport?.[0] || null
11664
+ };
11610
11665
  } catch (e) {
11611
11666
  logError("GraphQL: GetLatestReportByRepoUrl failed", {
11612
11667
  error: e,
@@ -11655,7 +11710,8 @@ var McpGQLClient = class {
11655
11710
  }
11656
11711
  return {
11657
11712
  fixes: res.fixReport?.[0]?.fixes || [],
11658
- totalCount: res.fixReport?.[0]?.filteredFixesCount?.aggregate?.count || 0
11713
+ totalCount: res.fixReport?.[0]?.filteredFixesCount?.aggregate?.count || 0,
11714
+ expiredReport: res.expiredReport?.[0] || null
11659
11715
  };
11660
11716
  } catch (e) {
11661
11717
  logError("GraphQL: GetReportFixes failed", {
@@ -11667,30 +11723,39 @@ var McpGQLClient = class {
11667
11723
  }
11668
11724
  }
11669
11725
  };
11670
- async function openBrowser(url) {
11671
- const now = Date.now();
11672
- if (!process.env["TEST"] && now - lastBrowserOpenTime < BROWSER_COOLDOWN_MS) {
11673
- logDebug(`browser cooldown active, skipping open for ${url}`);
11674
- return;
11726
+ async function openBrowser(url, isToolsCall) {
11727
+ if (isToolsCall) {
11728
+ const now = Date.now();
11729
+ const lastBrowserOpenTime = mobbConfigStore.get("lastBrowserOpenTime") || 0;
11730
+ if (now - lastBrowserOpenTime < MCP_TOOLS_BROWSER_COOLDOWN_MS) {
11731
+ logDebug(`browser cooldown active, skipping open for ${url}`);
11732
+ return;
11733
+ }
11675
11734
  }
11676
11735
  logDebug(`opening browser url ${url}`);
11677
11736
  await open4(url);
11678
- lastBrowserOpenTime = now;
11737
+ mobbConfigStore.set("lastBrowserOpenTime", Date.now());
11679
11738
  }
11680
- async function getMcpGQLClient() {
11681
- logDebug("getting config", { apiToken: config4.get("apiToken") });
11739
+ async function getMcpGQLClient({
11740
+ isToolsCall = false
11741
+ } = {}) {
11742
+ logDebug("getting config", { apiToken: mobbConfigStore.get("apiToken") });
11682
11743
  const inGqlClient = new McpGQLClient({
11683
- apiKey: config4.get("apiToken") || process.env["API_KEY"] || "",
11744
+ apiKey: process.env["MOBB_API_KEY"] || process.env["API_KEY"] || // fallback for backward compatibility
11745
+ mobbConfigStore.get("apiToken") || "",
11684
11746
  type: "apiKey"
11685
11747
  });
11686
11748
  const isConnected = await inGqlClient.verifyConnection();
11749
+ logDebug("isConnected", { isConnected });
11687
11750
  if (!isConnected) {
11688
11751
  throw new ApiConnectionError("Error: failed to connect to Mobb API");
11689
11752
  }
11753
+ logDebug("verifying token");
11690
11754
  const userVerify = await inGqlClient.verifyToken();
11691
11755
  if (userVerify) {
11692
11756
  return inGqlClient;
11693
11757
  }
11758
+ logDebug("verifying token failed");
11694
11759
  const { publicKey, privateKey } = crypto2.generateKeyPairSync("rsa", {
11695
11760
  modulusLength: 2048
11696
11761
  });
@@ -11705,10 +11770,10 @@ async function getMcpGQLClient() {
11705
11770
  const webLoginUrl2 = `${WEB_APP_URL}/cli-login`;
11706
11771
  const browserUrl = `${webLoginUrl2}/${loginId}?hostname=${os2.hostname()}`;
11707
11772
  logDebug(`opening browser url ${browserUrl}`);
11708
- await openBrowser(browserUrl);
11773
+ await openBrowser(browserUrl, isToolsCall);
11709
11774
  logDebug(`waiting for login to complete`);
11710
11775
  let newApiToken = null;
11711
- for (let i = 0; i < LOGIN_MAX_WAIT2 / LOGIN_CHECK_DELAY2; i++) {
11776
+ for (let i = 0; i < MCP_LOGIN_MAX_WAIT / MCP_LOGIN_CHECK_DELAY; i++) {
11712
11777
  const encryptedApiToken = await inGqlClient.getEncryptedApiToken({
11713
11778
  loginId
11714
11779
  });
@@ -11718,7 +11783,7 @@ async function getMcpGQLClient() {
11718
11783
  logDebug("API token decrypted");
11719
11784
  break;
11720
11785
  }
11721
- await sleep(LOGIN_CHECK_DELAY2);
11786
+ await sleep(MCP_LOGIN_CHECK_DELAY);
11722
11787
  }
11723
11788
  if (!newApiToken) {
11724
11789
  throw new FailedToGetApiTokenError(
@@ -11729,7 +11794,7 @@ async function getMcpGQLClient() {
11729
11794
  const loginSuccess = await newGqlClient.verifyToken();
11730
11795
  if (loginSuccess) {
11731
11796
  logDebug(`set api token ${newApiToken}`);
11732
- config4.set("apiToken", newApiToken);
11797
+ mobbConfigStore.set("apiToken", newApiToken);
11733
11798
  } else {
11734
11799
  throw new AuthenticationError("Invalid API token");
11735
11800
  }
@@ -11772,14 +11837,14 @@ var ToolRegistry = class {
11772
11837
 
11773
11838
  // src/mcp/core/McpServer.ts
11774
11839
  var McpServer = class {
11775
- constructor(config5) {
11840
+ constructor(config4) {
11776
11841
  __publicField(this, "server");
11777
11842
  __publicField(this, "toolRegistry");
11778
11843
  __publicField(this, "isEventHandlersSetup", false);
11779
11844
  this.server = new Server(
11780
11845
  {
11781
- name: config5.name,
11782
- version: config5.version
11846
+ name: config4.name,
11847
+ version: config4.version
11783
11848
  },
11784
11849
  {
11785
11850
  capabilities: {
@@ -11790,7 +11855,7 @@ var McpServer = class {
11790
11855
  this.toolRegistry = new ToolRegistry();
11791
11856
  this.setupHandlers();
11792
11857
  this.setupProcessEventHandlers();
11793
- logInfo("MCP server instance created", config5);
11858
+ logInfo("MCP server instance created", config4);
11794
11859
  }
11795
11860
  setupProcessEventHandlers() {
11796
11861
  if (this.isEventHandlersSetup) {
@@ -11838,16 +11903,10 @@ var McpServer = class {
11838
11903
  }
11839
11904
  async handleListToolsRequest(request) {
11840
11905
  logInfo("Received list_tools request", { params: request.params });
11841
- logInfo("Environment", {
11842
- env: process.env
11843
- });
11844
11906
  logInfo("Request", {
11845
11907
  request: JSON.parse(JSON.stringify(request))
11846
11908
  });
11847
- logInfo("Server", {
11848
- server: this.server
11849
- });
11850
- void getMcpGQLClient();
11909
+ void getMcpGQLClient({ isToolsCall: true });
11851
11910
  const toolsDefinitions = this.toolRegistry.getAllTools();
11852
11911
  const response = {
11853
11912
  tools: toolsDefinitions.map((tool) => ({
@@ -11867,15 +11926,9 @@ var McpServer = class {
11867
11926
  async handleCallToolRequest(request) {
11868
11927
  const { name, arguments: args } = request.params;
11869
11928
  logInfo(`Received call tool request for ${name}`, { name, args });
11870
- logInfo("Environment", {
11871
- env: process.env
11872
- });
11873
11929
  logInfo("Request", {
11874
11930
  request: JSON.parse(JSON.stringify(request))
11875
11931
  });
11876
- logInfo("Server", {
11877
- server: this.server
11878
- });
11879
11932
  try {
11880
11933
  const tool = this.toolRegistry.getTool(name);
11881
11934
  if (!tool) {
@@ -11938,53 +11991,74 @@ var McpServer = class {
11938
11991
  }
11939
11992
  };
11940
11993
 
11941
- // src/mcp/tools/checkForAvailableFixes/AvailableFixesTool.ts
11994
+ // src/mcp/tools/checkForNewAvailableFixes/CheckForNewAvailableFixesTool.ts
11942
11995
  import { z as z32 } from "zod";
11943
11996
 
11944
11997
  // src/mcp/services/PathValidation.ts
11945
11998
  import fs9 from "fs";
11946
11999
  import path11 from "path";
11947
- var PathValidation = class {
11948
- /**
11949
- * Validates a path for MCP usage - combines security and existence checks
11950
- */
11951
- async validatePath(inputPath) {
11952
- logDebug("Validating MCP path", { inputPath });
11953
- if (inputPath.includes("..")) {
11954
- const error = `Path contains path traversal patterns: ${inputPath}`;
11955
- logError(error);
11956
- return { isValid: false, error };
11957
- }
11958
- const normalizedPath = path11.normalize(inputPath);
11959
- if (normalizedPath.includes("..")) {
11960
- const error = `Normalized path contains path traversal patterns: ${inputPath}`;
11961
- logError(error);
11962
- return { isValid: false, error };
11963
- }
11964
- const decodedPath = decodeURIComponent(inputPath);
11965
- if (decodedPath.includes("..") || decodedPath !== inputPath) {
11966
- const error = `Path contains encoded traversal attempts: ${inputPath}`;
11967
- logError(error);
11968
- return { isValid: false, error };
11969
- }
11970
- if (inputPath.includes("\0") || inputPath.includes("\0")) {
11971
- const error = `Path contains dangerous characters: ${inputPath}`;
12000
+ async function validatePath(inputPath) {
12001
+ logDebug("Validating MCP path", { inputPath });
12002
+ if (/^\/[a-zA-Z]:\//.test(inputPath)) {
12003
+ inputPath = inputPath.slice(1);
12004
+ }
12005
+ if (inputPath === "." || inputPath === "./") {
12006
+ if (process.env["WORKSPACE_FOLDER_PATHS"]) {
12007
+ logDebug("Fallback to workspace folder path", {
12008
+ inputPath,
12009
+ workspaceFolderPaths: process.env["WORKSPACE_FOLDER_PATHS"]
12010
+ });
12011
+ return {
12012
+ isValid: true,
12013
+ path: process.env["WORKSPACE_FOLDER_PATHS"]
12014
+ };
12015
+ } else {
12016
+ const error = `"." is not a valid path, please provide a full localpath to the repository`;
11972
12017
  logError(error);
11973
- return { isValid: false, error };
11974
- }
11975
- logDebug("Path validation successful", { inputPath });
11976
- logDebug("Checking path existence", { inputPath });
11977
- try {
11978
- await fs9.promises.access(inputPath);
11979
- logDebug("Path exists and is accessible", { inputPath });
11980
- return { isValid: true };
11981
- } catch (error) {
11982
- const errorMessage = `Path does not exist or is not accessible: ${inputPath}`;
11983
- logError(errorMessage, { error });
11984
- return { isValid: false, error: errorMessage };
12018
+ return { isValid: false, error, path: inputPath };
11985
12019
  }
11986
12020
  }
11987
- };
12021
+ if (inputPath.includes("..")) {
12022
+ const error = `Path contains path traversal patterns: ${inputPath}`;
12023
+ logError(error);
12024
+ return { isValid: false, error, path: inputPath };
12025
+ }
12026
+ const normalizedPath = path11.normalize(inputPath);
12027
+ if (normalizedPath.includes("..")) {
12028
+ const error = `Normalized path contains path traversal patterns: ${inputPath}`;
12029
+ logError(error);
12030
+ return { isValid: false, error, path: inputPath };
12031
+ }
12032
+ let decodedPath;
12033
+ try {
12034
+ decodedPath = decodeURIComponent(inputPath);
12035
+ } catch (err) {
12036
+ const error = `Failed to decode path: ${inputPath}`;
12037
+ logError(error, { err });
12038
+ return { isValid: false, error, path: inputPath };
12039
+ }
12040
+ if (decodedPath.includes("..") || decodedPath !== inputPath) {
12041
+ const error = `Path contains encoded traversal attempts: ${inputPath}`;
12042
+ logError(error);
12043
+ return { isValid: false, error, path: inputPath };
12044
+ }
12045
+ if (inputPath.includes("\0") || inputPath.includes("\0")) {
12046
+ const error = `Path contains dangerous characters: ${inputPath}`;
12047
+ logError(error);
12048
+ return { isValid: false, error, path: inputPath };
12049
+ }
12050
+ logDebug("Path validation successful", { inputPath });
12051
+ logDebug("Checking path existence", { inputPath });
12052
+ try {
12053
+ await fs9.promises.access(inputPath);
12054
+ logDebug("Path exists and is accessible", { inputPath });
12055
+ return { isValid: true, path: inputPath };
12056
+ } catch (error) {
12057
+ const errorMessage = `Path does not exist or is not accessible: ${inputPath}`;
12058
+ logError(errorMessage, { error });
12059
+ return { isValid: false, error: errorMessage, path: inputPath };
12060
+ }
12061
+ }
11988
12062
 
11989
12063
  // src/mcp/tools/base/BaseTool.ts
11990
12064
  import { z as z31 } from "zod";
@@ -11998,7 +12072,6 @@ var BaseTool = class {
11998
12072
  };
11999
12073
  }
12000
12074
  async execute(args) {
12001
- logInfo(`Executing tool: ${this.name}`, { args });
12002
12075
  logInfo(`Authenticating tool: ${this.name}`, { args });
12003
12076
  const mcpGqlClient = await getMcpGQLClient();
12004
12077
  const userInfo = await mcpGqlClient.getUserInfo();
@@ -12053,7 +12126,8 @@ var applyFixesPrompt = ({
12053
12126
  totalCount,
12054
12127
  nextOffset,
12055
12128
  shownCount,
12056
- currentTool
12129
+ currentTool,
12130
+ offset = 0
12057
12131
  }) => {
12058
12132
  if (fixes.length === 0) {
12059
12133
  return noFixesReturnedForParameters;
@@ -12111,7 +12185,7 @@ If you cannot apply a patch:
12111
12185
 
12112
12186
  ${fixList.map(
12113
12187
  (fix, index) => `
12114
- ## Fix ${index + 1}: ${fix.vulnerabilityType}
12188
+ ## Fix ${offset + index + 1}: ${fix.vulnerabilityType}
12115
12189
 
12116
12190
  **\u{1F3AF} Target:** Apply this patch to fix a ${fix.vulnerabilityType.toLowerCase()} vulnerability
12117
12191
 
@@ -12164,7 +12238,7 @@ We were unable to find a previous vulnerability report for this repository. This
12164
12238
 
12165
12239
  ### \u{1F3AF} Recommended Actions
12166
12240
  1. **Run a new security scan** to analyze your codebase
12167
- - Use the \`fix_vulnerabilities\` tool to start a fresh scan
12241
+ - Use the \`scan_and_fix_vulnerabilities\` tool to start a fresh scan
12168
12242
  - This will analyze your current code for security issues
12169
12243
 
12170
12244
  2. **Verify repository access**
@@ -12178,6 +12252,27 @@ We were unable to find a previous vulnerability report for this repository. This
12178
12252
  For assistance, please:
12179
12253
  - Visit our documentation at https://docs.mobb.ai
12180
12254
  - Contact support at support@mobb.ai`;
12255
+ var expiredReportPrompt = ({
12256
+ lastReportDate
12257
+ }) => `\u{1F50D} **MOBB SECURITY SCAN STATUS**
12258
+
12259
+ ## Out-of-Date Vulnerability Report
12260
+
12261
+ Your most recent vulnerability report for this repository **expired on ${lastReportDate}** and is no longer available for fetching automated fixes.
12262
+
12263
+ ### \u{1F4CB} Why Did This Happen?
12264
+ - Reports are automatically purged after a retention period for security and storage optimisation.
12265
+ - No new scans have been run since the last report expired.
12266
+
12267
+ ### \u{1F3AF} Recommended Actions
12268
+ 1. **Run a fresh security scan** to generate an up-to-date vulnerability report.
12269
+ - Use the \`scan_and_fix_vulnerabilities\` tool.
12270
+ 2. **Verify repository access** if scans fail to run or the repository has moved.
12271
+ 3. **Review your CI/CD pipeline** to ensure regular scans are triggered.
12272
+
12273
+ For more help:
12274
+ - Documentation: https://docs.mobb.ai
12275
+ - Support: support@mobb.ai`;
12181
12276
  var noFixesAvailablePrompt = `There are no fixes available for this repository at this time.
12182
12277
  `;
12183
12278
  var fixesFoundPrompt = ({
@@ -12225,20 +12320,49 @@ ${applyFixesPrompt({
12225
12320
  hasMore,
12226
12321
  nextOffset: 0,
12227
12322
  shownCount: fixReport.fixes.length,
12228
- currentTool: "check_for_available_fixes"
12323
+ currentTool: "fetch_available_fixes",
12324
+ offset
12229
12325
  })}`;
12230
12326
  };
12231
- var noFixesFoundPrompt = `\u{1F50D} **MOBB SECURITY SCAN COMPLETED**
12327
+ var nextStepsPrompt = ({ scannedFiles }) => `
12328
+ ### \u{1F4C1} Scanned Files
12329
+ ${scannedFiles.map((file) => `- ${file}`).join("\n")}
12330
+
12331
+ ### Extend the scan scope
12332
+
12333
+ To scan a larger number of files, include the additional parameter:
12334
+
12335
+ - **maxFiles**: <number_of_files_to_scan>
12336
+
12337
+ This will scan up to the specified number of recently changed files.
12338
+
12339
+ ### \u{1F504} Running a Fresh Scan
12340
+
12341
+ To perform a **rescan** of your repository (fetching a brand-new vulnerability report and updated fixes), include the additional parameter:
12342
+
12343
+ - **rescan**: true
12344
+
12345
+ This will start a new analysis, discard any cached results.
12346
+
12347
+ \u26A0\uFE0F *Note:* A full rescan may take longer to complete than simply fetching additional fixes because your repository is re-uploaded and re-analyzed from scratch.
12348
+
12349
+ `;
12350
+ var noFixesFoundPrompt = ({
12351
+ scannedFiles
12352
+ }) => `\u{1F50D} **MOBB SECURITY SCAN COMPLETED**
12232
12353
 
12233
12354
  Mobb security scan completed successfully but found no automated fixes available at this time.
12355
+
12356
+ ${nextStepsPrompt({ scannedFiles })}
12234
12357
  `;
12235
12358
  var fixesPrompt = ({
12236
12359
  fixes,
12237
12360
  totalCount,
12238
- offset
12361
+ offset,
12362
+ scannedFiles
12239
12363
  }) => {
12240
12364
  if (totalCount === 0) {
12241
- return noFixesFoundPrompt;
12365
+ return noFixesFoundPrompt({ scannedFiles });
12242
12366
  }
12243
12367
  const shownCount = fixes.length;
12244
12368
  const nextOffset = offset + shownCount;
@@ -12251,28 +12375,491 @@ ${applyFixesPrompt({
12251
12375
  hasMore,
12252
12376
  nextOffset,
12253
12377
  shownCount,
12254
- currentTool: "fix_vulnerabilities"
12378
+ currentTool: "scan_and_fix_vulnerabilities",
12379
+ offset
12255
12380
  })}
12256
12381
 
12257
- ### \u{1F504} Running a Fresh Scan
12382
+ ${nextStepsPrompt({ scannedFiles })}
12383
+ `;
12384
+ };
12385
+ var noFreshFixesPrompt = `No fresh fixes available for this repository at this time.
12386
+ `;
12387
+ var initialScanInProgressPrompt = `Initial scan in progress. Call the tool again in 1 minute to check for available fixes.`;
12388
+ var freshFixesPrompt = ({ fixes }) => {
12389
+ return `Here are the fresh fixes to the vulnerabilities discovered by Mobb MCP
12258
12390
 
12259
- To perform a **rescan** of your repository (fetching a brand-new vulnerability report and updated fixes), include the additional parameter:
12391
+ ${applyFixesPrompt({
12392
+ fixes,
12393
+ totalCount: fixes.length,
12394
+ hasMore: false,
12395
+ nextOffset: 0,
12396
+ shownCount: fixes.length,
12397
+ currentTool: "fetch_available_fixes",
12398
+ offset: 0
12399
+ })}
12400
+ `;
12401
+ };
12260
12402
 
12261
- - **isRescan**: true
12403
+ // src/mcp/services/GetLocalFiles.ts
12404
+ import fs10 from "fs/promises";
12405
+ import nodePath from "path";
12406
+ var getLocalFiles = async ({
12407
+ path: path13,
12408
+ maxFileSize = 1024 * 1024 * 5,
12409
+ maxFiles
12410
+ }) => {
12411
+ const resolvedRepoPath = await fs10.realpath(path13);
12412
+ const gitService = new GitService(resolvedRepoPath, log);
12413
+ const gitValidation = await gitService.validateRepository();
12414
+ let files = [];
12415
+ if (!gitValidation.isValid) {
12416
+ logDebug(
12417
+ "Git repository validation failed, using all files in the repository",
12418
+ {
12419
+ path: path13
12420
+ }
12421
+ );
12422
+ files = FileUtils.getLastChangedFiles({
12423
+ dir: path13,
12424
+ maxFileSize,
12425
+ maxFiles
12426
+ });
12427
+ logDebug("Found files in the repository", {
12428
+ files,
12429
+ fileCount: files.length
12430
+ });
12431
+ } else {
12432
+ logDebug("maxFiles", {
12433
+ maxFiles
12434
+ });
12435
+ const gitResult = await gitService.getChangedFiles();
12436
+ files = gitResult.files;
12437
+ if (files.length === 0 || maxFiles) {
12438
+ const recentResult = await gitService.getRecentlyChangedFiles({
12439
+ maxFiles
12440
+ });
12441
+ files = recentResult.files;
12442
+ logDebug(
12443
+ "No changes found, using recently changed files from git history",
12444
+ {
12445
+ files,
12446
+ fileCount: files.length,
12447
+ commitsChecked: recentResult.commitCount
12448
+ }
12449
+ );
12450
+ } else {
12451
+ logDebug("Found changed files in the git repository", {
12452
+ files,
12453
+ fileCount: files.length
12454
+ });
12455
+ }
12456
+ }
12457
+ files = files.filter(
12458
+ (file) => FileUtils.shouldPackFile(
12459
+ nodePath.resolve(resolvedRepoPath, file),
12460
+ maxFileSize
12461
+ )
12462
+ );
12463
+ const filesWithStats = await Promise.all(
12464
+ files.map(async (file) => {
12465
+ const absoluteFilePath = nodePath.resolve(resolvedRepoPath, file);
12466
+ const relativePath = nodePath.relative(resolvedRepoPath, absoluteFilePath);
12467
+ let fileStat;
12468
+ try {
12469
+ fileStat = await fs10.stat(absoluteFilePath);
12470
+ } catch (e) {
12471
+ logDebug("File not found", {
12472
+ file
12473
+ });
12474
+ }
12475
+ return {
12476
+ filename: nodePath.basename(absoluteFilePath),
12477
+ relativePath,
12478
+ fullPath: absoluteFilePath,
12479
+ lastEdited: fileStat?.mtime.getTime() ?? 0
12480
+ };
12481
+ })
12482
+ );
12483
+ return filesWithStats.filter((file) => file.lastEdited > 0);
12484
+ };
12262
12485
 
12263
- This will start a new analysis, discard any cached results.
12486
+ // src/mcp/services/ScanFiles.ts
12487
+ import fs11 from "fs";
12488
+ import path12 from "path";
12489
+ import AdmZip2 from "adm-zip";
12490
+ var scanFiles = async (fileList, repositoryPath, gqlClient) => {
12491
+ const repoUploadInfo = await initializeReport(gqlClient);
12492
+ const fixReportId = repoUploadInfo.fixReportId;
12493
+ const zipBuffer = await packFiles(fileList, repositoryPath);
12494
+ await uploadFiles(zipBuffer, repoUploadInfo);
12495
+ const projectId = await getProjectId(gqlClient);
12496
+ await runScan({ fixReportId, projectId, gqlClient });
12497
+ return {
12498
+ fixReportId,
12499
+ projectId
12500
+ };
12501
+ };
12502
+ var initializeReport = async (gqlClient) => {
12503
+ if (!gqlClient) {
12504
+ throw new GqlClientError();
12505
+ }
12506
+ try {
12507
+ const {
12508
+ uploadS3BucketInfo: { repoUploadInfo }
12509
+ } = await gqlClient.uploadS3BucketInfo();
12510
+ logInfo("Upload info retrieved", { uploadKey: repoUploadInfo?.uploadKey });
12511
+ return repoUploadInfo;
12512
+ } catch (error) {
12513
+ const message = error.message;
12514
+ throw new ReportInitializationError(`Error initializing report: ${message}`);
12515
+ }
12516
+ };
12517
+ var packFiles = async (fileList, repositoryPath) => {
12518
+ try {
12519
+ logInfo(`FilePacking: packing files from ${repositoryPath}`);
12520
+ const zip = new AdmZip2();
12521
+ let packedFilesCount = 0;
12522
+ const resolvedRepoPath = path12.resolve(repositoryPath);
12523
+ logInfo("FilePacking: compressing files");
12524
+ for (const filepath of fileList) {
12525
+ const absoluteFilepath = path12.join(repositoryPath, filepath);
12526
+ const resolvedFilePath = path12.resolve(absoluteFilepath);
12527
+ if (!resolvedFilePath.startsWith(resolvedRepoPath)) {
12528
+ logInfo(
12529
+ `FilePacking: skipping ${filepath} due to potential path traversal`
12530
+ );
12531
+ continue;
12532
+ }
12533
+ if (!FileUtils.shouldPackFile(absoluteFilepath, MCP_MAX_FILE_SIZE)) {
12534
+ logInfo(
12535
+ `FilePacking: ignoring ${filepath} because it is excluded or invalid`
12536
+ );
12537
+ continue;
12538
+ }
12539
+ let data;
12540
+ try {
12541
+ data = fs11.readFileSync(absoluteFilepath);
12542
+ } catch (fsError) {
12543
+ logInfo(
12544
+ `FilePacking: failed to read ${filepath} from filesystem: ${fsError}`
12545
+ );
12546
+ continue;
12547
+ }
12548
+ zip.addFile(filepath, data);
12549
+ packedFilesCount++;
12550
+ }
12551
+ const zipBuffer = zip.toBuffer();
12552
+ logInfo(
12553
+ `FilePacking: read ${packedFilesCount} source files. total size: ${zipBuffer.length} bytes`
12554
+ );
12555
+ logInfo("Files packed successfully", { fileCount: fileList.length });
12556
+ return zipBuffer;
12557
+ } catch (error) {
12558
+ const message = error.message;
12559
+ throw new FileProcessingError(`Error packing files: ${message}`);
12560
+ }
12561
+ };
12562
+ var uploadFiles = async (zipBuffer, repoUploadInfo) => {
12563
+ if (!repoUploadInfo) {
12564
+ throw new FileUploadError("Upload info is required");
12565
+ }
12566
+ try {
12567
+ await uploadFile({
12568
+ file: zipBuffer,
12569
+ url: repoUploadInfo.url,
12570
+ uploadFields: JSON.parse(repoUploadInfo.uploadFieldsJSON),
12571
+ uploadKey: repoUploadInfo.uploadKey
12572
+ });
12573
+ logInfo("File uploaded successfully");
12574
+ } catch (error) {
12575
+ logError("File upload failed", { error: error.message });
12576
+ throw new FileUploadError(
12577
+ `Failed to upload the file: ${error.message}`
12578
+ );
12579
+ }
12580
+ };
12581
+ var getProjectId = async (gqlClient) => {
12582
+ if (!gqlClient) {
12583
+ throw new GqlClientError();
12584
+ }
12585
+ const projectId = await gqlClient.getProjectId();
12586
+ logInfo("Project ID retrieved", { projectId });
12587
+ return projectId;
12588
+ };
12589
+ var runScan = async ({
12590
+ fixReportId,
12591
+ projectId,
12592
+ gqlClient
12593
+ }) => {
12594
+ if (!gqlClient) {
12595
+ throw new GqlClientError();
12596
+ }
12597
+ logInfo("Starting scan", { fixReportId, projectId });
12598
+ const submitVulnerabilityReportVariables = {
12599
+ fixReportId,
12600
+ projectId,
12601
+ repoUrl: "",
12602
+ reference: "no-branch",
12603
+ scanSource: "MCP" /* Mcp */
12604
+ };
12605
+ logInfo("Submitting vulnerability report");
12606
+ const submitRes = await gqlClient.submitVulnerabilityReport(
12607
+ submitVulnerabilityReportVariables
12608
+ );
12609
+ if (submitRes.submitVulnerabilityReport.__typename !== "VulnerabilityReport") {
12610
+ logError("Vulnerability report submission failed", {
12611
+ response: submitRes
12612
+ });
12613
+ throw new ScanError("\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed");
12614
+ }
12615
+ logInfo("Vulnerability report submitted successfully", {
12616
+ analysisId: submitRes.submitVulnerabilityReport.fixReportId
12617
+ });
12618
+ logInfo("Starting analysis subscription");
12619
+ await gqlClient.subscribeToGetAnalysis({
12620
+ subscribeToAnalysisParams: {
12621
+ analysisId: submitRes.submitVulnerabilityReport.fixReportId
12622
+ },
12623
+ callback: () => {
12624
+ },
12625
+ callbackStates: ["Finished" /* Finished */],
12626
+ timeoutInMs: MCP_VUL_REPORT_DIGEST_TIMEOUT_MS
12627
+ });
12628
+ logInfo("Analysis subscription completed");
12629
+ };
12264
12630
 
12265
- \u26A0\uFE0F *Note:* A full rescan may take longer to complete than simply fetching additional fixes because your repository is re-uploaded and re-analyzed from scratch.
12631
+ // src/mcp/tools/checkForNewAvailableFixes/CheckForNewAvailableFixesService.ts
12632
+ function extractPathFromPatch(patch) {
12633
+ const match = patch?.match(/^diff --git a\/([^\s]+) b\//);
12634
+ return match?.[1] ?? null;
12635
+ }
12636
+ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService {
12637
+ constructor() {
12638
+ /**
12639
+ * Cache of the last known total number of fixes per repository URL so that we
12640
+ * can determine whether *new* fixes have been generated since the user last
12641
+ * asked.
12642
+ */
12643
+ __publicField(this, "path", "");
12644
+ __publicField(this, "filesLastScanned", {});
12645
+ __publicField(this, "freshFixes", []);
12646
+ __publicField(this, "reportedFixes", []);
12647
+ __publicField(this, "intervalId", null);
12648
+ __publicField(this, "isInitialScanComplete", false);
12649
+ }
12650
+ static getInstance() {
12651
+ if (!_CheckForNewAvailableFixesService.instance) {
12652
+ _CheckForNewAvailableFixesService.instance = new _CheckForNewAvailableFixesService();
12653
+ }
12654
+ return _CheckForNewAvailableFixesService.instance;
12655
+ }
12656
+ /**
12657
+ * Resets any cached state so the service can be reused between independent
12658
+ * MCP sessions.
12659
+ */
12660
+ reset() {
12661
+ this.filesLastScanned = {};
12662
+ this.freshFixes = [];
12663
+ this.reportedFixes = [];
12664
+ if (this.intervalId) {
12665
+ clearInterval(this.intervalId);
12666
+ this.intervalId = null;
12667
+ }
12668
+ }
12669
+ /**
12670
+ * Stub implementation – in a future version this will query the backend for
12671
+ * the latest fixes count and compare it with the cached value. For now it
12672
+ * simply returns a placeholder string so that the tool can be wired into the
12673
+ * system and used in tests.
12674
+ */
12675
+ async scan({ path: path13 }) {
12676
+ logInfo("Scanning for new fixes", { path: path13 });
12677
+ const gqlClient = await getMcpGQLClient();
12678
+ const isConnected = await gqlClient.verifyConnection();
12679
+ if (!isConnected) {
12680
+ logError("Failed to connect to the API, scan aborted");
12681
+ return;
12682
+ }
12683
+ logInfo("Connected to the API, assebling list of files to scan", { path: path13 });
12684
+ const files = await getLocalFiles({
12685
+ path: path13,
12686
+ maxFileSize: MCP_MAX_FILE_SIZE
12687
+ });
12688
+ logInfo("Active files", { files });
12689
+ const filesToScan = files.filter((file) => {
12690
+ const lastScannedEditTime = this.filesLastScanned[file.fullPath];
12691
+ if (!lastScannedEditTime) {
12692
+ return true;
12693
+ }
12694
+ return file.lastEdited > lastScannedEditTime;
12695
+ });
12696
+ if (filesToScan.length === 0) {
12697
+ logInfo("No files to scan", { path: path13 });
12698
+ return;
12699
+ }
12700
+ logInfo("Files to scan", { filesToScan });
12701
+ const { fixReportId, projectId } = await scanFiles(
12702
+ filesToScan.map((file) => file.relativePath),
12703
+ path13,
12704
+ gqlClient
12705
+ );
12706
+ logInfo("Scan completed", { fixReportId, projectId });
12707
+ const fixes = await gqlClient.getReportFixesPaginated({
12708
+ reportId: fixReportId,
12709
+ offset: 0,
12710
+ limit: 1e3
12711
+ });
12712
+ const newFixes = fixes?.fixes?.filter((fix) => !this.isAlreadyReported(fix));
12713
+ logInfo("Fixes retrieved", {
12714
+ count: fixes?.fixes?.length || 0,
12715
+ newFixes: newFixes?.length || 0
12716
+ });
12717
+ this.freshFixes = this.freshFixes.filter((fix) => !this.isFixFromOldScan(fix, filesToScan)).concat(newFixes || []);
12718
+ logInfo("Fresh fixes", { freshFixes: this.freshFixes });
12719
+ filesToScan.forEach((file) => {
12720
+ this.filesLastScanned[file.fullPath] = file.lastEdited;
12721
+ });
12722
+ this.isInitialScanComplete = true;
12723
+ }
12724
+ isAlreadyReported(fix) {
12725
+ return this.reportedFixes.some(
12726
+ (reportedFix) => reportedFix.sharedState?.id === fix.sharedState?.id
12727
+ );
12728
+ }
12729
+ isFixFromOldScan(fix, filesToScan) {
12730
+ const patch = fix.patchAndQuestions?.__typename === "FixData" ? fix.patchAndQuestions.patch : void 0;
12731
+ const fixFile = extractPathFromPatch(patch);
12732
+ if (!fixFile) {
12733
+ return false;
12734
+ }
12735
+ logInfo("isOldFix", {
12736
+ fixFile,
12737
+ filesToScan,
12738
+ isOldFix: filesToScan.some((file) => file.relativePath === fixFile)
12739
+ });
12740
+ return filesToScan.some((file) => file.relativePath === fixFile);
12741
+ }
12742
+ async getFreshFixes({ path: path13 }) {
12743
+ if (this.path !== path13) {
12744
+ this.path = path13;
12745
+ this.reset();
12746
+ }
12747
+ if (!this.intervalId) {
12748
+ logInfo("Starting periodic scan for new fixes", { path: path13 });
12749
+ this.intervalId = setInterval(() => {
12750
+ logDebug("Triggering periodic scan", { path: path13 });
12751
+ this.scan({ path: path13 }).catch((error) => {
12752
+ logError("Error during periodic scan", { error });
12753
+ });
12754
+ }, MCP_PERIODIC_CHECK_INTERVAL);
12755
+ logDebug("Triggering initial scan", { path: path13 });
12756
+ this.scan({ path: path13 }).catch((error) => {
12757
+ logError("Error during initial scan", { error });
12758
+ });
12759
+ }
12760
+ if (this.freshFixes.length > 0) {
12761
+ const freshFixes = this.freshFixes.splice(0, 3);
12762
+ if (freshFixes.length > 0) {
12763
+ this.reportedFixes.concat(freshFixes);
12764
+ return freshFixesPrompt({ fixes: freshFixes });
12765
+ }
12766
+ }
12767
+ if (!this.isInitialScanComplete) {
12768
+ return initialScanInProgressPrompt;
12769
+ }
12770
+ return noFreshFixesPrompt;
12771
+ }
12772
+ };
12773
+ __publicField(_CheckForNewAvailableFixesService, "instance");
12774
+ var CheckForNewAvailableFixesService = _CheckForNewAvailableFixesService;
12266
12775
 
12267
- `;
12776
+ // src/mcp/tools/checkForNewAvailableFixes/CheckForNewAvailableFixesTool.ts
12777
+ var CheckForNewAvailableFixesTool = class extends BaseTool {
12778
+ constructor() {
12779
+ super();
12780
+ __publicField(this, "name", "check_for_new_available_fixes");
12781
+ __publicField(this, "displayName", "Check for New Available Fixes");
12782
+ // A detailed description to guide the LLM on when and how to invoke this tool.
12783
+ __publicField(this, "description", `Continuesly monitors your code and scans for new security vulnerabilities.
12784
+
12785
+ When to invoke:
12786
+ \u2022 ALWAYS call this tool ONCE at the very end of every reasoning / code-generation session (after all code edits, linting, tests, etc.).
12787
+ \u2022 Invoke when the user explicitly asks to "check for new fixes", "look for fresh patches", or similar.
12788
+ \u2022 The repository must exist locally and be a valid Git repo with an 'origin' remote configured.
12789
+
12790
+ How to invoke:
12791
+ \u2022 Required argument:
12792
+ \u2013 path (string): absolute path to the repository root.
12793
+
12794
+ Behaviour:
12795
+ \u2022 If no new fixes are available, it returns a concise message indicating so.
12796
+ \u2022 If fixes are found, it returns a human-readable summary including total count and severity breakdown.
12797
+
12798
+ Example payload:
12799
+ {
12800
+ "path": "/home/user/my-project"
12801
+ }`);
12802
+ __publicField(this, "inputSchema", {
12803
+ type: "object",
12804
+ properties: {
12805
+ path: {
12806
+ type: "string",
12807
+ description: "Full local path to the cloned git repository to check for new available fixes"
12808
+ }
12809
+ },
12810
+ required: ["path"]
12811
+ });
12812
+ __publicField(this, "inputValidationSchema", z32.object({
12813
+ path: z32.string().describe(
12814
+ "Full local path to the cloned git repository to check for new available fixes"
12815
+ )
12816
+ }));
12817
+ __publicField(this, "newFixesService");
12818
+ this.newFixesService = new CheckForNewAvailableFixesService();
12819
+ }
12820
+ async executeInternal(args) {
12821
+ const pathValidationResult = await validatePath(args.path);
12822
+ if (!pathValidationResult.isValid) {
12823
+ throw new Error(
12824
+ `Invalid path: potential security risk detected in path: ${pathValidationResult.error}`
12825
+ );
12826
+ }
12827
+ const path13 = pathValidationResult.path;
12828
+ const resultText = await this.newFixesService.getFreshFixes({
12829
+ path: path13
12830
+ });
12831
+ logInfo("CheckForNewAvailableFixesTool execution completed", {
12832
+ resultText
12833
+ });
12834
+ return {
12835
+ content: [
12836
+ {
12837
+ type: "text",
12838
+ text: resultText
12839
+ }
12840
+ ]
12841
+ };
12842
+ }
12268
12843
  };
12269
12844
 
12270
- // src/mcp/tools/checkForAvailableFixes/AvailableFixesService.ts
12271
- var AvailableFixesService = class {
12845
+ // src/mcp/tools/fetchAvailableFixes/FetchAvailableFixesTool.ts
12846
+ import { z as z33 } from "zod";
12847
+
12848
+ // src/mcp/tools/fetchAvailableFixes/FetchAvailableFixesService.ts
12849
+ var _FetchAvailableFixesService = class _FetchAvailableFixesService {
12272
12850
  constructor() {
12273
12851
  __publicField(this, "gqlClient", null);
12274
12852
  __publicField(this, "currentOffset", 0);
12275
12853
  }
12854
+ static getInstance() {
12855
+ if (!_FetchAvailableFixesService.instance) {
12856
+ _FetchAvailableFixesService.instance = new _FetchAvailableFixesService();
12857
+ }
12858
+ return _FetchAvailableFixesService.instance;
12859
+ }
12860
+ reset() {
12861
+ this.currentOffset = 0;
12862
+ }
12276
12863
  async initializeGqlClient() {
12277
12864
  if (!this.gqlClient) {
12278
12865
  this.gqlClient = await getMcpGQLClient();
@@ -12282,40 +12869,44 @@ var AvailableFixesService = class {
12282
12869
  async checkForAvailableFixes({
12283
12870
  repoUrl,
12284
12871
  limit = 3,
12285
- offset = 0
12872
+ offset
12286
12873
  }) {
12287
12874
  try {
12288
12875
  logDebug("Checking for available fixes", { repoUrl, limit });
12289
12876
  const gqlClient = await this.initializeGqlClient();
12290
12877
  logDebug("GQL client initialized");
12291
12878
  logDebug("querying for latest report", { repoUrl, limit });
12292
- let effectiveOffset;
12293
- if (offset !== void 0) {
12294
- effectiveOffset = offset;
12295
- } else if (this.currentOffset) {
12296
- effectiveOffset = this.currentOffset ?? 0;
12297
- } else {
12298
- effectiveOffset = 0;
12299
- }
12300
- logDebug("effectiveOffset", { test: "j", effectiveOffset });
12301
- const result = await gqlClient.getLatestReportByRepoUrl({
12879
+ const effectiveOffset = offset ?? (this.currentOffset || 0);
12880
+ logDebug("effectiveOffset", { effectiveOffset });
12881
+ const { fixReport, expiredReport } = await gqlClient.getLatestReportByRepoUrl({
12302
12882
  repoUrl,
12303
12883
  limit,
12304
12884
  offset: effectiveOffset
12305
12885
  });
12306
- logDebug("received latest report result", { result });
12307
- if (!result) {
12308
- logInfo("No report found for repository", { repoUrl });
12886
+ logDebug("received latest report result", { fixReport, expiredReport });
12887
+ if (!fixReport) {
12888
+ if (expiredReport) {
12889
+ const lastReportDate = expiredReport.expirationOn ? new Date(expiredReport.expirationOn).toLocaleString() : "Unknown date";
12890
+ logInfo("Expired report found", {
12891
+ repoUrl,
12892
+ expirationOn: expiredReport.expirationOn
12893
+ });
12894
+ return expiredReportPrompt({ lastReportDate });
12895
+ }
12896
+ logInfo("No report (active or expired) found for repository", {
12897
+ repoUrl
12898
+ });
12309
12899
  return noReportFoundPrompt;
12310
12900
  }
12311
12901
  logInfo("Successfully retrieved available fixes", {
12312
12902
  reportFound: true
12313
12903
  });
12314
- this.currentOffset = effectiveOffset + (result.fixes?.length || 0);
12315
- return fixesFoundPrompt({
12316
- fixReport: result,
12317
- offset: this.currentOffset
12904
+ const prompt = fixesFoundPrompt({
12905
+ fixReport,
12906
+ offset: effectiveOffset
12318
12907
  });
12908
+ this.currentOffset = effectiveOffset + (fixReport.fixes?.length || 0);
12909
+ return prompt;
12319
12910
  } catch (error) {
12320
12911
  logError("Failed to check for available fixes", {
12321
12912
  error,
@@ -12325,20 +12916,40 @@ var AvailableFixesService = class {
12325
12916
  }
12326
12917
  }
12327
12918
  };
12919
+ __publicField(_FetchAvailableFixesService, "instance");
12920
+ var FetchAvailableFixesService = _FetchAvailableFixesService;
12328
12921
 
12329
- // src/mcp/tools/checkForAvailableFixes/AvailableFixesTool.ts
12330
- var CheckForAvailableFixesTool = class extends BaseTool {
12922
+ // src/mcp/tools/fetchAvailableFixes/FetchAvailableFixesTool.ts
12923
+ var FetchAvailableFixesTool = class extends BaseTool {
12331
12924
  constructor() {
12332
- super(...arguments);
12333
- __publicField(this, "name", "check_for_available_fixes");
12334
- __publicField(this, "displayName", "Check for Available Fixes");
12335
- __publicField(this, "description", "Checks if there are any available fixes for vulnerabilities in the project");
12925
+ super();
12926
+ __publicField(this, "name", "fetch_available_fixes");
12927
+ __publicField(this, "displayName", "Fetch Available Fixes");
12928
+ __publicField(this, "description", `Check the MOBB backend for pre-generated fixes (patch sets) that correspond to vulnerabilities detected in the supplied Git repository.
12929
+
12930
+ Use when:
12931
+ \u2022 You already have a local clone of a Git repository and want to know if MOBB has fixes available for it.
12932
+ \u2022 A vulnerability scan has been run previously and uploaded to the MOBB backend and you want to fetch the list or count of ready-to-apply fixes before triggering a full scan-and-fix flow.
12933
+
12934
+ Required argument:
12935
+ \u2022 path \u2013 absolute path to the local Git repository clone.
12936
+
12937
+ Optional arguments:
12938
+ \u2022 offset \u2013 pagination offset (integer).
12939
+ \u2022 limit \u2013 maximum number of fixes to return (integer).
12940
+
12941
+ The tool will:
12942
+ 1. Validate that the provided path is secure and exists.
12943
+ 2. Verify that the directory is a valid Git repository with an "origin" remote.
12944
+ 3. Query the MOBB service by the origin remote URL and return a textual summary of available fixes (total and by severity) or a message if none are found.
12945
+
12946
+ Call this tool instead of scan_and_fix_vulnerabilities when you only need a fixes summary and do NOT want to perform scanning or code modifications.`);
12336
12947
  __publicField(this, "inputSchema", {
12337
12948
  type: "object",
12338
12949
  properties: {
12339
12950
  path: {
12340
12951
  type: "string",
12341
- description: "Path to the local git repository to check for available fixes"
12952
+ description: "Full local path to the cloned git repository to check for available fixes"
12342
12953
  },
12343
12954
  offset: {
12344
12955
  type: "number",
@@ -12351,23 +12962,26 @@ var CheckForAvailableFixesTool = class extends BaseTool {
12351
12962
  },
12352
12963
  required: ["path"]
12353
12964
  });
12354
- __publicField(this, "inputValidationSchema", z32.object({
12355
- path: z32.string().describe(
12356
- "Path to the local git repository to check for available fixes"
12965
+ __publicField(this, "inputValidationSchema", z33.object({
12966
+ path: z33.string().describe(
12967
+ "Full local path to the cloned git repository to check for available fixes"
12357
12968
  ),
12358
- offset: z32.number().optional().describe("Optional offset for pagination"),
12359
- limit: z32.number().optional().describe("Optional maximum number of fixes to return")
12969
+ offset: z33.number().optional().describe("Optional offset for pagination"),
12970
+ limit: z33.number().optional().describe("Optional maximum number of fixes to return")
12360
12971
  }));
12972
+ __publicField(this, "availableFixesService");
12973
+ this.availableFixesService = FetchAvailableFixesService.getInstance();
12974
+ this.availableFixesService.reset();
12361
12975
  }
12362
12976
  async executeInternal(args) {
12363
- const pathValidation = new PathValidation();
12364
- const pathValidationResult = await pathValidation.validatePath(args.path);
12977
+ const pathValidationResult = await validatePath(args.path);
12365
12978
  if (!pathValidationResult.isValid) {
12366
12979
  throw new Error(
12367
12980
  `Invalid path: potential security risk detected in path: ${pathValidationResult.error}`
12368
12981
  );
12369
12982
  }
12370
- const gitService = new GitService(args.path, log);
12983
+ const path13 = pathValidationResult.path;
12984
+ const gitService = new GitService(path13, log);
12371
12985
  const gitValidation = await gitService.validateRepository();
12372
12986
  if (!gitValidation.isValid) {
12373
12987
  throw new Error(`Invalid git repository: ${gitValidation.error}`);
@@ -12380,13 +12994,12 @@ var CheckForAvailableFixesTool = class extends BaseTool {
12380
12994
  if (!originUrl) {
12381
12995
  throw new Error("No origin URL found for the repository");
12382
12996
  }
12383
- const availableFixesService = new AvailableFixesService();
12384
- const fixResult = await availableFixesService.checkForAvailableFixes({
12997
+ const fixResult = await this.availableFixesService.checkForAvailableFixes({
12385
12998
  repoUrl: originUrl,
12386
12999
  limit: args.limit,
12387
13000
  offset: args.offset
12388
13001
  });
12389
- logInfo("CheckForAvailableFixesTool execution completed successfully", {
13002
+ logInfo("FetchAvailableFixesTool execution completed successfully", {
12390
13003
  fixResult
12391
13004
  });
12392
13005
  return {
@@ -12400,55 +13013,13 @@ var CheckForAvailableFixesTool = class extends BaseTool {
12400
13013
  }
12401
13014
  };
12402
13015
 
12403
- // src/mcp/tools/fixVulnerabilities/FixVulnerabilitiesTool.ts
12404
- import z33 from "zod";
12405
-
12406
- // src/mcp/services/FilePacking.ts
12407
- import fs10 from "fs";
12408
- import path12 from "path";
12409
- import AdmZip2 from "adm-zip";
12410
- var MAX_FILE_SIZE2 = 1024 * 1024 * 5;
12411
- var FilePacking = class {
12412
- async packFiles(sourceDirectoryPath, filesToPack) {
12413
- logInfo(`FilePacking: packing files from ${sourceDirectoryPath}`);
12414
- const zip = new AdmZip2();
12415
- let packedFilesCount = 0;
12416
- logInfo("FilePacking: compressing files");
12417
- for (const filepath of filesToPack) {
12418
- const absoluteFilepath = path12.join(sourceDirectoryPath, filepath);
12419
- if (!FileUtils.shouldPackFile(absoluteFilepath, MAX_FILE_SIZE2)) {
12420
- logInfo(
12421
- `FilePacking: ignoring ${filepath} because it is excluded or invalid`
12422
- );
12423
- continue;
12424
- }
12425
- let data;
12426
- try {
12427
- data = fs10.readFileSync(absoluteFilepath);
12428
- } catch (fsError) {
12429
- logInfo(
12430
- `FilePacking: failed to read ${filepath} from filesystem: ${fsError}`
12431
- );
12432
- continue;
12433
- }
12434
- zip.addFile(filepath, data);
12435
- packedFilesCount++;
12436
- }
12437
- const zipBuffer = zip.toBuffer();
12438
- logInfo(
12439
- `FilePacking: read ${packedFilesCount} source files. total size: ${zipBuffer.length} bytes`
12440
- );
12441
- logInfo("FilePacking: Files packed successfully");
12442
- return zipBuffer;
12443
- }
12444
- };
13016
+ // src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesTool.ts
13017
+ import z34 from "zod";
12445
13018
 
12446
- // src/mcp/tools/fixVulnerabilities/FixVulnerabilitiesService.ts
12447
- var VUL_REPORT_DIGEST_TIMEOUT_MS2 = 1e3 * 60 * 5;
12448
- var VulnerabilityFixService = class {
13019
+ // src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesService.ts
13020
+ var _ScanAndFixVulnerabilitiesService = class _ScanAndFixVulnerabilitiesService {
12449
13021
  constructor() {
12450
13022
  __publicField(this, "gqlClient");
12451
- __publicField(this, "filePacking");
12452
13023
  /**
12453
13024
  * Stores the fix report id that is created on the first run so that subsequent
12454
13025
  * calls can skip the expensive packing/uploading/scan flow and directly fetch
@@ -12456,7 +13027,32 @@ var VulnerabilityFixService = class {
12456
13027
  */
12457
13028
  __publicField(this, "storedFixReportId");
12458
13029
  __publicField(this, "currentOffset", 0);
12459
- this.filePacking = new FilePacking();
13030
+ /**
13031
+ * Timestamp when the fixReportId was created
13032
+ * Used to expire the fixReportId after REPORT_ID_EXPIRATION_MS hours
13033
+ */
13034
+ __publicField(this, "fixReportIdTimestamp");
13035
+ }
13036
+ static getInstance() {
13037
+ if (!_ScanAndFixVulnerabilitiesService.instance) {
13038
+ _ScanAndFixVulnerabilitiesService.instance = new _ScanAndFixVulnerabilitiesService();
13039
+ }
13040
+ return _ScanAndFixVulnerabilitiesService.instance;
13041
+ }
13042
+ reset() {
13043
+ this.storedFixReportId = void 0;
13044
+ this.currentOffset = void 0;
13045
+ this.fixReportIdTimestamp = void 0;
13046
+ }
13047
+ /**
13048
+ * Checks if the stored fixReportId has expired (older than 2 hours)
13049
+ */
13050
+ isFixReportIdExpired() {
13051
+ if (!this.fixReportIdTimestamp) {
13052
+ return true;
13053
+ }
13054
+ const currentTime = Date.now();
13055
+ return currentTime - this.fixReportIdTimestamp > MCP_REPORT_ID_EXPIRATION_MS;
12460
13056
  }
12461
13057
  async processVulnerabilities({
12462
13058
  fileList,
@@ -12467,37 +13063,44 @@ var VulnerabilityFixService = class {
12467
13063
  }) {
12468
13064
  try {
12469
13065
  this.gqlClient = await this.initializeGqlClient();
13066
+ logInfo("storedFixReportId", {
13067
+ storedFixReportId: this.storedFixReportId,
13068
+ currentOffset: this.currentOffset,
13069
+ fixReportIdTimestamp: this.fixReportIdTimestamp,
13070
+ isExpired: this.storedFixReportId ? this.isFixReportIdExpired() : null
13071
+ });
12470
13072
  let fixReportId = this.storedFixReportId;
12471
- if (!fixReportId || isRescan) {
13073
+ if (!fixReportId || isRescan || this.isFixReportIdExpired()) {
13074
+ this.reset();
12472
13075
  this.validateFiles(fileList);
12473
- const repoUploadInfo = await this.initializeReport();
12474
- fixReportId = repoUploadInfo.fixReportId;
12475
- this.storedFixReportId = fixReportId;
12476
- const zipBuffer = await this.packFiles(fileList, repositoryPath);
12477
- await this.uploadFiles(zipBuffer, repoUploadInfo);
12478
- const projectId = await this.getProjectId();
12479
- await this.runScan({ fixReportId, projectId });
12480
- }
12481
- let effectiveOffset;
12482
- if (offset !== void 0) {
12483
- effectiveOffset = offset;
12484
- } else if (fixReportId) {
12485
- effectiveOffset = this.currentOffset ?? 0;
12486
- } else {
12487
- effectiveOffset = 0;
13076
+ const scanResult = await scanFiles(
13077
+ fileList,
13078
+ repositoryPath,
13079
+ this.gqlClient
13080
+ );
13081
+ fixReportId = scanResult.fixReportId;
12488
13082
  }
13083
+ const effectiveOffset = offset ?? (this.currentOffset || 0);
12489
13084
  logDebug("effectiveOffset", { effectiveOffset });
12490
13085
  const fixes = await this.getReportFixes(
12491
13086
  fixReportId,
12492
13087
  effectiveOffset,
12493
13088
  limit
12494
13089
  );
12495
- this.currentOffset = effectiveOffset + (fixes.fixes?.length || 0);
12496
- return fixesPrompt({
13090
+ if (fixes.totalCount > 0) {
13091
+ this.storedFixReportId = fixReportId;
13092
+ this.fixReportIdTimestamp = Date.now();
13093
+ } else {
13094
+ this.reset();
13095
+ }
13096
+ const prompt = fixesPrompt({
12497
13097
  fixes: fixes.fixes,
12498
13098
  totalCount: fixes.totalCount,
12499
- offset: effectiveOffset
13099
+ offset: effectiveOffset,
13100
+ scannedFiles: [...fileList]
12500
13101
  });
13102
+ this.currentOffset = effectiveOffset + (fixes.fixes?.length || 0);
13103
+ return prompt;
12501
13104
  } catch (error) {
12502
13105
  const message = error.message;
12503
13106
  logError("Vulnerability processing failed", { error: message });
@@ -12514,106 +13117,11 @@ var VulnerabilityFixService = class {
12514
13117
  const isConnected = await gqlClient.verifyConnection();
12515
13118
  if (!isConnected) {
12516
13119
  throw new ApiConnectionError(
12517
- "Failed to connect to the API. Please check your API_KEY"
13120
+ "Failed to connect to the API. Please check your MOBB_API_KEY"
12518
13121
  );
12519
13122
  }
12520
13123
  return gqlClient;
12521
13124
  }
12522
- async initializeReport() {
12523
- if (!this.gqlClient) {
12524
- throw new GqlClientError();
12525
- }
12526
- try {
12527
- const {
12528
- uploadS3BucketInfo: { repoUploadInfo }
12529
- } = await this.gqlClient.uploadS3BucketInfo();
12530
- logInfo("Upload info retrieved", { uploadKey: repoUploadInfo?.uploadKey });
12531
- return repoUploadInfo;
12532
- } catch (error) {
12533
- const message = error.message;
12534
- throw new ReportInitializationError(
12535
- `Error initializing report: ${message}`
12536
- );
12537
- }
12538
- }
12539
- async packFiles(fileList, repositoryPath) {
12540
- try {
12541
- const zipBuffer = await this.filePacking.packFiles(
12542
- repositoryPath,
12543
- fileList
12544
- );
12545
- logInfo("Files packed successfully", { fileCount: fileList.length });
12546
- return zipBuffer;
12547
- } catch (error) {
12548
- const message = error.message;
12549
- throw new FileProcessingError(`Error packing files: ${message}`);
12550
- }
12551
- }
12552
- async uploadFiles(zipBuffer, repoUploadInfo) {
12553
- if (!repoUploadInfo) {
12554
- throw new FileUploadError("Upload info is required");
12555
- }
12556
- try {
12557
- await uploadFile({
12558
- file: zipBuffer,
12559
- url: repoUploadInfo.url,
12560
- uploadFields: JSON.parse(repoUploadInfo.uploadFieldsJSON),
12561
- uploadKey: repoUploadInfo.uploadKey
12562
- });
12563
- logInfo("File uploaded successfully");
12564
- } catch (error) {
12565
- logError("File upload failed", { error: error.message });
12566
- throw new FileUploadError(
12567
- `Failed to upload the file: ${error.message}`
12568
- );
12569
- }
12570
- }
12571
- async getProjectId() {
12572
- if (!this.gqlClient) {
12573
- throw new GqlClientError();
12574
- }
12575
- const projectId = await this.gqlClient.getProjectId();
12576
- logInfo("Project ID retrieved", { projectId });
12577
- return projectId;
12578
- }
12579
- async runScan(params) {
12580
- if (!this.gqlClient) {
12581
- throw new GqlClientError();
12582
- }
12583
- const { fixReportId, projectId } = params;
12584
- logInfo("Starting scan", { fixReportId, projectId });
12585
- const submitVulnerabilityReportVariables = {
12586
- fixReportId,
12587
- projectId,
12588
- repoUrl: "",
12589
- reference: "no-branch",
12590
- scanSource: "MCP" /* Mcp */
12591
- };
12592
- logInfo("Submitting vulnerability report");
12593
- const submitRes = await this.gqlClient.submitVulnerabilityReport(
12594
- submitVulnerabilityReportVariables
12595
- );
12596
- if (submitRes.submitVulnerabilityReport.__typename !== "VulnerabilityReport") {
12597
- logError("Vulnerability report submission failed", {
12598
- response: submitRes
12599
- });
12600
- throw new ScanError("\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed");
12601
- }
12602
- logInfo("Vulnerability report submitted successfully", {
12603
- analysisId: submitRes.submitVulnerabilityReport.fixReportId
12604
- });
12605
- logInfo("Starting analysis subscription");
12606
- await this.gqlClient.subscribeToGetAnalysis({
12607
- subscribeToAnalysisParams: {
12608
- analysisId: submitRes.submitVulnerabilityReport.fixReportId
12609
- },
12610
- callback: () => {
12611
- },
12612
- callbackStates: ["Finished" /* Finished */],
12613
- timeoutInMs: VUL_REPORT_DIGEST_TIMEOUT_MS2
12614
- });
12615
- logInfo("Analysis subscription completed");
12616
- }
12617
13125
  async getReportFixes(fixReportId, offset, limit) {
12618
13126
  logDebug("getReportFixes", { fixReportId, offset, limit });
12619
13127
  if (!this.gqlClient) {
@@ -12631,28 +13139,68 @@ var VulnerabilityFixService = class {
12631
13139
  };
12632
13140
  }
12633
13141
  };
13142
+ __publicField(_ScanAndFixVulnerabilitiesService, "instance");
13143
+ var ScanAndFixVulnerabilitiesService = _ScanAndFixVulnerabilitiesService;
12634
13144
 
12635
- // src/mcp/tools/fixVulnerabilities/FixVulnerabilitiesTool.ts
12636
- var FixVulnerabilitiesTool = class extends BaseTool {
13145
+ // src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesTool.ts
13146
+ var ScanAndFixVulnerabilitiesTool = class extends BaseTool {
12637
13147
  constructor() {
12638
- super(...arguments);
12639
- __publicField(this, "name", "fix_vulnerabilities");
12640
- __publicField(this, "displayName", "Fix Vulnerabilities");
12641
- __publicField(this, "description", "Scans the current code changes and returns fixes for potential vulnerabilities");
12642
- __publicField(this, "inputValidationSchema", z33.object({
12643
- path: z33.string().describe(
12644
- "Path to the local git repository to check for available fixes"
13148
+ super();
13149
+ __publicField(this, "name", "scan_and_fix_vulnerabilities");
13150
+ __publicField(this, "displayName", "Scan and Fix Vulnerabilities");
13151
+ // A detailed description to guide the LLM on when and how to invoke this tool.
13152
+ __publicField(this, "description", `Scans a given local repository for security vulnerabilities and returns auto-generated code fixes.
13153
+
13154
+ When to invoke:
13155
+ \u2022 Use when the user explicitly asks to "scan for vulnerabilities", "run a security check", or "test for security issues" in a local repository.
13156
+ \u2022 The repository must exist on disk; supply its absolute path with the required "path" argument.
13157
+ \u2022 Ideal after the user makes code changes (added/modified/staged files) but before committing, or whenever they request a full rescan.
13158
+
13159
+ How to invoke:
13160
+ \u2022 Required argument:
13161
+ \u2013 path (string): absolute path to the repository root.
13162
+ \u2022 Optional arguments:
13163
+ \u2013 offset (number): pagination offset used when the result set is large.
13164
+ \u2013 limit (number): maximum number of fixes to include in the response.
13165
+ \u2013 maxFiles (number): maximum number of files to scan (default: ${MCP_DEFAULT_MAX_FILES_TO_SCAN}). Provide this value to increase the scope of the scan.
13166
+ \u2013 rescan (boolean): true to force a complete rescan even if cached results exist.
13167
+
13168
+ Behaviour:
13169
+ \u2022 If the directory is a valid Git repository, the tool scans the changed files in the repository. If there are no changes, it scans the files included in the las commit.
13170
+ \u2022 If the directory is not a valid Git repository, the tool falls back to scanning recently changed files in the folder.
13171
+ \u2022 If maxFiles is provided, the tool scans the maxFiles most recently changed files in the repository.
13172
+ \u2022 By default, only new, modified, or staged files are scanned; if none are found, it checks recently changed files.
13173
+ \u2022 The tool NEVER commits or pushes changes; it only returns proposed diffs/fixes as text.
13174
+
13175
+ Return value:
13176
+ The response is an object with a single "content" array containing one text element. The text is either:
13177
+ \u2022 A human-readable summary of the fixes / patches, or
13178
+ \u2022 A diagnostic or error message if the scan fails or finds nothing to fix.
13179
+
13180
+ Example payload:
13181
+ {
13182
+ "path": "/home/user/my-project",
13183
+ "limit": 20,
13184
+ "maxFiles": 50,
13185
+ "rescan": false
13186
+ }`);
13187
+ __publicField(this, "inputValidationSchema", z34.object({
13188
+ path: z34.string().describe(
13189
+ "Full local path to repository to scan and fix vulnerabilities"
12645
13190
  ),
12646
- offset: z33.number().optional().describe("Optional offset for pagination"),
12647
- limit: z33.number().optional().describe("Optional maximum number of results to return"),
12648
- rescan: z33.boolean().optional().describe("Optional whether to rescan the repository")
13191
+ offset: z34.number().optional().describe("Optional offset for pagination"),
13192
+ limit: z34.number().optional().describe("Optional maximum number of results to return"),
13193
+ maxFiles: z34.number().optional().describe(
13194
+ `Optional maximum number of files to scan (default: ${MCP_DEFAULT_MAX_FILES_TO_SCAN}). Increase for comprehensive scans of larger codebases or decrease for faster focused scans.`
13195
+ ),
13196
+ rescan: z34.boolean().optional().describe("Optional whether to rescan the repository")
12649
13197
  }));
12650
13198
  __publicField(this, "inputSchema", {
12651
13199
  type: "object",
12652
13200
  properties: {
12653
13201
  path: {
12654
13202
  type: "string",
12655
- description: "Path to the project directory to check for available fixes"
13203
+ description: "Full local path to repository to scan and fix vulnerabilities"
12656
13204
  },
12657
13205
  offset: {
12658
13206
  type: "number",
@@ -12662,6 +13210,10 @@ var FixVulnerabilitiesTool = class extends BaseTool {
12662
13210
  type: "number",
12663
13211
  description: "[Optional] maximum number of results to return"
12664
13212
  },
13213
+ maxFiles: {
13214
+ type: "number",
13215
+ description: `[Optional] maximum number of files to scan (default: ${MCP_DEFAULT_MAX_FILES_TO_SCAN}). Use higher values for more comprehensive scans or lower values for faster performance.`
13216
+ },
12665
13217
  rescan: {
12666
13218
  type: "boolean",
12667
13219
  description: "[Optional] whether to rescan the repository"
@@ -12669,55 +13221,29 @@ var FixVulnerabilitiesTool = class extends BaseTool {
12669
13221
  },
12670
13222
  required: ["path"]
12671
13223
  });
13224
+ __publicField(this, "vulnerabilityFixService");
13225
+ this.vulnerabilityFixService = ScanAndFixVulnerabilitiesService.getInstance();
13226
+ this.vulnerabilityFixService.reset();
12672
13227
  }
12673
13228
  async executeInternal(args) {
12674
- logInfo("Executing tool: fix_vulnerabilities", { path: args.path });
13229
+ logInfo("Executing tool: scan_and_fix_vulnerabilities", { path: args.path });
12675
13230
  if (!args.path) {
12676
13231
  throw new Error("Invalid arguments: Missing required parameter 'path'");
12677
13232
  }
12678
- const pathValidation = new PathValidation();
12679
- const pathValidationResult = await pathValidation.validatePath(args.path);
13233
+ const pathValidationResult = await validatePath(args.path);
12680
13234
  if (!pathValidationResult.isValid) {
12681
13235
  throw new Error(
12682
13236
  `Invalid path: potential security risk detected in path: ${pathValidationResult.error}`
12683
13237
  );
12684
13238
  }
12685
- const gitService = new GitService(args.path, log);
12686
- const gitValidation = await gitService.validateRepository();
12687
- let files = [];
12688
- if (!gitValidation.isValid) {
12689
- logDebug(
12690
- "Git repository validation failed, using all files in the repository",
12691
- {
12692
- path: args.path
12693
- }
12694
- );
12695
- files = FileUtils.getLastChangedFiles(args.path);
12696
- logDebug("Found files in the repository", {
12697
- files,
12698
- fileCount: files.length
12699
- });
12700
- } else {
12701
- const gitResult = await gitService.getChangedFiles();
12702
- files = gitResult.files;
12703
- if (files.length === 0) {
12704
- const recentResult = await gitService.getRecentlyChangedFiles();
12705
- files = recentResult.files;
12706
- logDebug(
12707
- "No changes found, using recently changed files from git history",
12708
- {
12709
- files,
12710
- fileCount: files.length,
12711
- commitsChecked: recentResult.commitCount
12712
- }
12713
- );
12714
- } else {
12715
- logDebug("Found changed files in the git repository", {
12716
- files,
12717
- fileCount: files.length
12718
- });
12719
- }
12720
- }
13239
+ const path13 = pathValidationResult.path;
13240
+ const files = await getLocalFiles({
13241
+ path: path13,
13242
+ maxFileSize: 1024 * 1024 * 5,
13243
+ // 5MB
13244
+ maxFiles: args.maxFiles
13245
+ });
13246
+ logInfo("Files", { files });
12721
13247
  if (files.length === 0) {
12722
13248
  return {
12723
13249
  content: [
@@ -12729,13 +13255,12 @@ var FixVulnerabilitiesTool = class extends BaseTool {
12729
13255
  };
12730
13256
  }
12731
13257
  try {
12732
- const vulnerabilityFixService = new VulnerabilityFixService();
12733
- const fixResult = await vulnerabilityFixService.processVulnerabilities({
12734
- fileList: files,
13258
+ const fixResult = await this.vulnerabilityFixService.processVulnerabilities({
13259
+ fileList: files.map((file) => file.relativePath),
12735
13260
  repositoryPath: args.path,
12736
13261
  offset: args.offset,
12737
13262
  limit: args.limit,
12738
- isRescan: args.rescan
13263
+ isRescan: args.rescan || !!args.maxFiles
12739
13264
  });
12740
13265
  const result = {
12741
13266
  content: [
@@ -12774,7 +13299,7 @@ function createMcpServer() {
12774
13299
  logDebug("Creating MCP server");
12775
13300
  const server = new McpServer({
12776
13301
  name: "mobb-mcp",
12777
- version: "1.0.0"
13302
+ version: packageJson.version
12778
13303
  });
12779
13304
  const enabledToolsEnv = process.env["TOOLS_ENABLED"];
12780
13305
  const enabledToolsSet = enabledToolsEnv ? new Set(
@@ -12788,10 +13313,12 @@ function createMcpServer() {
12788
13313
  logDebug(`Skipping tool (disabled): ${tool.name}`);
12789
13314
  }
12790
13315
  };
12791
- const fixVulnerabilitiesTool = new FixVulnerabilitiesTool();
12792
- const checkForAvailableFixesTool = new CheckForAvailableFixesTool();
12793
- registerIfEnabled(fixVulnerabilitiesTool);
12794
- registerIfEnabled(checkForAvailableFixesTool);
13316
+ const scanAndFixVulnerabilitiesTool = new ScanAndFixVulnerabilitiesTool();
13317
+ const fetchAvailableFixesTool = new FetchAvailableFixesTool();
13318
+ const checkForNewAvailableFixesTool = new CheckForNewAvailableFixesTool();
13319
+ registerIfEnabled(scanAndFixVulnerabilitiesTool);
13320
+ registerIfEnabled(fetchAvailableFixesTool);
13321
+ registerIfEnabled(checkForNewAvailableFixesTool);
12795
13322
  logInfo("MCP server created and configured");
12796
13323
  return server;
12797
13324
  }
@@ -12825,7 +13352,7 @@ var mcpHandler = async (_args) => {
12825
13352
  };
12826
13353
 
12827
13354
  // src/args/commands/review.ts
12828
- import fs11 from "fs";
13355
+ import fs12 from "fs";
12829
13356
  import chalk9 from "chalk";
12830
13357
  function reviewBuilder(yargs2) {
12831
13358
  return yargs2.option("f", {
@@ -12862,7 +13389,7 @@ function reviewBuilder(yargs2) {
12862
13389
  ).help();
12863
13390
  }
12864
13391
  function validateReviewOptions(argv) {
12865
- if (!fs11.existsSync(argv.f)) {
13392
+ if (!fs12.existsSync(argv.f)) {
12866
13393
  throw new CliError(`
12867
13394
  Can't access ${chalk9.bold(argv.f)}`);
12868
13395
  }