@convisoappsec/mcp 0.3.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Conviso
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ <img src="assets/conviso_banner.png" width="128">
2
+
3
+ # Conviso MCP Server
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.
6
+
7
+ ## 🛠 Available Tools (Capabilities)
8
+
9
+ The server exposes the following tools to the LLM (see `node/manifest.json` for the authoritative definitions):
10
+
11
+ | Category | Tool | Description |
12
+ | --- | --- | --- |
13
+ | **General** | `get_companies` | List companies and associated IDs. |
14
+ | **General** | `get_company_info` | Plan details, integrations, and company branding info. |
15
+ | **Vulnerabilities** | `get_issues` | List vulnerabilities by company or project. |
16
+ | **Vulnerabilities** | `get_issue` | Technical details, including **code snippets** and raw requests/responses. |
17
+ | **Vulnerabilities** | `get_top_vulnerabilities` | Risk overview (vulnerability count by severity). |
18
+ | **Management** | `get_projects` | List active security projects. |
19
+ | **Management** | `get_project` | Get specific project in Conviso Platform by project ID. |
20
+ | **Assets** | `get_assets` | List assets mapped within the platform. |
21
+ | **Assets** | `get_asset` | Get asset in Conviso Platform by asset ID. |
22
+ | **Utilities** | `create_issue_url` | Generates a direct link to the specific issue on the Conviso Platform. |
23
+ | **Utilities** | `create_project_url` | Generates a direct link to the specific project on the Conviso Platform. |
24
+ | **Utilities** | `get_today_date` | Return current day/month/year (utility). |
25
+ | **Metrics** | `get_mttr_over_time` | Get Mean Time To Resolution (MTTR) metrics over time for a company. Returns resolution times by severity level. |
26
+ | **Metrics** | `get_overall_risk_score_history` | Get overall risk score history for a company, including current score and difference from last period. |
27
+
28
+ ## 🚀 Installation and Configuration (Node.js bundle)
29
+
30
+ ### Prerequisites
31
+
32
+ * Node.js 18 or later.
33
+ * A Conviso Platform API Key (obtained from your profile settings).
34
+ * An MCP-compatible client (e.g., Claude Desktop, Cursor, etc.).
35
+
36
+ ### Setup
37
+
38
+ ```bash
39
+ git clone https://github.com/convisoappsec/conviso-mcp.git
40
+ cd conviso-mcp/node
41
+ npm install
42
+ ```
43
+
44
+ ### Run
45
+
46
+ ```bash
47
+ export CONVISO_API_KEY=your_api_key_here
48
+ npm start
49
+ ```
50
+
51
+ ## Staging (optional)
52
+
53
+ Set the `STAGING` environment variable to `true` to make the server use
54
+ `https://staging.convisoappsec.com` instead of the production API.
55
+
56
+ Example:
57
+
58
+ ```bash
59
+ # staging (testing only)
60
+ export STAGING=true
61
+ npm start
62
+ ```
63
+
64
+
65
+ ## Client configuration examples
66
+
67
+ 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).
68
+
69
+ ### A. Node.js (Local)
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "conviso-mcp": {
75
+ "command": "node",
76
+ "args": [
77
+ "/ABSOLUTE/PATH/TO/REPO/node/src/conviso_mcp/server.js"
78
+ ],
79
+ "env": {
80
+ "CONVISO_API_KEY": "your_api_key_here"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ Notes:
88
+ - Replace `/ABSOLUTE/PATH/TO/REPO` with the absolute path to your cloned repository.
89
+ - Ensure `node` is available in the PATH used by Claude Desktop.
90
+
91
+ ### B. Docker (Node bundle)
92
+
93
+ ```json
94
+ {
95
+ "mcpServers": {
96
+ "conviso-mcp": {
97
+ "command": "docker",
98
+ "args": [
99
+ "run",
100
+ "-i",
101
+ "--rm",
102
+ "--init",
103
+ "-e",
104
+ "CONVISO_API_KEY=your_api_key_here",
105
+ "conviso-mcp-node-image"
106
+ ]
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ Notes:
113
+ - Build the Docker image first (example):
114
+
115
+ ```bash
116
+ cd node
117
+ docker build -t conviso-mcp-node-image .
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Privacy
123
+
124
+ This bundle references the project's privacy policy. See `node/manifest.json` privacy_policies for details.
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@convisoappsec/mcp",
3
+ "version": "0.3.0",
4
+ "description": "MCP Server for Conviso Platform integration",
5
+ "type": "module",
6
+ "main": "src/conviso_mcp/server.js",
7
+ "scripts": {
8
+ "start": "node src/conviso_mcp/server.js",
9
+ "dev": "node src/conviso_mcp/server.js",
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "bin": {
13
+ "conviso-mcp": "src/conviso_mcp/server.js"
14
+ },
15
+ "files": [
16
+ "src"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/convisoappsec/conviso-mcp.git"
27
+ },
28
+ "keywords": [
29
+ "appsec",
30
+ "mcp",
31
+ "llm",
32
+ "ai"
33
+ ],
34
+ "author": {
35
+ "name": "Conviso",
36
+ "url": "https://github.com/convisoappsec"
37
+ },
38
+ "license": "MIT",
39
+ "bugs": {
40
+ "url": "https://github.com/convisoappsec/conviso-mcp/issues"
41
+ },
42
+ "homepage": "https://github.com/convisoappsec/conviso-mcp#readme",
43
+ "dependencies": {
44
+ "@hono/node-server": "^1.19.11",
45
+ "@modelcontextprotocol/sdk": "^1.28.0",
46
+ "axios": "^1.13.5",
47
+ "dotenv": "^16.6.1",
48
+ "zod": "^4.3.6"
49
+ }
50
+ }
@@ -0,0 +1,80 @@
1
+ import 'dotenv/config';
2
+ import { GraphQLClient } from './graphql_client.js';
3
+
4
+ class FeedGateway {
5
+ constructor(graphql_api_key = '') {
6
+ this.base_url = 'https://app.convisoappsec.com';
7
+ const apiKey = process.env.CONVISO_API_KEY || graphql_api_key || '';
8
+ this.graphql = new GraphQLClient(`${this.base_url}/graphql`, apiKey);
9
+ }
10
+
11
+ async get_companies(page = 1, limit = 10, search = '') {
12
+ return this.graphql.get_companies(page, limit, search);
13
+ }
14
+
15
+ async get_company_by_id(company_id) {
16
+ return this.graphql.get_company_by_id(company_id);
17
+ }
18
+
19
+ async get_issue_by_id(issue_id, return_snippets = false) {
20
+ return this.graphql.get_issue_by_id(issue_id, return_snippets);
21
+ }
22
+
23
+ async get_issues(company_id, search = '', page = 1, limit = 1, project_id = null) {
24
+ return this.graphql.get_issues(company_id, search, page, limit, project_id);
25
+ }
26
+
27
+ async get_issue_with_company_id(company_id, issue_id) {
28
+ return this.graphql.get_issues(company_id, '', 1, 5, null, [issue_id]);
29
+ }
30
+
31
+ async get_issues_by_asset_ids(company_id, page = 1, limit = 1, asset_ids = []) {
32
+ return this.graphql.get_issues(company_id, '', page, limit, null, [], asset_ids);
33
+ }
34
+
35
+ async get_projects(company_id, page = 1, limit = 1000, search = '') {
36
+ return this.graphql.get_projects(company_id, page, limit, search);
37
+ }
38
+
39
+ async get_project_by_id(project_id) {
40
+ return this.graphql.get_project_by_id(project_id);
41
+ }
42
+
43
+ async get_assets(company_id, page = 1, limit = 1000) {
44
+ return this.graphql.get_assets_by_company(company_id, page, limit);
45
+ }
46
+
47
+ async get_asset_by_id(asset_id) {
48
+ return this.graphql.get_asset_by_id(asset_id);
49
+ }
50
+
51
+ async get_top_vulnerabilities(company_id) {
52
+ return this.graphql.get_top_vulnerabilities(company_id);
53
+ }
54
+
55
+ async generate_project_report(project_id, language = 'en', vulnerability_criticity = null, vulnerability_statuses = null, requirements = true, evidences = true) {
56
+ return this.graphql.generate_project_report(project_id, language, vulnerability_criticity, vulnerability_statuses, requirements, evidences);
57
+ }
58
+
59
+ async generate_project_report_progress(project_id, report_id) {
60
+ return this.graphql.generate_project_report_progress(project_id, report_id);
61
+ }
62
+
63
+ create_project_url(company_id, project_id) {
64
+ return `${this.base_url}/spa/company/${company_id}/projects/${project_id}`;
65
+ }
66
+
67
+ create_issue_url(company_id, issue_id) {
68
+ return `${this.base_url}/spa/company/${company_id}/vulnerabilities?title=&search=${issue_id}`;
69
+ }
70
+
71
+ async get_mttr_over_time(company_id, start_date, end_date, severities = null, statuses = null, asset_ids = null, asset_tags = null) {
72
+ return this.graphql.get_mttr_over_time(company_id, start_date, end_date, severities, statuses, asset_ids, asset_tags);
73
+ }
74
+
75
+ async get_overall_risk_score_history(company_id) {
76
+ return this.graphql.get_overall_risk_score_history(company_id);
77
+ }
78
+ }
79
+
80
+ export { FeedGateway };
@@ -0,0 +1,535 @@
1
+ import axios from 'axios';
2
+
3
+ const GraphQLFieldTemplates = {
4
+ complete_issue: `
5
+ id
6
+ title
7
+ description
8
+ status
9
+ severity
10
+ createdAt
11
+ updatedAt
12
+ category
13
+ patterns
14
+ reference
15
+ permittedStatus
16
+ statusHistory {
17
+ status
18
+ createdAt
19
+ }
20
+ asset {
21
+ id
22
+ name
23
+ }
24
+ project {
25
+ id
26
+ label
27
+
28
+ company {
29
+ id
30
+ }
31
+ }
32
+ author {
33
+ id
34
+ name
35
+ email
36
+ }
37
+ `,
38
+
39
+ complete_issue_with_snippet: null,
40
+
41
+ project: `
42
+ id
43
+ label
44
+ status
45
+ createdAt
46
+ startDate
47
+ endDate
48
+ allocatedAnalyst {
49
+ portalUser {
50
+ name
51
+ }
52
+ }
53
+ projectType {
54
+ label
55
+ }
56
+
57
+ company {
58
+ id
59
+ }
60
+ `
61
+ };
62
+
63
+ GraphQLFieldTemplates.complete_issue_with_snippet = `
64
+ ${GraphQLFieldTemplates.complete_issue}
65
+ ... on FindingInterface {
66
+ severity
67
+ solution
68
+ scanSource
69
+ originalIssueIdFromTool
70
+ }
71
+ ... on VulnerabilityInterface {
72
+ severity
73
+ impactDescription
74
+ solution
75
+ summary
76
+ stepsToReproduce
77
+ impactLevel
78
+ probabilityLevel
79
+ compromisedEnvironment
80
+ }
81
+ ... on DastFinding {
82
+ detail {
83
+ url
84
+ port
85
+ scheme
86
+ method
87
+ request
88
+ response
89
+ }
90
+ }
91
+ ... on WebVulnerability {
92
+ detail {
93
+ url
94
+ port
95
+ scheme
96
+ method
97
+ request
98
+ response
99
+ parameters
100
+ }
101
+ }
102
+ ... on SourceCodeVulnerability {
103
+ detail {
104
+ codeSnippet
105
+ fileName
106
+ vulnerableLine
107
+ firstLine
108
+ source
109
+ sink
110
+ }
111
+ }
112
+ ... on NetworkVulnerability {
113
+ detail {
114
+ address
115
+ port
116
+ protocol
117
+ attackVector
118
+ }
119
+ }
120
+ ... on ScaFinding {
121
+ detail {
122
+ affectedVersion
123
+ package
124
+ patchedVersion
125
+ cve
126
+ cvssScore
127
+ cvssMetric
128
+ fileName
129
+ }
130
+ }
131
+ ... on SastFinding {
132
+ detail {
133
+ codeSnippet
134
+ fileName
135
+ vulnerableLine
136
+ firstLine
137
+ }
138
+ }
139
+ ... on IacFinding {
140
+ detail {
141
+ codeSnippet
142
+ fileName
143
+ vulnerableLine
144
+ firstLine
145
+ }
146
+ }
147
+ ... on SecretFinding {
148
+ detail {
149
+ codeSnippet
150
+ fileName
151
+ vulnerableLine
152
+ firstLine
153
+ }
154
+ }
155
+ ... on ContainerFinding {
156
+ detail {
157
+ affectedVersion
158
+ package
159
+ patchedVersion
160
+ cve
161
+ }
162
+ }
163
+ `;
164
+
165
+ class GraphQLClient {
166
+ constructor(endpoint, apiKey) {
167
+ this.endpoint = endpoint;
168
+ this.headers = {
169
+ 'Content-Type': 'application/json'
170
+ };
171
+ if (apiKey) {
172
+ this.headers['X-Api-Key'] = `${apiKey}`;
173
+ }
174
+ }
175
+
176
+ async execute(query, variables = {}) {
177
+ const payload = { query, variables };
178
+
179
+ let response;
180
+
181
+ try {
182
+ response = await axios.post(this.endpoint, payload, { headers: this.headers });
183
+ } catch (err) {
184
+ if (err.response) {
185
+ const e = new Error('GraphQL request failed');
186
+ e.status = err.response.status;
187
+ throw e;
188
+ }
189
+
190
+ if (err.code === 'ECONNREFUSED') {
191
+ const e = new Error('Upstream service unavailable');
192
+ e.status = 503;
193
+ throw e;
194
+ }
195
+
196
+ if (err.code === 'ETIMEDOUT') {
197
+ const e = new Error('Upstream request timeout');
198
+ e.status = 504;
199
+ throw e;
200
+ }
201
+
202
+ throw err;
203
+ }
204
+
205
+ if (response.data?.errors) {
206
+ const e = new Error('GraphQL error');
207
+ e.status = 400;
208
+ throw e;
209
+ }
210
+
211
+ return response.data.data;
212
+ }
213
+
214
+ async get_issues(company_id, search, page = 1, limit = 1, project_id = null, issue_ids = [], asset_ids = []) {
215
+ const query = `
216
+ query GetIssues($companyId: ID!, $pagination: PaginationInput!, $filters: IssuesFiltersInput) {
217
+ issues(companyId: $companyId, pagination: $pagination, filters: $filters) {
218
+ collection {
219
+ id
220
+ title
221
+ severity
222
+ project {
223
+ company {
224
+ id
225
+ }
226
+ }
227
+
228
+ asset {
229
+ id
230
+ }
231
+ }
232
+ }
233
+ }
234
+ `;
235
+
236
+ const variables = {
237
+ companyId: company_id,
238
+ filters: { title: search },
239
+ pagination: { page: page, perPage: limit }
240
+ };
241
+
242
+ if (project_id !== null && project_id !== 0) {
243
+ variables.filters.projectIds = [project_id];
244
+ }
245
+ if (Array.isArray(issue_ids) && issue_ids.length > 0) {
246
+ variables.filters.ids = issue_ids;
247
+ }
248
+ if (Array.isArray(asset_ids) && asset_ids.length > 0) {
249
+ variables.filters.assetIds = asset_ids;
250
+ }
251
+
252
+ return this.execute(query, variables);
253
+ }
254
+
255
+ async get_issue_by_id(issue_id, return_snippets = false) {
256
+ let query = `
257
+ query GetIssue($id: ID!) {
258
+ issue(id: $id) {
259
+ %s
260
+ }
261
+ }
262
+ `;
263
+ query = query.replace('%s', return_snippets ? GraphQLFieldTemplates.complete_issue_with_snippet : GraphQLFieldTemplates.complete_issue);
264
+ const variables = { id: issue_id };
265
+ return this.execute(query, variables);
266
+ }
267
+
268
+ async get_companies(page = 1, limit = 10, search = '') {
269
+ const query = `
270
+ query companies($page: Int, $limit: Int, $params: CompanySearch, $order: OrderScopesParams, $orderType: OrderParams){
271
+ companies(page: $page, limit: $limit, params: $params, order: $order, orderType : $orderType) {
272
+ collection {
273
+ id
274
+ label
275
+ }
276
+ }
277
+ }
278
+ `;
279
+ const variables = { page, limit, params: { labelCont: search } };
280
+ return this.execute(query, variables);
281
+ }
282
+
283
+ async get_projects(company_id, page = 1, limit = 1000, search = '') {
284
+ const query = `
285
+ query projects($page: Int, $limit: Int, $params: ProjectSearch, $sortBy: String, $descending: Boolean){
286
+ projects(page: $page, limit: $limit, params: $params, sortBy: $sortBy, descending : $descending) {
287
+ collection {
288
+ id
289
+ label
290
+ status
291
+ createdAt
292
+ startDate
293
+ endDate
294
+ allocatedAnalyst {
295
+ portalUser {
296
+ name
297
+ }
298
+ }
299
+ projectType {
300
+ label
301
+ }
302
+
303
+ company {
304
+ id
305
+ }
306
+ }
307
+ }
308
+ }
309
+ `;
310
+ const variables = {
311
+ page,
312
+ limit,
313
+ params: { scopeIdEq: company_id, labelCont: search },
314
+ sortBy: 'createdAt',
315
+ descending: true
316
+ };
317
+ return this.execute(query, variables);
318
+ }
319
+
320
+ async get_project_by_id(project_id) {
321
+ const query = `
322
+ query GetProject($id: ID!) {
323
+ project(id: $id) {
324
+ ${GraphQLFieldTemplates.project}
325
+ }
326
+ }
327
+ `;
328
+ const variables = { id: project_id };
329
+ return this.execute(query, variables);
330
+ }
331
+
332
+ async get_company_by_id(company_id) {
333
+ const query = `
334
+ query Company($id: ID!) {
335
+ company(id: $id) {
336
+ id
337
+ label
338
+ sid
339
+ brandUrl
340
+ brandId
341
+ brandFilename
342
+ brandSize
343
+ customFeatures
344
+ integrations
345
+ createdAt
346
+ updatedAt
347
+ configured
348
+ companyPlan {
349
+ name
350
+ id
351
+ }
352
+ }
353
+ }
354
+ `;
355
+ const variables = { id: company_id };
356
+ return this.execute(query, variables);
357
+ }
358
+
359
+ async get_assets_by_company(company_id, page = 1, limit = 10) {
360
+ const query = `
361
+ query ListAssets($companyId: ID!, $page: Int, $limit: Int) {
362
+ assets(companyId: $companyId, page: $page, limit: $limit) {
363
+ collection {
364
+ id
365
+ name
366
+ assetType
367
+ environment
368
+ audience
369
+ createdAt
370
+ updatedAt
371
+ }
372
+ metadata {
373
+ totalCount
374
+ totalPages
375
+ currentPage
376
+ limitValue
377
+ }
378
+ }
379
+ }
380
+ `;
381
+ const variables = { companyId: company_id, page, limit };
382
+ return this.execute(query, variables);
383
+ }
384
+
385
+ async get_asset_by_id(asset_id) {
386
+ const query = `
387
+ query asset($id: ID!) {
388
+ asset(id: $id) {
389
+ id
390
+ name
391
+ assetType
392
+ businessImpact
393
+ architectureType
394
+ technologies
395
+ environment
396
+ audience
397
+ description
398
+ createdAt
399
+ updatedAt
400
+ company {
401
+ id
402
+ }
403
+ riskScore{
404
+ current{
405
+ value
406
+ }
407
+ }
408
+ }
409
+ }
410
+ `;
411
+ const variables = { id: asset_id };
412
+ return this.execute(query, variables);
413
+ }
414
+
415
+ async get_top_vulnerabilities(company_id) {
416
+ const query = `
417
+ query TopVulnerabilities($companyId: ID!) {
418
+ topVulnerabilities(companyId: $companyId) {
419
+ affectedAssetsCount
420
+ criticalCount
421
+ highCount
422
+ lowCount
423
+ mediumCount
424
+ title
425
+ totalCount
426
+ }
427
+ }
428
+ `;
429
+ const variables = { companyId: company_id };
430
+ return this.execute(query, variables);
431
+ }
432
+
433
+ async get_mttr_over_time(company_id, start_date, end_date, severities = null, statuses = null, asset_ids = null, asset_tags = null) {
434
+ const query = `
435
+ query MttrOverTime($companyId: ID!, $params: FilterParams!) {
436
+ mttrOverTime(companyId: $companyId, params: $params) {
437
+ all
438
+ critical
439
+ dates
440
+ high
441
+ low
442
+ medium
443
+ notification
444
+ }
445
+ }
446
+ `;
447
+ const variables = {
448
+ companyId: company_id,
449
+ params: {
450
+ startDate: start_date,
451
+ endDate: end_date,
452
+ severities: severities || ["NOTIFICATION", "LOW", "MEDIUM", "HIGH", "CRITICAL"],
453
+ statuses: statuses || ["CREATED", "DRAFT", "IDENTIFIED", "IN_PROGRESS", "AWAITING_VALIDATION", "FIX_ACCEPTED", "RISK_ACCEPTED", "FALSE_POSITIVE", "SUPPRESSED"],
454
+ assetIds: asset_ids || [],
455
+ assetTags: asset_tags || []
456
+ }
457
+ };
458
+ return this.execute(query, variables);
459
+ }
460
+
461
+ async get_overall_risk_score_history(company_id) {
462
+ const query = `
463
+ query OverallRiskScoreHistory($companyId: ID!) {
464
+ overallRiskScoreHistory(companyId: $companyId) {
465
+ company {
466
+ id
467
+ label
468
+ }
469
+ current {
470
+ date
471
+ value
472
+ }
473
+ differenceFromLast {
474
+ date
475
+ value
476
+ }
477
+ }
478
+ }
479
+ `;
480
+ const variables = { companyId: company_id };
481
+ return this.execute(query, variables);
482
+ }
483
+
484
+ async generate_project_report(project_id, language = 'en', vulnerability_criticity = null, vulnerability_statuses = null, requirements = true, evidences = true) {
485
+ const query = `
486
+ query GenerateProjectReport(
487
+ $projectId: ID!,
488
+ $language: String!,
489
+ $vulnerabilityCriticity: [SeverityCategory!],
490
+ $vulnerabilityStatuses: [IssueStatusLabel!],
491
+ $requirements: Boolean!,
492
+ $evidences: Boolean!
493
+ ) {
494
+ generateProjectReport(
495
+ projectId: $projectId,
496
+ language: $language,
497
+ vulnerabilityCriticity: $vulnerabilityCriticity,
498
+ vulnerabilityStatuses: $vulnerabilityStatuses,
499
+ requirements: $requirements,
500
+ evidences: $evidences
501
+ ) {
502
+ id
503
+ reportUrl
504
+ status
505
+ }
506
+ }
507
+ `;
508
+ const variables = {
509
+ projectId: project_id,
510
+ language,
511
+ vulnerabilityCriticity: vulnerability_criticity || ["CRITICAL", "HIGH", "MEDIUM", "LOW", "NOTIFICATION"],
512
+ vulnerabilityStatuses: vulnerability_statuses || ["IDENTIFIED", "IN_PROGRESS", "AWAITING_VALIDATION", "FIX_ACCEPTED", "RISK_ACCEPTED", "FALSE_POSITIVE"],
513
+ requirements,
514
+ evidences
515
+ };
516
+ return this.execute(query, variables);
517
+ }
518
+
519
+ async generate_project_report_progress(project_id, report_id) {
520
+ const query = `
521
+ query GenerateProjectReport($projectId: ID!, $reportId: ID!) {
522
+ projectReport(projectId: $projectId, reportId: $reportId) {
523
+ id
524
+ progress
525
+ reportUrl
526
+ status
527
+ }
528
+ }
529
+ `;
530
+ const variables = { projectId: project_id, reportId: report_id };
531
+ return this.execute(query, variables);
532
+ }
533
+ }
534
+
535
+ export { GraphQLClient };
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env node
2
+
3
+ import 'dotenv/config';
4
+ import http from 'node:http';
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
+ import { z } from 'zod';
9
+
10
+ import { FeedGateway } from './feed_gateway.js';
11
+ import pkg from '../../package.json' with { type: 'json' };
12
+
13
+ const gateway = new FeedGateway();
14
+
15
+ console.error('[+] Starting Conviso MCP Server (MCP SDK)');
16
+
17
+ const server = new McpServer({
18
+ name: pkg.name || 'conviso-mcp',
19
+ version: pkg.version || '0.3.0',
20
+ });
21
+
22
+ function sanitizeError(err, message = 'Request failed') {
23
+ const error_id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
24
+
25
+ const status =
26
+ err?.response?.status ??
27
+ err?.response?.statusCode ??
28
+ err?.status ??
29
+ err?.statusCode ??
30
+ (err?.code === 'ECONNREFUSED' ? 503 : undefined) ??
31
+ (err?.code === 'ETIMEDOUT' ? 504 : undefined) ??
32
+ 500;
33
+
34
+ console.error('[tool_error]', {
35
+ error_id,
36
+ name: err?.name,
37
+ code: err?.code,
38
+ message: err?.message?.slice(0, 200),
39
+ status,
40
+ });
41
+
42
+ return { error: message, status, error_id };
43
+ }
44
+
45
+ function ok(data) {
46
+ return {
47
+ content: [
48
+ {
49
+ type: 'text',
50
+ text: typeof data === 'string' ? data : JSON.stringify(data),
51
+ },
52
+ ],
53
+ };
54
+ }
55
+
56
+ function fail(err, msg) {
57
+ return ok(sanitizeError(err, msg));
58
+ }
59
+
60
+ server.registerTool(
61
+ 'get_companies',
62
+ {
63
+ description: 'Return a paginated list of companies accessible with the provided API key. Use `search` to filter by company name.',
64
+ inputSchema: z.object({
65
+ page: z.number().optional(),
66
+ limit: z.number().optional(),
67
+ search: z.string().optional(),
68
+ }),
69
+ annotations: {
70
+ title: 'List Companies',
71
+ readOnlyHint: true,
72
+ destructiveHint: false,
73
+ idempotentHint: true,
74
+ openWorldHint: true,
75
+ },
76
+ },
77
+ async ({ page = 1, limit = 10, search = '' }) => {
78
+ try {
79
+ return ok(await gateway.get_companies(page, limit, search));
80
+ } catch (err) {
81
+ return fail(err, 'Failed to list companies');
82
+ }
83
+ }
84
+ );
85
+
86
+ server.registerTool(
87
+ 'get_company_info',
88
+ {
89
+ description: 'Retrieve detailed information about a specific company, including plan, integrations, and branding metadata.',
90
+ inputSchema: z.object({ company_id: z.number() }),
91
+ annotations: {
92
+ title: 'Company Details',
93
+ readOnlyHint: true,
94
+ destructiveHint: false,
95
+ idempotentHint: true,
96
+ openWorldHint: true,
97
+ },
98
+ },
99
+ async ({ company_id }) => {
100
+ try {
101
+ return ok(await gateway.get_company_by_id(company_id));
102
+ } catch (err) {
103
+ return fail(err, 'Failed to get company info');
104
+ }
105
+ }
106
+ );
107
+
108
+ server.registerTool(
109
+ 'get_issue',
110
+ {
111
+ description: 'Fetch detailed technical data for a specific vulnerability/issue. Optionally include raw request/response and vulnerable code snippets when `return_vulnerable_data` is true. WARNING: setting `return_vulnerable_data=true` may return sensitive data (exploit code, raw HTTP requests/responses, or secrets) — use with caution.',
112
+ inputSchema: z.object({
113
+ id: z.number(),
114
+ return_vulnerable_data: z.boolean().optional(),
115
+ }),
116
+ annotations: {
117
+ title: 'Issue Details',
118
+ readOnlyHint: true,
119
+ destructiveHint: false,
120
+ idempotentHint: true,
121
+ openWorldHint: true,
122
+ },
123
+ },
124
+ async ({ id, return_vulnerable_data }) => {
125
+ try {
126
+ return ok(await gateway.get_issue_by_id(id, return_vulnerable_data));
127
+ } catch (err) {
128
+ return fail(err, 'Failed to get issue details');
129
+ }
130
+ }
131
+ );
132
+
133
+ server.registerTool(
134
+ 'get_issues',
135
+ {
136
+ description: 'List vulnerabilities for a company or project.',
137
+ inputSchema: z.object({
138
+ company_id: z.number(),
139
+ page: z.number().optional(),
140
+ limit: z.number().optional(),
141
+ project_id: z.number().optional(),
142
+ }),
143
+ annotations: {
144
+ title: 'List Issues',
145
+ readOnlyHint: true,
146
+ destructiveHint: false,
147
+ idempotentHint: true,
148
+ openWorldHint: true,
149
+ },
150
+ },
151
+ async ({ company_id, page = 1, limit = 10, project_id }) => {
152
+ try {
153
+ return ok(await gateway.get_issues(company_id, '', page, limit, project_id));
154
+ } catch (err) {
155
+ return fail(err, 'Failed to list issues');
156
+ }
157
+ }
158
+ );
159
+
160
+ server.registerTool(
161
+ 'get_top_vulnerabilities',
162
+ {
163
+ description: 'Return a summary of vulnerability counts grouped by severity for a given company (risk overview).',
164
+ inputSchema: z.object({ company_id: z.number() }),
165
+ annotations: {
166
+ title: 'Top Vulnerabilities',
167
+ readOnlyHint: true,
168
+ destructiveHint: false,
169
+ idempotentHint: true,
170
+ openWorldHint: true,
171
+ },
172
+ },
173
+ async ({ company_id }) => {
174
+ try {
175
+ return ok(await gateway.get_top_vulnerabilities(company_id));
176
+ } catch (err) {
177
+ return fail(err, 'Failed to get top vulnerabilities');
178
+ }
179
+ }
180
+ );
181
+
182
+ server.registerTool(
183
+ 'get_projects',
184
+ {
185
+ description: 'Return a paginated list of active security projects for a company. Defaults to 25 results per page to conserve tokens.',
186
+ inputSchema: z.object({
187
+ company_id: z.number(),
188
+ page: z.number().optional(),
189
+ limit: z.number().optional(),
190
+ search: z.string().optional(),
191
+ }),
192
+ annotations: {
193
+ title: 'List Projects',
194
+ readOnlyHint: true,
195
+ destructiveHint: false,
196
+ idempotentHint: true,
197
+ openWorldHint: true,
198
+ },
199
+ },
200
+ async ({ company_id, page = 1, limit = 25, search = '' }) => {
201
+ try {
202
+ return ok(await gateway.get_projects(company_id, page, limit, search));
203
+ } catch (err) {
204
+ return fail(err, 'Failed to list projects');
205
+ }
206
+ }
207
+ );
208
+
209
+ server.registerTool(
210
+ 'get_project',
211
+ {
212
+ description: 'Retrieve detailed metadata for a specific project by its ID.',
213
+ inputSchema: z.object({ project_id: z.number() }),
214
+ annotations: {
215
+ title: 'Project Details',
216
+ readOnlyHint: true,
217
+ destructiveHint: false,
218
+ idempotentHint: true,
219
+ openWorldHint: true,
220
+ },
221
+ },
222
+ async ({ project_id }) => {
223
+ try {
224
+ return ok(await gateway.get_project_by_id(project_id));
225
+ } catch (err) {
226
+ return fail(err, 'Failed to get project');
227
+ }
228
+ }
229
+ );
230
+
231
+ server.registerTool(
232
+ 'get_asset',
233
+ {
234
+ description: 'Fetch information about a specific asset by its ID.',
235
+ inputSchema: z.object({ asset_id: z.number() }),
236
+ annotations: {
237
+ title: 'Asset Details',
238
+ readOnlyHint: true,
239
+ destructiveHint: false,
240
+ idempotentHint: true,
241
+ openWorldHint: true,
242
+ },
243
+ },
244
+ async ({ asset_id }) => {
245
+ try {
246
+ return ok(await gateway.get_asset_by_id(asset_id));
247
+ } catch (err) {
248
+ return fail(err, 'Failed to get asset');
249
+ }
250
+ }
251
+ );
252
+
253
+ server.registerTool(
254
+ 'get_assets',
255
+ {
256
+ description: 'Return a paginated list of assets for a company. Defaults to 25 results per page to reduce token usage.',
257
+ inputSchema: z.object({
258
+ company_id: z.number(),
259
+ page: z.number().optional(),
260
+ limit: z.number().optional(),
261
+ }),
262
+ annotations: {
263
+ title: 'List Assets',
264
+ readOnlyHint: true,
265
+ destructiveHint: false,
266
+ idempotentHint: true,
267
+ openWorldHint: true,
268
+ },
269
+ },
270
+ async ({ company_id, page = 1, limit = 25 }) => {
271
+ try {
272
+ return ok(await gateway.get_assets(company_id, page, limit));
273
+ } catch (err) {
274
+ return fail(err, 'Failed to list assets');
275
+ }
276
+ }
277
+ );
278
+
279
+ server.registerTool(
280
+ 'create_project_url',
281
+ {
282
+ description: 'Return a direct URL to open a project in the Conviso Platform for quick navigation.',
283
+ inputSchema: z.object({
284
+ company_id: z.number(),
285
+ project_id: z.number(),
286
+ }),
287
+ annotations: {
288
+ title: 'Project URL Generator',
289
+ readOnlyHint: true,
290
+ destructiveHint: false,
291
+ idempotentHint: true,
292
+ openWorldHint: true,
293
+ },
294
+ },
295
+ async ({ company_id, project_id }) => {
296
+ try {
297
+ return ok(await gateway.create_project_url(company_id, project_id));
298
+ } catch (err) {
299
+ return fail(err, 'Failed to create project URL');
300
+ }
301
+ }
302
+ );
303
+
304
+ server.registerTool(
305
+ 'create_issue_url',
306
+ {
307
+ description: 'Return a direct URL to open a specific issue in the Conviso Platform for triage or review.',
308
+ inputSchema: z.object({
309
+ company_id: z.number(),
310
+ issue_id: z.number(),
311
+ }),
312
+ annotations: {
313
+ title: 'Issue URL Generator',
314
+ readOnlyHint: true,
315
+ destructiveHint: false,
316
+ idempotentHint: true,
317
+ openWorldHint: true,
318
+ },
319
+ },
320
+ async ({ company_id, issue_id }) => {
321
+ try {
322
+ return ok(await gateway.create_issue_url(company_id, issue_id));
323
+ } catch (err) {
324
+ return fail(err, 'Failed to create issue URL');
325
+ }
326
+ }
327
+ );
328
+
329
+ server.registerTool(
330
+ 'get_mttr_over_time',
331
+ {
332
+ description: 'Get Mean Time To Resolution (MTTR) aggregated over a date range. Supports filtering by severities, statuses, and assets.',
333
+ inputSchema: z.object({
334
+ company_id: z.number(),
335
+ start_date: z.string(),
336
+ end_date: z.string(),
337
+ severities: z.array(z.string()).optional(),
338
+ statuses: z.array(z.string()).optional(),
339
+ asset_ids: z.array(z.number()).optional(),
340
+ asset_tags: z.array(z.string()).optional(),
341
+ }),
342
+ annotations: {
343
+ title: 'MTTR Over Time',
344
+ readOnlyHint: true,
345
+ destructiveHint: false,
346
+ idempotentHint: true,
347
+ openWorldHint: true,
348
+ },
349
+ },
350
+ async (args) => {
351
+ try {
352
+ return ok(await gateway.get_mttr_over_time(
353
+ args.company_id,
354
+ args.start_date,
355
+ args.end_date,
356
+ args.severities,
357
+ args.statuses,
358
+ args.asset_ids,
359
+ args.asset_tags
360
+ ));
361
+ } catch (err) {
362
+ return fail(err, 'Failed to get MTTR metrics');
363
+ }
364
+ }
365
+ );
366
+
367
+ server.registerTool(
368
+ 'get_overall_risk_score_history',
369
+ {
370
+ description: 'Retrieve historical overall risk scores for a company, useful for trend analysis and reporting.',
371
+ inputSchema: z.object({
372
+ company_id: z.number(),
373
+ }),
374
+ annotations: {
375
+ title: 'Risk Score History',
376
+ readOnlyHint: true,
377
+ destructiveHint: false,
378
+ idempotentHint: true,
379
+ openWorldHint: true,
380
+ },
381
+ },
382
+ async ({ company_id }) => {
383
+ try {
384
+ return ok(await gateway.get_overall_risk_score_history(company_id));
385
+ } catch (err) {
386
+ return fail(err, 'Failed to get risk score history');
387
+ }
388
+ }
389
+ );
390
+
391
+ server.registerTool(
392
+ 'get_today_date',
393
+ {
394
+ description: 'Utility tool returning the current date.',
395
+ inputSchema: z.object({}),
396
+ annotations: {
397
+ title: 'Get Today Date',
398
+ readOnlyHint: true,
399
+ destructiveHint: false,
400
+ idempotentHint: true,
401
+ openWorldHint: false,
402
+ },
403
+ },
404
+ async () => {
405
+ try {
406
+ const d = new Date();
407
+ return ok({ day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear() });
408
+ } catch (err) {
409
+ return fail(err, 'Failed to get current date');
410
+ }
411
+ }
412
+ );
413
+
414
+ /**
415
+ * START
416
+ */
417
+
418
+ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : null;
419
+
420
+ if (PORT) {
421
+ const httpServer = http.createServer(async (req, res) => {
422
+ if (req.method !== 'POST' && req.method !== 'GET' && req.method !== 'DELETE') {
423
+ res.writeHead(405).end();
424
+ return;
425
+ }
426
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
427
+ await server.connect(transport);
428
+ await transport.handleRequest(req, res);
429
+ });
430
+
431
+ httpServer.listen(PORT, () => {
432
+ console.error(`Conviso MCP Server running on HTTP port ${PORT}`);
433
+ });
434
+ } else {
435
+ const transport = new StdioServerTransport();
436
+ await server.connect(transport);
437
+ console.error('Conviso MCP Server running on stdio');
438
+ }