@convisoappsec/mcp 0.5.0 → 0.6.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Conviso MCP Server
4
4
 
5
- This repository contains both a Python and a Node.js MCP server. This `node/` folder is the Node.js bundle and is packaged into the `.mcpb` bundle for MCP clients that run Node runtimes.
5
+ This `node/` folder contains the Conviso MCP server (Node.js). It is published to npm as `@convisoappsec/mcp` and packaged into the `.mcpb` bundle for MCP clients.
6
6
 
7
7
  ## 🛠 Available Tools (Capabilities)
8
8
 
@@ -15,8 +15,6 @@ The server exposes the following tools to the LLM (see `node/manifest.json` for
15
15
  | **Vulnerabilities** | `get_issues` | List vulnerabilities by company or project. |
16
16
  | **Vulnerabilities** | `get_issue` | Technical details, including **code snippets** and raw requests/responses. |
17
17
  | **Vulnerabilities** | `get_top_vulnerabilities` | Risk overview (vulnerability count by severity). |
18
- | **Vulnerabilities** | `get_issues_by_asset_id` | List vulnerabilities for a company filtered by a single asset ID. |
19
- | **Vulnerabilities** | `get_issues_by_project_id` | List vulnerabilities for a company filtered by a project ID. |
20
18
  | **Management** | `get_projects` | List active security projects. |
21
19
  | **Management** | `get_project` | Get specific project in Conviso Platform by project ID. |
22
20
  | **Assets** | `get_assets` | List assets mapped within the platform. |
@@ -33,15 +31,18 @@ The server exposes the following tools to the LLM (see `node/manifest.json` for
33
31
  | **Supply chain** | `get_sbom_components` | SBOM / dependency components per company. |
34
32
  | **AI-Pentest** | `get_pentest_artifacts` / `get_pentest_artifact` / `get_pentest_execution` | Pentest artifacts, scope and execution results. |
35
33
  | **Threat Modeling** | `get_threat_model_artifacts` / `get_threat_model_artifact` | Threat model artifacts and versions. |
36
- | **Write engine** | `list_mutations` / `describe_mutation` / `execute_mutation` | Discover, describe and run any of the allowlisted mutations. |
37
- | **Write (curated)** | `change_issue_status` | Change an issue/vulnerability status. |
38
- | **Write (curated)** | `create_source_code_vulnerability` | Create a manual source-code vulnerability. |
39
- | **Write (curated)** | `create_project` / `create_asset` / `create_ticket` | Create a project, asset or ticket. |
40
- | **Write (curated)** | `run_dast` | Start a Conviso DAST scan on an asset. |
41
- | **Write (curated)** | `trigger_pentest` / `create_pentest_artifact` | Trigger an AI-Pentest execution / create its artifact. |
42
-
43
- > Write tools are **Node-only**. Mutations are restricted to an allowlist
44
- > (`src/conviso_mcp/operation_allowlist.js`); see the root `README.md` for the write workflow.
34
+ | **Writes engine** | `list_mutations` / `describe_mutation` / `execute_mutation` | Discover, describe and run the permitted write operations below. |
35
+ | **Writes — Issues** | `execute_mutation` | Create, update, delete and change status of vulnerabilities/issues. |
36
+ | **Writes — Assets** | `execute_mutation` | Create and update assets; run a DAST scan. |
37
+ | **Writes — Tickets** | `execute_mutation` | Create tickets. |
38
+ | **Writes — Projects** | `execute_mutation` | Create, update, change status and remove projects. |
39
+ | **Writes — Requirements** | `execute_mutation` | Create and update requirements. |
40
+ | **Writes — AI-Pentest** | `execute_mutation` | Create artifacts, schedule, trigger executions and retests. |
41
+ | **Writes Applications** | `execute_mutation` | Create and update applications. |
42
+ | **Writes Threat Modeling** | `execute_mutation` | Create and update threat-model artifacts and versions. |
43
+
44
+ > Write tools are **Node-only** and limited to the supported client-facing capabilities; see
45
+ > the root `README.md` for the write workflow.
45
46
 
46
47
  ## 🚀 Installation and Configuration (Node.js bundle)
47
48
 
@@ -66,20 +67,6 @@ export CONVISO_API_KEY=your_api_key_here
66
67
  npm start
67
68
  ```
68
69
 
69
- ## Staging (optional)
70
-
71
- Set the `STAGING` environment variable to `true` to make the server use
72
- `https://staging.convisoappsec.com` instead of the production API.
73
-
74
- Example:
75
-
76
- ```bash
77
- # staging (testing only)
78
- export STAGING=true
79
- npm start
80
- ```
81
-
82
-
83
70
  ## Client configuration examples
84
71
 
85
72
  Below are quick examples showing how to configure Claude Desktop to run the Node bundle locally or via Docker. Paste the appropriate JSON into your Claude Desktop configuration file (see Claude docs for exact path on your OS).
@@ -137,6 +124,11 @@ docker build -t conviso-mcp-node-image .
137
124
 
138
125
  ---
139
126
 
140
- ## Privacy
127
+ ## Privacy Policy
128
+
129
+ This connector communicates only with the Conviso Platform API (`https://app.convisoappsec.com`)
130
+ using the API key you provide. It does not collect, store, or share your data with any third
131
+ party — requests and responses stay between your MCP client and the Conviso Platform. Error
132
+ logs go to `stderr` only.
141
133
 
142
- This bundle references the project's privacy policy. See `node/manifest.json` privacy_policies for details.
134
+ Full privacy policy: https://www.iubenda.com/privacy-policy/55589285
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@convisoappsec/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "MCP Server for Conviso Platform integration",
5
5
  "type": "module",
6
6
  "main": "src/conviso_mcp/server.js",
@@ -1,4 +1,5 @@
1
1
  import axios from 'axios';
2
+ import https from 'node:https';
2
3
  import * as F from './filters.js';
3
4
  import { buildMutationQuery } from './mutations.js';
4
5
 
@@ -450,6 +451,16 @@ export function buildProjectsVariables(companyId, page = 1, limit = 1000, opts =
450
451
  return { page, limit, params, sortBy, descending };
451
452
  }
452
453
 
454
+ // Shared HTTP client: keep-alive reuses the TLS connection across the many sequential
455
+ // calls an agent session makes; the timeout stops a hung upstream from hanging a tool
456
+ // call (and the MCP client) forever.
457
+ const httpClient = axios.create({
458
+ timeout: 30_000,
459
+ httpsAgent: new https.Agent({ keepAlive: true }),
460
+ });
461
+
462
+ const RETRYABLE_STATUS = new Set([429, 502, 503]);
463
+
453
464
  class GraphQLClient {
454
465
  constructor(endpoint, apiKey) {
455
466
  this.endpoint = endpoint;
@@ -462,16 +473,30 @@ class GraphQLClient {
462
473
  }
463
474
 
464
475
  async execute(query, variables = {}) {
465
- const payload = { query, variables };
476
+ // Queries are idempotent retry a transient failure once. Mutations never retry.
477
+ const isRead = /^\s*query\b/i.test(query);
478
+ try {
479
+ return await this.#post(query, variables);
480
+ } catch (err) {
481
+ const transient = RETRYABLE_STATUS.has(err.status) || err.code === 'ECONNRESET';
482
+ if (!isRead || !transient) throw err;
483
+ await new Promise((r) => setTimeout(r, 300));
484
+ return this.#post(query, variables);
485
+ }
486
+ }
466
487
 
488
+ async #post(query, variables) {
467
489
  let response;
468
-
469
490
  try {
470
- response = await axios.post(this.endpoint, payload, { headers: this.headers });
491
+ response = await httpClient.post(this.endpoint, { query, variables }, { headers: this.headers });
471
492
  } catch (err) {
472
493
  if (err.response) {
494
+ const status = err.response.status;
473
495
  const e = new Error('GraphQL request failed');
474
- e.status = err.response.status;
496
+ e.status = status;
497
+ if (status === 401 || status === 403) {
498
+ e.authHint = 'Authentication failed — check that CONVISO_API_KEY is set to a valid Conviso Platform API key.';
499
+ }
475
500
  throw e;
476
501
  }
477
502
 
@@ -481,7 +506,7 @@ class GraphQLClient {
481
506
  throw e;
482
507
  }
483
508
 
484
- if (err.code === 'ETIMEDOUT') {
509
+ if (err.code === 'ETIMEDOUT' || err.code === 'ECONNABORTED') {
485
510
  const e = new Error('Upstream request timeout');
486
511
  e.status = 504;
487
512
  throw e;
@@ -508,18 +533,6 @@ class GraphQLClient {
508
533
  return this.execute(ISSUES_QUERY, variables);
509
534
  }
510
535
 
511
- // Backward-compatible positional-argument wrapper used by existing FeedGateway callers.
512
- async get_issues(company_id, search, page = 1, limit = 1, project_id = null, issue_ids = [], asset_ids = []) {
513
- return this.getIssues(company_id, {
514
- page,
515
- limit,
516
- search,
517
- projectId: project_id,
518
- issueIds: issue_ids,
519
- assetIds: asset_ids,
520
- });
521
- }
522
-
523
536
  async get_issue_by_id(issue_id, return_snippets = false) {
524
537
  let query = `
525
538
  query GetIssue($id: ID!) {
@@ -544,6 +557,29 @@ class GraphQLClient {
544
557
  return this.execute(PROJECTS_QUERY, variables);
545
558
  }
546
559
 
560
+ async get_project_types(search = null) {
561
+ const query = `
562
+ query GetProjectTypes($page: Int, $limit: Int, $params: ProjectTypeSearch) {
563
+ projectTypes(page: $page, limit: $limit, params: $params) {
564
+ collection { id code label description defaultDuration }
565
+ metadata { totalCount totalPages currentPage limitValue }
566
+ }
567
+ }`;
568
+ const params = search ? { labelCont: search } : undefined;
569
+ return this.execute(query, { page: 1, limit: 100, params });
570
+ }
571
+
572
+ async get_project_statuses() {
573
+ const query = `
574
+ query GetProjectStatuses($page: Int, $limit: Int) {
575
+ projectStatuses(page: $page, limit: $limit) {
576
+ collection { id label isInitial }
577
+ metadata { totalCount }
578
+ }
579
+ }`;
580
+ return this.execute(query, { page: 1, limit: 100 });
581
+ }
582
+
547
583
  async get_project_by_id(project_id) {
548
584
  const query = `
549
585
  query GetProject($id: ID!) {
@@ -687,56 +723,6 @@ class GraphQLClient {
687
723
  return this.execute(query, variables);
688
724
  }
689
725
 
690
- async generate_project_report(project_id, language = 'en', vulnerability_criticity = null, vulnerability_statuses = null, requirements = true, evidences = true) {
691
- const query = `
692
- query GenerateProjectReport(
693
- $projectId: ID!,
694
- $language: String!,
695
- $vulnerabilityCriticity: [SeverityCategory!],
696
- $vulnerabilityStatuses: [IssueStatusLabel!],
697
- $requirements: Boolean!,
698
- $evidences: Boolean!
699
- ) {
700
- generateProjectReport(
701
- projectId: $projectId,
702
- language: $language,
703
- vulnerabilityCriticity: $vulnerabilityCriticity,
704
- vulnerabilityStatuses: $vulnerabilityStatuses,
705
- requirements: $requirements,
706
- evidences: $evidences
707
- ) {
708
- id
709
- reportUrl
710
- status
711
- }
712
- }
713
- `;
714
- const variables = {
715
- projectId: project_id,
716
- language,
717
- vulnerabilityCriticity: vulnerability_criticity || ["CRITICAL", "HIGH", "MEDIUM", "LOW", "NOTIFICATION"],
718
- vulnerabilityStatuses: vulnerability_statuses || ["IDENTIFIED", "IN_PROGRESS", "AWAITING_VALIDATION", "FIX_ACCEPTED", "RISK_ACCEPTED", "FALSE_POSITIVE"],
719
- requirements,
720
- evidences
721
- };
722
- return this.execute(query, variables);
723
- }
724
-
725
- async generate_project_report_progress(project_id, report_id) {
726
- const query = `
727
- query GenerateProjectReport($projectId: ID!, $reportId: ID!) {
728
- projectReport(projectId: $projectId, reportId: $reportId) {
729
- id
730
- progress
731
- reportUrl
732
- status
733
- }
734
- }
735
- `;
736
- const variables = { projectId: project_id, reportId: report_id };
737
- return this.execute(query, variables);
738
- }
739
-
740
726
  // --- Mutations -------------------------------------------------------------
741
727
 
742
728
  // Generic engine: run any catalogued mutation. Builds the GraphQL document from the
@@ -107,6 +107,15 @@ export function buildMutationQuery(name, variables = {}, returnFields = null) {
107
107
  if (!m) {
108
108
  throw new Error(`Unknown mutation '${name}'. Call list_mutations to discover valid names.`);
109
109
  }
110
+ // Every catalogued mutation takes a single `input` object. Callers often pass the input
111
+ // fields bare (without the { input: ... } wrapper) — accept that instead of failing.
112
+ if (
113
+ m.args.length === 1 && m.args[0].name === 'input' &&
114
+ variables && typeof variables === 'object' && !Array.isArray(variables) &&
115
+ !('input' in variables) && Object.keys(variables).length > 0
116
+ ) {
117
+ variables = { input: variables };
118
+ }
110
119
  const opName = name.charAt(0).toUpperCase() + name.slice(1);
111
120
  const decls = m.args.map((a) => `$${a.name}: ${a.type}`).join(', ');
112
121
  const pass = m.args.map((a) => `${a.name}: $${a.name}`).join(', ');