@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 +20 -28
- package/package.json +1 -1
- package/src/conviso_mcp/graphql_client.js +53 -67
- package/src/conviso_mcp/mutations.js +9 -0
- package/src/conviso_mcp/server.js +434 -1108
- package/src/conviso_mcp/feed_gateway.js +0 -198
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Conviso MCP Server
|
|
4
4
|
|
|
5
|
-
This
|
|
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
|
-
| **
|
|
37
|
-
| **
|
|
38
|
-
| **
|
|
39
|
-
| **
|
|
40
|
-
| **
|
|
41
|
-
| **
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
134
|
+
Full privacy policy: https://www.iubenda.com/privacy-policy/55589285
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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(', ');
|