@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 +21 -0
- package/README.md +124 -0
- package/package.json +50 -0
- package/src/conviso_mcp/feed_gateway.js +80 -0
- package/src/conviso_mcp/graphql_client.js +535 -0
- package/src/conviso_mcp/server.js +438 -0
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
|
+
}
|