@convisoappsec/mcp 0.5.0 → 0.6.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/README.md +20 -28
- package/package.json +1 -1
- package/src/conviso_mcp/graphql_client.js +30 -67
- package/src/conviso_mcp/mutations.js +9 -0
- package/src/conviso_mcp/server.js +422 -1108
- package/src/conviso_mcp/feed_gateway.js +0 -198
|
@@ -7,17 +7,14 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { GraphQLClient } from './graphql_client.js';
|
|
11
|
+
import { listMutations, describeMutation } from './mutations.js';
|
|
11
12
|
import pkg from '../../package.json' with { type: 'json' };
|
|
12
13
|
|
|
13
|
-
const gateway = new FeedGateway();
|
|
14
|
-
|
|
15
14
|
console.error('[+] Starting Conviso MCP Server (MCP SDK)');
|
|
16
15
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
version: pkg.version || '0.4.0',
|
|
20
|
-
});
|
|
16
|
+
const BASE_URL = 'https://app.convisoappsec.com';
|
|
17
|
+
const gql = new GraphQLClient(`${BASE_URL}/graphql`, process.env.CONVISO_API_KEY || '');
|
|
21
18
|
|
|
22
19
|
function sanitizeError(err, message = 'Request failed') {
|
|
23
20
|
const error_id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
@@ -45,6 +42,9 @@ function sanitizeError(err, message = 'Request failed') {
|
|
|
45
42
|
if (Array.isArray(err?.graphqlErrors) && err.graphqlErrors.length) {
|
|
46
43
|
result.details = err.graphqlErrors;
|
|
47
44
|
}
|
|
45
|
+
if (err?.authHint) {
|
|
46
|
+
result.hint = err.authHint;
|
|
47
|
+
}
|
|
48
48
|
return result;
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -59,260 +59,86 @@ function ok(data) {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Shared enum strings so tool descriptions state each list exactly once.
|
|
63
|
+
const SEVERITIES = 'NOTIFICATION, LOW, MEDIUM, HIGH, CRITICAL';
|
|
64
|
+
const ISSUE_STATUSES = 'CREATED, DRAFT, IDENTIFIED, IN_PROGRESS, AWAITING_VALIDATION, FIX_ACCEPTED, RISK_ACCEPTED, FALSE_POSITIVE, SUPPRESSED';
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Build a fresh McpServer with all tools registered. stdio mode uses one instance for the
|
|
68
|
+
* whole session; HTTP mode builds one per request (the SDK's stateless pattern — reusing a
|
|
69
|
+
* single instance across concurrent transports leaks state between requests).
|
|
70
|
+
*/
|
|
71
|
+
function buildServer() {
|
|
72
|
+
const server = new McpServer({
|
|
73
|
+
name: pkg.name || 'conviso-mcp',
|
|
74
|
+
version: pkg.version || '0.6.0',
|
|
75
|
+
});
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
// Registration helper: one place for the try/catch, error shape, and annotations.
|
|
78
|
+
function tool(name, { title, desc, schema, write = false, destructive = false, local = false }, handler) {
|
|
79
|
+
server.registerTool(
|
|
80
|
+
name,
|
|
81
|
+
{
|
|
82
|
+
description: desc,
|
|
83
|
+
inputSchema: schema,
|
|
84
|
+
annotations: {
|
|
85
|
+
title,
|
|
86
|
+
readOnlyHint: !write,
|
|
87
|
+
destructiveHint: destructive,
|
|
88
|
+
idempotentHint: !write,
|
|
89
|
+
openWorldHint: !local,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
async (args) => {
|
|
93
|
+
try {
|
|
94
|
+
return ok(await handler(args));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return ok(sanitizeError(err, `${name} failed`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* READS — companies, issues, projects, assets, metrics
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
tool('get_companies', {
|
|
107
|
+
title: 'List Companies',
|
|
108
|
+
desc: 'List companies accessible with the API key. search = name contains; label_eq = exact name match.',
|
|
109
|
+
schema: z.object({
|
|
75
110
|
page: z.number().optional(),
|
|
76
111
|
limit: z.number().optional(),
|
|
77
112
|
search: z.string().optional(),
|
|
78
113
|
label_eq: z.string().optional(),
|
|
79
114
|
}),
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
server.registerTool(
|
|
98
|
-
'get_company_info',
|
|
99
|
-
{
|
|
100
|
-
description: 'Retrieve detailed information about a specific company, including plan, integrations, and branding metadata.',
|
|
101
|
-
inputSchema: z.object({ company_id: z.number() }),
|
|
102
|
-
annotations: {
|
|
103
|
-
title: 'Company Details',
|
|
104
|
-
readOnlyHint: true,
|
|
105
|
-
destructiveHint: false,
|
|
106
|
-
idempotentHint: true,
|
|
107
|
-
openWorldHint: true,
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
async ({ company_id }) => {
|
|
111
|
-
try {
|
|
112
|
-
return ok(await gateway.get_company_by_id(company_id));
|
|
113
|
-
} catch (err) {
|
|
114
|
-
return fail(err, 'Failed to get company info');
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
server.registerTool(
|
|
120
|
-
'get_issue',
|
|
121
|
-
{
|
|
122
|
-
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.',
|
|
123
|
-
inputSchema: z.object({
|
|
115
|
+
}, ({ page = 1, limit = 10, search = '', label_eq = null }) =>
|
|
116
|
+
gql.get_companies(page, limit, search, label_eq));
|
|
117
|
+
|
|
118
|
+
tool('get_company_info', {
|
|
119
|
+
title: 'Company Details',
|
|
120
|
+
desc: 'Get a company by ID: plan, integrations, branding metadata.',
|
|
121
|
+
schema: z.object({ company_id: z.number() }),
|
|
122
|
+
}, ({ company_id }) => gql.get_company_by_id(company_id));
|
|
123
|
+
|
|
124
|
+
tool('get_issue', {
|
|
125
|
+
title: 'Issue Details',
|
|
126
|
+
desc: 'Get full technical detail for one issue/vulnerability. Set return_vulnerable_data=true to include raw requests/responses and vulnerable code snippets (may contain sensitive data).',
|
|
127
|
+
schema: z.object({
|
|
124
128
|
id: z.number(),
|
|
125
129
|
return_vulnerable_data: z.boolean().optional(),
|
|
126
130
|
}),
|
|
127
|
-
|
|
128
|
-
title: 'Issue Details',
|
|
129
|
-
readOnlyHint: true,
|
|
130
|
-
destructiveHint: false,
|
|
131
|
-
idempotentHint: true,
|
|
132
|
-
openWorldHint: true,
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
async ({ id, return_vulnerable_data }) => {
|
|
136
|
-
try {
|
|
137
|
-
return ok(await gateway.get_issue_by_id(id, return_vulnerable_data));
|
|
138
|
-
} catch (err) {
|
|
139
|
-
return fail(err, 'Failed to get issue details');
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
server.registerTool(
|
|
145
|
-
'get_issues',
|
|
146
|
-
{
|
|
147
|
-
description: `Get issues (vulnerabilities) for a company, with rich filtering and sorting.
|
|
131
|
+
}, ({ id, return_vulnerable_data }) => gql.get_issue_by_id(id, return_vulnerable_data));
|
|
148
132
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
FIX_ACCEPTED, RISK_ACCEPTED, FALSE_POSITIVE, SUPPRESSED.
|
|
154
|
-
- sla_states: any of ON_TRACK, APPROACHING, BREACHED, RESOLVED, NOT_TRACKED, NOT_PARAMETERIZED.
|
|
155
|
-
- created_after / created_before: ISO8601 dates (YYYY-MM-DD). For relative ranges
|
|
156
|
-
("last 30 days") call get_today_date first and compute the bounds.
|
|
157
|
-
- assignee_emails: list of assignee emails.
|
|
158
|
-
- project_id: restrict to one project. asset filtering: use get_issues_by_asset_id.
|
|
159
|
-
- sort_by: one of RISK_SCORE, SEVERITY, ID, CREATED_AT, UPDATED_AT, SLA_DUE_AT. order: ASC or DESC.
|
|
160
|
-
- extra_filters: dict mapping directly to IssuesFiltersInput for advanced keys, e.g.
|
|
161
|
-
{"cves": [...], "categories": [...], "reachableBy": ["STATIC_ANALYSIS"],
|
|
162
|
-
"businessImpact": ["HIGH"], "exploitability": "INTERNET_FACING",
|
|
163
|
-
"compromisedEnvironment": true, "aiFpAnalyzed": true, "assetTags": [...]}.
|
|
164
|
-
extra_filters values are sent as-is — omit a key rather than passing an empty list.
|
|
165
|
-
|
|
166
|
-
Returns issue collection (id, title, severity, status, dates, sla, assignedUsers,
|
|
167
|
-
asset, project) plus metadata (totalCount, totalPages, currentPage) for pagination.`,
|
|
168
|
-
inputSchema: z.object({
|
|
169
|
-
company_id: z.number(),
|
|
170
|
-
page: z.number().optional(),
|
|
171
|
-
limit: z.number().optional(),
|
|
172
|
-
project_id: z.number().optional(),
|
|
173
|
-
search: z.string().optional(),
|
|
174
|
-
severities: z.array(z.string()).optional(),
|
|
175
|
-
statuses: z.array(z.string()).optional(),
|
|
176
|
-
sla_states: z.array(z.string()).optional(),
|
|
177
|
-
created_after: z.string().optional(),
|
|
178
|
-
created_before: z.string().optional(),
|
|
179
|
-
assignee_emails: z.array(z.string()).optional(),
|
|
180
|
-
sort_by: z.string().optional(),
|
|
181
|
-
order: z.string().optional(),
|
|
182
|
-
extra_filters: z.record(z.string(), z.any()).optional(),
|
|
183
|
-
}),
|
|
184
|
-
annotations: {
|
|
185
|
-
title: 'List Issues',
|
|
186
|
-
readOnlyHint: true,
|
|
187
|
-
destructiveHint: false,
|
|
188
|
-
idempotentHint: true,
|
|
189
|
-
openWorldHint: true,
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
async ({
|
|
193
|
-
company_id, page = 1, limit = 10, project_id, search = '',
|
|
194
|
-
severities, statuses, sla_states, created_after, created_before,
|
|
195
|
-
assignee_emails, sort_by, order = 'DESC', extra_filters,
|
|
196
|
-
}) => {
|
|
197
|
-
try {
|
|
198
|
-
return ok(await gateway.getIssues(company_id, {
|
|
199
|
-
page,
|
|
200
|
-
limit,
|
|
201
|
-
projectId: project_id,
|
|
202
|
-
search,
|
|
203
|
-
severities,
|
|
204
|
-
statuses,
|
|
205
|
-
slaStates: sla_states,
|
|
206
|
-
createdAfter: created_after,
|
|
207
|
-
createdBefore: created_before,
|
|
208
|
-
assigneeEmails: assignee_emails,
|
|
209
|
-
sortBy: sort_by,
|
|
210
|
-
order,
|
|
211
|
-
extraFilters: extra_filters,
|
|
212
|
-
}));
|
|
213
|
-
} catch (err) {
|
|
214
|
-
return fail(err, 'Failed to list issues');
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
server.registerTool(
|
|
220
|
-
'get_issues_by_asset_id',
|
|
221
|
-
{
|
|
222
|
-
description: `List vulnerabilities for a company filtered by a single asset ID, with rich filtering and sorting.
|
|
223
|
-
|
|
224
|
-
Filters (all optional):
|
|
225
|
-
- search: substring match on issue title.
|
|
226
|
-
- severities: any of NOTIFICATION, LOW, MEDIUM, HIGH, CRITICAL.
|
|
227
|
-
- statuses: any of CREATED, DRAFT, IDENTIFIED, IN_PROGRESS, AWAITING_VALIDATION,
|
|
228
|
-
FIX_ACCEPTED, RISK_ACCEPTED, FALSE_POSITIVE, SUPPRESSED.
|
|
229
|
-
- sla_states: any of ON_TRACK, APPROACHING, BREACHED, RESOLVED, NOT_TRACKED, NOT_PARAMETERIZED.
|
|
230
|
-
- created_after / created_before: ISO8601 dates (YYYY-MM-DD). For relative ranges
|
|
231
|
-
("last 30 days") call get_today_date first and compute the bounds.
|
|
232
|
-
- assignee_emails: list of assignee emails.
|
|
233
|
-
- sort_by: one of RISK_SCORE, SEVERITY, ID, CREATED_AT, UPDATED_AT, SLA_DUE_AT. order: ASC or DESC.
|
|
234
|
-
- extra_filters: dict mapping directly to IssuesFiltersInput for advanced keys, e.g.
|
|
235
|
-
{"cves": [...], "categories": [...], "reachableBy": ["STATIC_ANALYSIS"],
|
|
236
|
-
"businessImpact": ["HIGH"], "exploitability": "INTERNET_FACING",
|
|
237
|
-
"compromisedEnvironment": true, "aiFpAnalyzed": true, "assetTags": [...]}.
|
|
238
|
-
|
|
239
|
-
Returns issue collection (id, title, severity, status, dates, sla, assignedUsers,
|
|
240
|
-
asset, project) plus metadata (totalCount, totalPages, currentPage) for pagination.`,
|
|
241
|
-
inputSchema: z.object({
|
|
133
|
+
tool('get_issues', {
|
|
134
|
+
title: 'List Issues',
|
|
135
|
+
desc: `List a company's vulnerabilities with filtering and sorting. Optional: search (title substring), project_id, asset_id, severities (${SEVERITIES}), statuses (${ISSUE_STATUSES}), sla_states (ON_TRACK, APPROACHING, BREACHED, RESOLVED, NOT_TRACKED, NOT_PARAMETERIZED), created_after/created_before (YYYY-MM-DD — call get_today_date for relative ranges), assignee_emails, sort_by (RISK_SCORE, SEVERITY, ID, CREATED_AT, UPDATED_AT, SLA_DUE_AT) with order ASC|DESC, and extra_filters (raw IssuesFiltersInput keys, e.g. cves, categories, businessImpact — sent as-is). Returns collection + metadata (totalCount/totalPages) for pagination.`,
|
|
136
|
+
schema: z.object({
|
|
242
137
|
company_id: z.number(),
|
|
243
|
-
asset_id: z.number(),
|
|
244
|
-
page: z.number().optional(),
|
|
245
|
-
limit: z.number().optional(),
|
|
246
|
-
search: z.string().optional(),
|
|
247
|
-
severities: z.array(z.string()).optional(),
|
|
248
|
-
statuses: z.array(z.string()).optional(),
|
|
249
|
-
sla_states: z.array(z.string()).optional(),
|
|
250
|
-
created_after: z.string().optional(),
|
|
251
|
-
created_before: z.string().optional(),
|
|
252
|
-
assignee_emails: z.array(z.string()).optional(),
|
|
253
|
-
sort_by: z.string().optional(),
|
|
254
|
-
order: z.string().optional(),
|
|
255
|
-
extra_filters: z.record(z.string(), z.any()).optional(),
|
|
256
|
-
}),
|
|
257
|
-
annotations: {
|
|
258
|
-
title: 'List Issues by Asset ID',
|
|
259
|
-
readOnlyHint: true,
|
|
260
|
-
destructiveHint: false,
|
|
261
|
-
idempotentHint: true,
|
|
262
|
-
openWorldHint: true,
|
|
263
|
-
},
|
|
264
|
-
},
|
|
265
|
-
async ({
|
|
266
|
-
company_id, asset_id, page = 1, limit = 10, search = '',
|
|
267
|
-
severities, statuses, sla_states, created_after, created_before,
|
|
268
|
-
assignee_emails, sort_by, order = 'DESC', extra_filters,
|
|
269
|
-
}) => {
|
|
270
|
-
try {
|
|
271
|
-
const asset_ids = Array.isArray(asset_id) ? asset_id : [asset_id];
|
|
272
|
-
return ok(await gateway.get_issues_by_asset_ids(company_id, page, limit, asset_ids, search, {
|
|
273
|
-
severities,
|
|
274
|
-
statuses,
|
|
275
|
-
slaStates: sla_states,
|
|
276
|
-
createdAfter: created_after,
|
|
277
|
-
createdBefore: created_before,
|
|
278
|
-
assigneeEmails: assignee_emails,
|
|
279
|
-
sortBy: sort_by,
|
|
280
|
-
order,
|
|
281
|
-
extraFilters: extra_filters,
|
|
282
|
-
}));
|
|
283
|
-
} catch (err) {
|
|
284
|
-
return fail(err, 'Failed to list issues by asset id');
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
server.registerTool(
|
|
290
|
-
'get_issues_by_project_id',
|
|
291
|
-
{
|
|
292
|
-
description: `List vulnerabilities for a company filtered by a project ID, with rich filtering and sorting.
|
|
293
|
-
|
|
294
|
-
Filters (all optional):
|
|
295
|
-
- search: substring match on issue title.
|
|
296
|
-
- severities: any of NOTIFICATION, LOW, MEDIUM, HIGH, CRITICAL.
|
|
297
|
-
- statuses: any of CREATED, DRAFT, IDENTIFIED, IN_PROGRESS, AWAITING_VALIDATION,
|
|
298
|
-
FIX_ACCEPTED, RISK_ACCEPTED, FALSE_POSITIVE, SUPPRESSED.
|
|
299
|
-
- sla_states: any of ON_TRACK, APPROACHING, BREACHED, RESOLVED, NOT_TRACKED, NOT_PARAMETERIZED.
|
|
300
|
-
- created_after / created_before: ISO8601 dates (YYYY-MM-DD). For relative ranges
|
|
301
|
-
("last 30 days") call get_today_date first and compute the bounds.
|
|
302
|
-
- assignee_emails: list of assignee emails.
|
|
303
|
-
- sort_by: one of RISK_SCORE, SEVERITY, ID, CREATED_AT, UPDATED_AT, SLA_DUE_AT. order: ASC or DESC.
|
|
304
|
-
- extra_filters: dict mapping directly to IssuesFiltersInput for advanced keys, e.g.
|
|
305
|
-
{"cves": [...], "categories": [...], "reachableBy": ["STATIC_ANALYSIS"],
|
|
306
|
-
"businessImpact": ["HIGH"], "exploitability": "INTERNET_FACING",
|
|
307
|
-
"compromisedEnvironment": true, "aiFpAnalyzed": true, "assetTags": [...]}.
|
|
308
|
-
|
|
309
|
-
Returns issue collection (id, title, severity, status, dates, sla, assignedUsers,
|
|
310
|
-
asset, project) plus metadata (totalCount, totalPages, currentPage) for pagination.`,
|
|
311
|
-
inputSchema: z.object({
|
|
312
|
-
company_id: z.number(),
|
|
313
|
-
project_id: z.number(),
|
|
314
138
|
page: z.number().optional(),
|
|
315
139
|
limit: z.number().optional(),
|
|
140
|
+
project_id: z.number().optional(),
|
|
141
|
+
asset_id: z.number().optional(),
|
|
316
142
|
search: z.string().optional(),
|
|
317
143
|
severities: z.array(z.string()).optional(),
|
|
318
144
|
statuses: z.array(z.string()).optional(),
|
|
@@ -324,51 +150,31 @@ asset, project) plus metadata (totalCount, totalPages, currentPage) for paginati
|
|
|
324
150
|
order: z.string().optional(),
|
|
325
151
|
extra_filters: z.record(z.string(), z.any()).optional(),
|
|
326
152
|
}),
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
readOnlyHint: true,
|
|
330
|
-
destructiveHint: false,
|
|
331
|
-
idempotentHint: true,
|
|
332
|
-
openWorldHint: true,
|
|
333
|
-
},
|
|
334
|
-
},
|
|
335
|
-
async ({
|
|
336
|
-
company_id, project_id, page = 1, limit = 10, search = '',
|
|
153
|
+
}, ({
|
|
154
|
+
company_id, page = 1, limit = 10, project_id, asset_id, search = '',
|
|
337
155
|
severities, statuses, sla_states, created_after, created_before,
|
|
338
156
|
assignee_emails, sort_by, order = 'DESC', extra_filters,
|
|
339
|
-
}) => {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
server.registerTool(
|
|
363
|
-
'get_top_vulnerabilities',
|
|
364
|
-
{
|
|
365
|
-
description: 'Return a summary of vulnerability counts grouped by severity for a given company (risk overview). '
|
|
366
|
-
+ 'Optional filters (severities, statuses, asset_ids, asset_tags, created_after/created_before) narrow the '
|
|
367
|
-
+ 'overview; when none are set the response is identical to calling with no arguments at all. '
|
|
368
|
-
+ 'severities: NOTIFICATION, LOW, MEDIUM, HIGH, CRITICAL. statuses: CREATED, DRAFT, IDENTIFIED, IN_PROGRESS, '
|
|
369
|
-
+ 'AWAITING_VALIDATION, FIX_ACCEPTED, RISK_ACCEPTED, FALSE_POSITIVE, SUPPRESSED. created_after/created_before '
|
|
370
|
-
+ 'are ISO8601 dates (YYYY-MM-DD).',
|
|
371
|
-
inputSchema: z.object({
|
|
157
|
+
}) => gql.getIssues(company_id, {
|
|
158
|
+
page,
|
|
159
|
+
limit,
|
|
160
|
+
projectId: project_id,
|
|
161
|
+
assetIds: asset_id ? [asset_id] : undefined,
|
|
162
|
+
search,
|
|
163
|
+
severities,
|
|
164
|
+
statuses,
|
|
165
|
+
slaStates: sla_states,
|
|
166
|
+
createdAfter: created_after,
|
|
167
|
+
createdBefore: created_before,
|
|
168
|
+
assigneeEmails: assignee_emails,
|
|
169
|
+
sortBy: sort_by,
|
|
170
|
+
order,
|
|
171
|
+
extraFilters: extra_filters,
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
tool('get_top_vulnerabilities', {
|
|
175
|
+
title: 'Top Vulnerabilities',
|
|
176
|
+
desc: `Vulnerability counts grouped by title/severity for a company (risk overview). Optional filters: severities (${SEVERITIES}), statuses (${ISSUE_STATUSES}), asset_ids, asset_tags, created_after/created_before (YYYY-MM-DD).`,
|
|
177
|
+
schema: z.object({
|
|
372
178
|
company_id: z.number(),
|
|
373
179
|
severities: z.array(z.string()).optional(),
|
|
374
180
|
statuses: z.array(z.string()).optional(),
|
|
@@ -377,44 +183,20 @@ server.registerTool(
|
|
|
377
183
|
created_after: z.string().optional(),
|
|
378
184
|
created_before: z.string().optional(),
|
|
379
185
|
}),
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
assetTags: asset_tags,
|
|
395
|
-
createdAfter: created_after,
|
|
396
|
-
createdBefore: created_before,
|
|
397
|
-
}));
|
|
398
|
-
} catch (err) {
|
|
399
|
-
return fail(err, 'Failed to get top vulnerabilities');
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
server.registerTool(
|
|
405
|
-
'get_projects',
|
|
406
|
-
{
|
|
407
|
-
description: `Return a paginated list of security projects for a company, with filtering and sorting. Defaults to 25 results per page to conserve tokens.
|
|
408
|
-
|
|
409
|
-
Filters (all optional):
|
|
410
|
-
- search: substring match on project label.
|
|
411
|
-
- statuses: platform status labels (free text, e.g. "Fixing"), not an enum.
|
|
412
|
-
- project_types: platform project type labels (free text, e.g. "Pentest"), not an enum.
|
|
413
|
-
- created_after / created_before: ISO8601 dates (YYYY-MM-DD) bounding createdAt.
|
|
414
|
-
- tags: list of project tags.
|
|
415
|
-
- analyst_emails: list of allocated analyst emails.
|
|
416
|
-
- sort_by: field to sort by (default "createdAt"). descending: sort direction (default true).`,
|
|
417
|
-
inputSchema: z.object({
|
|
186
|
+
}, ({ company_id, severities, statuses, asset_ids, asset_tags, created_after, created_before }) =>
|
|
187
|
+
gql.get_top_vulnerabilities(company_id, {
|
|
188
|
+
severities,
|
|
189
|
+
statuses,
|
|
190
|
+
assetIds: asset_ids,
|
|
191
|
+
assetTags: asset_tags,
|
|
192
|
+
createdAfter: created_after,
|
|
193
|
+
createdBefore: created_before,
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
tool('get_projects', {
|
|
197
|
+
title: 'List Projects',
|
|
198
|
+
desc: 'List a company\'s security projects (paginated, default 25/page). Optional: search (label substring), statuses and project_types (platform labels, free text e.g. "Fixing", "Pentest"), created_after/created_before (YYYY-MM-DD), tags, analyst_emails, sort_by (default createdAt) + descending.',
|
|
199
|
+
schema: z.object({
|
|
418
200
|
company_id: z.number(),
|
|
419
201
|
page: z.number().optional(),
|
|
420
202
|
limit: z.number().optional(),
|
|
@@ -428,101 +210,37 @@ Filters (all optional):
|
|
|
428
210
|
sort_by: z.string().optional(),
|
|
429
211
|
descending: z.boolean().optional(),
|
|
430
212
|
}),
|
|
431
|
-
|
|
432
|
-
title: 'List Projects',
|
|
433
|
-
readOnlyHint: true,
|
|
434
|
-
destructiveHint: false,
|
|
435
|
-
idempotentHint: true,
|
|
436
|
-
openWorldHint: true,
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
async ({
|
|
213
|
+
}, ({
|
|
440
214
|
company_id, page = 1, limit = 25, search = '', statuses, project_types,
|
|
441
215
|
created_after, created_before, tags, analyst_emails, sort_by = 'createdAt',
|
|
442
216
|
descending = true,
|
|
443
|
-
}) => {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
idempotentHint: true,
|
|
471
|
-
openWorldHint: true,
|
|
472
|
-
},
|
|
473
|
-
},
|
|
474
|
-
async ({ project_id }) => {
|
|
475
|
-
try {
|
|
476
|
-
return ok(await gateway.get_project_by_id(project_id));
|
|
477
|
-
} catch (err) {
|
|
478
|
-
return fail(err, 'Failed to get project');
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
server.registerTool(
|
|
484
|
-
'get_asset',
|
|
485
|
-
{
|
|
486
|
-
description: 'Fetch information about a specific asset by its ID.',
|
|
487
|
-
inputSchema: z.object({ asset_id: z.number() }),
|
|
488
|
-
annotations: {
|
|
489
|
-
title: 'Asset Details',
|
|
490
|
-
readOnlyHint: true,
|
|
491
|
-
destructiveHint: false,
|
|
492
|
-
idempotentHint: true,
|
|
493
|
-
openWorldHint: true,
|
|
494
|
-
},
|
|
495
|
-
},
|
|
496
|
-
async ({ asset_id }) => {
|
|
497
|
-
try {
|
|
498
|
-
return ok(await gateway.get_asset_by_id(asset_id));
|
|
499
|
-
} catch (err) {
|
|
500
|
-
return fail(err, 'Failed to get asset');
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
);
|
|
504
|
-
|
|
505
|
-
server.registerTool(
|
|
506
|
-
'get_assets',
|
|
507
|
-
{
|
|
508
|
-
description: `Return a paginated list of assets for a company, with rich filtering and sorting. Defaults to 25 results per page to reduce token usage.
|
|
509
|
-
|
|
510
|
-
Filters (all optional):
|
|
511
|
-
- name / search: substring match on asset name.
|
|
512
|
-
- tags: list of asset tags.
|
|
513
|
-
- technology: list of technologies.
|
|
514
|
-
- business_impact: any of LOW, MEDIUM, HIGH, NOT_DEFINED.
|
|
515
|
-
- exploitability: any of INTERNET_FACING, INTERNAL, NOT_DEFINED.
|
|
516
|
-
- asset_type: asset type filter.
|
|
517
|
-
- environment_compromised: boolean filter for compromised environment.
|
|
518
|
-
- covered_by_scan: boolean filter for scan coverage.
|
|
519
|
-
- sort_by: one of updated_at, name, business_impact, risk_score. order: ASC or DESC.
|
|
520
|
-
- extra_filters: object mapping directly to AssetsSearch for advanced keys.
|
|
521
|
-
extra_filters values are sent as-is — omit a key rather than passing an empty list.
|
|
522
|
-
|
|
523
|
-
Returns asset collection (id, name, assetType, environment, audience, dates, riskScore)
|
|
524
|
-
plus metadata (totalCount, totalPages, currentPage) for pagination.`,
|
|
525
|
-
inputSchema: z.object({
|
|
217
|
+
}) => gql.get_projects(company_id, page, limit, search, {
|
|
218
|
+
statuses,
|
|
219
|
+
projectTypes: project_types,
|
|
220
|
+
createdAfter: created_after,
|
|
221
|
+
createdBefore: created_before,
|
|
222
|
+
tags,
|
|
223
|
+
analystEmails: analyst_emails,
|
|
224
|
+
sortBy: sort_by,
|
|
225
|
+
descending,
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
tool('get_project', {
|
|
229
|
+
title: 'Project Details',
|
|
230
|
+
desc: 'Get a project by ID.',
|
|
231
|
+
schema: z.object({ project_id: z.number() }),
|
|
232
|
+
}, ({ project_id }) => gql.get_project_by_id(project_id));
|
|
233
|
+
|
|
234
|
+
tool('get_asset', {
|
|
235
|
+
title: 'Asset Details',
|
|
236
|
+
desc: 'Get an asset by ID.',
|
|
237
|
+
schema: z.object({ asset_id: z.number() }),
|
|
238
|
+
}, ({ asset_id }) => gql.get_asset_by_id(asset_id));
|
|
239
|
+
|
|
240
|
+
tool('get_assets', {
|
|
241
|
+
title: 'List Assets',
|
|
242
|
+
desc: 'List a company\'s assets (paginated, default 25/page). Optional: name/search (substring), tags, technology, business_impact (LOW, MEDIUM, HIGH, NOT_DEFINED), exploitability (INTERNET_FACING, INTERNAL, NOT_DEFINED), asset_type, environment_compromised, covered_by_scan, sort_by (updated_at, name, business_impact, risk_score) + order, extra_filters (raw AssetsSearch keys). Returns collection + metadata.',
|
|
243
|
+
schema: z.object({
|
|
526
244
|
company_id: z.number(),
|
|
527
245
|
page: z.number().optional(),
|
|
528
246
|
limit: z.number().optional(),
|
|
@@ -539,95 +257,45 @@ plus metadata (totalCount, totalPages, currentPage) for pagination.`,
|
|
|
539
257
|
order: z.string().optional(),
|
|
540
258
|
extra_filters: z.record(z.string(), z.any()).optional(),
|
|
541
259
|
}),
|
|
542
|
-
|
|
543
|
-
title: 'List Assets',
|
|
544
|
-
readOnlyHint: true,
|
|
545
|
-
destructiveHint: false,
|
|
546
|
-
idempotentHint: true,
|
|
547
|
-
openWorldHint: true,
|
|
548
|
-
},
|
|
549
|
-
},
|
|
550
|
-
async ({
|
|
260
|
+
}, ({
|
|
551
261
|
company_id, page = 1, limit = 25, name, search, tags, technology,
|
|
552
262
|
business_impact, exploitability, asset_type, environment_compromised,
|
|
553
263
|
covered_by_scan, sort_by, order, extra_filters,
|
|
554
|
-
}) => {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
'
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
})
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
openWorldHint: true,
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
async ({ company_id, project_id }) => {
|
|
593
|
-
try {
|
|
594
|
-
return ok(await gateway.create_project_url(company_id, project_id));
|
|
595
|
-
} catch (err) {
|
|
596
|
-
return fail(err, 'Failed to create project URL');
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
);
|
|
600
|
-
|
|
601
|
-
server.registerTool(
|
|
602
|
-
'create_issue_url',
|
|
603
|
-
{
|
|
604
|
-
description: 'Return a direct URL to open a specific issue in the Conviso Platform for triage or review.',
|
|
605
|
-
inputSchema: z.object({
|
|
606
|
-
company_id: z.number(),
|
|
607
|
-
issue_id: z.number(),
|
|
608
|
-
}),
|
|
609
|
-
annotations: {
|
|
610
|
-
title: 'Issue URL Generator',
|
|
611
|
-
readOnlyHint: true,
|
|
612
|
-
destructiveHint: false,
|
|
613
|
-
idempotentHint: true,
|
|
614
|
-
openWorldHint: true,
|
|
615
|
-
},
|
|
616
|
-
},
|
|
617
|
-
async ({ company_id, issue_id }) => {
|
|
618
|
-
try {
|
|
619
|
-
return ok(await gateway.create_issue_url(company_id, issue_id));
|
|
620
|
-
} catch (err) {
|
|
621
|
-
return fail(err, 'Failed to create issue URL');
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
);
|
|
625
|
-
|
|
626
|
-
server.registerTool(
|
|
627
|
-
'get_mttr_over_time',
|
|
628
|
-
{
|
|
629
|
-
description: 'Get Mean Time To Resolution (MTTR) aggregated over a date range. Supports filtering by severities, statuses, and assets.',
|
|
630
|
-
inputSchema: z.object({
|
|
264
|
+
}) => gql.get_assets_by_company(company_id, page, limit, {
|
|
265
|
+
name,
|
|
266
|
+
search,
|
|
267
|
+
tags,
|
|
268
|
+
technology,
|
|
269
|
+
businessImpact: business_impact,
|
|
270
|
+
exploitability,
|
|
271
|
+
assetType: asset_type,
|
|
272
|
+
environmentCompromised: environment_compromised,
|
|
273
|
+
coveredByScan: covered_by_scan,
|
|
274
|
+
sortBy: sort_by,
|
|
275
|
+
order,
|
|
276
|
+
extraFilters: extra_filters,
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
tool('create_project_url', {
|
|
280
|
+
title: 'Project URL',
|
|
281
|
+
desc: 'Build the direct Conviso Platform URL for a project.',
|
|
282
|
+
schema: z.object({ company_id: z.number(), project_id: z.number() }),
|
|
283
|
+
local: true,
|
|
284
|
+
}, ({ company_id, project_id }) =>
|
|
285
|
+
`${BASE_URL}/spa/company/${company_id}/projects/${project_id}`);
|
|
286
|
+
|
|
287
|
+
tool('create_issue_url', {
|
|
288
|
+
title: 'Issue URL',
|
|
289
|
+
desc: 'Build the direct Conviso Platform URL for an issue.',
|
|
290
|
+
schema: z.object({ company_id: z.number(), issue_id: z.number() }),
|
|
291
|
+
local: true,
|
|
292
|
+
}, ({ company_id, issue_id }) =>
|
|
293
|
+
`${BASE_URL}/spa/company/${company_id}/vulnerabilities?title=&search=${issue_id}`);
|
|
294
|
+
|
|
295
|
+
tool('get_mttr_over_time', {
|
|
296
|
+
title: 'MTTR Over Time',
|
|
297
|
+
desc: 'Mean Time To Resolution over a date range, broken down by severity. Optional filters: severities, statuses, asset_ids, asset_tags.',
|
|
298
|
+
schema: z.object({
|
|
631
299
|
company_id: z.number(),
|
|
632
300
|
start_date: z.string(),
|
|
633
301
|
end_date: z.string(),
|
|
@@ -636,208 +304,220 @@ server.registerTool(
|
|
|
636
304
|
asset_ids: z.array(z.number()).optional(),
|
|
637
305
|
asset_tags: z.array(z.string()).optional(),
|
|
638
306
|
}),
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
);
|
|
307
|
+
}, (a) => gql.get_mttr_over_time(a.company_id, a.start_date, a.end_date, a.severities, a.statuses, a.asset_ids, a.asset_tags));
|
|
308
|
+
|
|
309
|
+
tool('get_overall_risk_score_history', {
|
|
310
|
+
title: 'Risk Score History',
|
|
311
|
+
desc: 'Historical overall risk score for a company (current value + difference from last period).',
|
|
312
|
+
schema: z.object({ company_id: z.number() }),
|
|
313
|
+
}, ({ company_id }) => gql.get_overall_risk_score_history(company_id));
|
|
314
|
+
|
|
315
|
+
tool('get_today_date', {
|
|
316
|
+
title: 'Get Today Date',
|
|
317
|
+
desc: 'Current day/month/year — use to compute relative date ranges before filtering by dates.',
|
|
318
|
+
schema: z.object({}),
|
|
319
|
+
local: true,
|
|
320
|
+
}, () => {
|
|
321
|
+
const d = new Date();
|
|
322
|
+
return { day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear() };
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* READS — tickets, requirements, applications, scans, supply chain, AI-pentest, threat modeling
|
|
327
|
+
*/
|
|
663
328
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
inputSchema: z.object({
|
|
329
|
+
tool('get_tickets', {
|
|
330
|
+
title: 'List Tickets',
|
|
331
|
+
desc: 'List a company\'s tickets (paginated). Optional: search, sort_by + descending, params (raw TicketSearch keys: types, statuses, priorities, impacts, tags, mineOnly...).',
|
|
332
|
+
schema: z.object({
|
|
669
333
|
company_id: z.number(),
|
|
334
|
+
page: z.number().optional(),
|
|
335
|
+
limit: z.number().optional(),
|
|
336
|
+
search: z.string().optional(),
|
|
337
|
+
sort_by: z.string().optional(),
|
|
338
|
+
descending: z.boolean().optional(),
|
|
339
|
+
params: z.record(z.string(), z.any()).optional(),
|
|
670
340
|
}),
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
},
|
|
678
|
-
},
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
)
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
341
|
+
}, ({ company_id, page, limit, search, sort_by, descending, params }) =>
|
|
342
|
+
gql.get_tickets(company_id, { page, limit, search, sort_by, descending, params }));
|
|
343
|
+
|
|
344
|
+
tool('get_ticket', {
|
|
345
|
+
title: 'Ticket Details',
|
|
346
|
+
desc: 'Get a ticket by ID (status, priority, impact, assignee).',
|
|
347
|
+
schema: z.object({ company_id: z.number(), ticket_id: z.number() }),
|
|
348
|
+
}, ({ company_id, ticket_id }) => gql.get_ticket(company_id, ticket_id));
|
|
349
|
+
|
|
350
|
+
tool('get_requirements', {
|
|
351
|
+
title: 'List Requirements',
|
|
352
|
+
desc: 'List security requirements/checklists for a scope (company) id, paginated. Optional: filters (raw RequirementsFilterInput).',
|
|
353
|
+
schema: z.object({
|
|
354
|
+
scope_id: z.number(),
|
|
355
|
+
page: z.number().optional(),
|
|
356
|
+
limit: z.number().optional(),
|
|
357
|
+
filters: z.record(z.string(), z.any()).optional(),
|
|
358
|
+
}),
|
|
359
|
+
}, ({ scope_id, page, limit, filters }) => gql.get_requirements(scope_id, { page, limit, filters }));
|
|
360
|
+
|
|
361
|
+
tool('get_requirement', {
|
|
362
|
+
title: 'Requirement Details',
|
|
363
|
+
desc: 'Get a requirement/checklist by ID.',
|
|
364
|
+
schema: z.object({ company_id: z.number(), requirement_id: z.number() }),
|
|
365
|
+
}, ({ company_id, requirement_id }) => gql.get_requirement(company_id, requirement_id));
|
|
366
|
+
|
|
367
|
+
tool('get_project_requirements', {
|
|
368
|
+
title: 'Project Requirements',
|
|
369
|
+
desc: 'List the requirements/checklists attached to a project.',
|
|
370
|
+
schema: z.object({ project_id: z.number() }),
|
|
371
|
+
}, ({ project_id }) => gql.get_project_requirements(project_id));
|
|
372
|
+
|
|
373
|
+
tool('get_applications', {
|
|
374
|
+
title: 'List Applications',
|
|
375
|
+
desc: 'List a company\'s applications (name, url, riskScore, assetsCount). Optional: search by name.',
|
|
376
|
+
schema: z.object({ company_id: z.number(), search: z.string().optional() }),
|
|
377
|
+
}, ({ company_id, search }) => gql.get_applications(company_id, search));
|
|
378
|
+
|
|
379
|
+
tool('get_application', {
|
|
380
|
+
title: 'Application Details',
|
|
381
|
+
desc: 'Get an application by ID, including its linked assets.',
|
|
382
|
+
schema: z.object({ company_id: z.number(), application_id: z.number() }),
|
|
383
|
+
}, ({ company_id, application_id }) => gql.get_application(company_id, application_id));
|
|
384
|
+
|
|
385
|
+
tool('get_scan_histories', {
|
|
386
|
+
title: 'List Scan Histories',
|
|
387
|
+
desc: 'List scan executions for a company (status, integration, duration, vulnerability counts). Optional: asset_ids, filters (raw ScansHistoriesFiltersInput).',
|
|
388
|
+
schema: z.object({
|
|
389
|
+
company_id: z.number(),
|
|
390
|
+
asset_ids: z.array(z.number()).optional(),
|
|
391
|
+
page: z.number().optional(),
|
|
392
|
+
limit: z.number().optional(),
|
|
393
|
+
filters: z.record(z.string(), z.any()).optional(),
|
|
394
|
+
}),
|
|
395
|
+
}, ({ company_id, asset_ids, page, limit, filters }) =>
|
|
396
|
+
gql.get_scan_histories(company_id, { assetIds: asset_ids, page, limit, filters }));
|
|
397
|
+
|
|
398
|
+
tool('get_asset_scans_count', {
|
|
399
|
+
title: 'Asset Scans Count',
|
|
400
|
+
desc: 'Scan coverage for a company: assets with/without scans and which scan types count.',
|
|
401
|
+
schema: z.object({ company_id: z.number() }),
|
|
402
|
+
}, ({ company_id }) => gql.get_asset_scans_count(company_id));
|
|
403
|
+
|
|
404
|
+
tool('get_sbom_components', {
|
|
405
|
+
title: 'List SBOM Components',
|
|
406
|
+
desc: 'List SBOM / supply-chain components (name, version, technology, package manager, license, issues by severity). Optional: search (raw SbomComponentSearchInput).',
|
|
407
|
+
schema: z.object({
|
|
408
|
+
company_id: z.number(),
|
|
409
|
+
page: z.number().optional(),
|
|
410
|
+
limit: z.number().optional(),
|
|
411
|
+
search: z.record(z.string(), z.any()).optional(),
|
|
412
|
+
}),
|
|
413
|
+
}, ({ company_id, page, limit, search }) => gql.get_sbom_components(company_id, { page, limit, search }));
|
|
720
414
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
415
|
+
tool('get_pentest_artifacts', {
|
|
416
|
+
title: 'List Pentest Artifacts',
|
|
417
|
+
desc: 'List AI-Pentest artifacts (label, type, scheduling, latest execution). Optional: search, assignee_email, pentest_type, application_id.',
|
|
418
|
+
schema: z.object({
|
|
419
|
+
company_id: z.number(),
|
|
420
|
+
page: z.number().optional(),
|
|
421
|
+
limit: z.number().optional(),
|
|
726
422
|
search: z.string().optional(),
|
|
727
|
-
|
|
423
|
+
assignee_email: z.string().optional(),
|
|
424
|
+
pentest_type: z.string().optional(),
|
|
425
|
+
application_id: z.number().optional(),
|
|
426
|
+
}),
|
|
427
|
+
}, ({ company_id, page, limit, search, assignee_email, pentest_type, application_id }) =>
|
|
428
|
+
gql.get_pentest_artifacts(company_id, {
|
|
429
|
+
page, limit, search, assigneeEmail: assignee_email, pentestType: pentest_type, applicationId: application_id,
|
|
430
|
+
}));
|
|
431
|
+
|
|
432
|
+
tool('get_pentest_artifact', {
|
|
433
|
+
title: 'Pentest Artifact Details',
|
|
434
|
+
desc: 'Get an AI-Pentest artifact by ID, including scope and executions.',
|
|
435
|
+
schema: z.object({ artifact_id: z.number() }),
|
|
436
|
+
}, ({ artifact_id }) => gql.get_pentest_artifact(artifact_id));
|
|
437
|
+
|
|
438
|
+
tool('get_pentest_execution', {
|
|
439
|
+
title: 'Pentest Execution Result',
|
|
440
|
+
desc: 'Get an AI-Pentest execution by ID: status, vulnerability count, severity breakdown, retest progress.',
|
|
441
|
+
schema: z.object({ execution_id: z.number() }),
|
|
442
|
+
}, ({ execution_id }) => gql.get_pentest_execution(execution_id));
|
|
443
|
+
|
|
444
|
+
tool('get_threat_model_artifacts', {
|
|
445
|
+
title: 'List Threat Model Artifacts',
|
|
446
|
+
desc: 'List Threat Modeling artifacts (label, scope, latest version). Optional: search, assignee_email, has_version.',
|
|
447
|
+
schema: z.object({
|
|
448
|
+
company_id: z.number(),
|
|
449
|
+
page: z.number().optional(),
|
|
728
450
|
limit: z.number().optional(),
|
|
451
|
+
search: z.string().optional(),
|
|
452
|
+
assignee_email: z.string().optional(),
|
|
453
|
+
has_version: z.boolean().optional(),
|
|
729
454
|
}),
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
name: z.string(),
|
|
455
|
+
}, ({ company_id, page, limit, search, assignee_email, has_version }) =>
|
|
456
|
+
gql.get_threat_model_artifacts(company_id, {
|
|
457
|
+
page, limit, search, assigneeEmail: assignee_email, hasVersion: has_version,
|
|
458
|
+
}));
|
|
459
|
+
|
|
460
|
+
tool('get_threat_model_artifact', {
|
|
461
|
+
title: 'Threat Model Artifact Details',
|
|
462
|
+
desc: 'Get a Threat Modeling artifact by ID, including its versions (diagrams, notes, scope).',
|
|
463
|
+
schema: z.object({ artifact_id: z.number() }),
|
|
464
|
+
}, ({ artifact_id }) => gql.get_threat_model_artifact(artifact_id));
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* MUTATIONS — engine (discover -> describe -> execute over the allowlist)
|
|
468
|
+
*/
|
|
469
|
+
|
|
470
|
+
tool('list_mutations', {
|
|
471
|
+
title: 'List Mutations',
|
|
472
|
+
desc: 'Discover the permitted write operations (name, description, category, destructive flag). Step 1 of the write workflow: list_mutations -> describe_mutation -> execute_mutation. Optional: search (substring), category (issue, ticket, project, asset, requirement, pentest, application, threat_model), limit (default 50).',
|
|
473
|
+
schema: z.object({
|
|
474
|
+
search: z.string().optional(),
|
|
475
|
+
category: z.string().optional(),
|
|
476
|
+
limit: z.number().optional(),
|
|
753
477
|
}),
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
},
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
server.registerTool(
|
|
772
|
-
'execute_mutation',
|
|
773
|
-
{
|
|
774
|
-
description: `Execute any allowlisted Conviso Platform mutation by name (use list_mutations to see the full set). Covers writes for issues/vulnerabilities, assets (+ DAST), tickets, projects, requirements, AI-pentest, applications and threat modeling.
|
|
775
|
-
|
|
776
|
-
Usage: call describe_mutation(name) first to learn the input shape, then pass it here. Every mutation takes a single input object, so variables must be { "input": { ...fields... } }.
|
|
777
|
-
|
|
778
|
-
- name: the mutation name (e.g. "changeIssueStatus", "createProject", "deleteAsset").
|
|
779
|
-
- variables: GraphQL variables, normally { input: { ... } }.
|
|
780
|
-
- return_fields: optional raw GraphQL selection set to override the default returned fields.
|
|
781
|
-
|
|
782
|
-
WARNING: this performs writes and can be destructive (the catalog marks delete/bulk/remove/cancel/revoke operations as destructive). Confirm intent before running delete or bulk mutations.`,
|
|
783
|
-
inputSchema: z.object({
|
|
478
|
+
local: true,
|
|
479
|
+
}, ({ search, category, limit }) => listMutations({ search, category, limit }));
|
|
480
|
+
|
|
481
|
+
tool('describe_mutation', {
|
|
482
|
+
title: 'Describe Mutation',
|
|
483
|
+
desc: 'Full input schema for one mutation: fields with types, required flags, enum values, nested inputs, plus the default returned fields. Call before execute_mutation.',
|
|
484
|
+
schema: z.object({ name: z.string() }),
|
|
485
|
+
local: true,
|
|
486
|
+
}, ({ name }) => describeMutation(name));
|
|
487
|
+
|
|
488
|
+
tool('execute_mutation', {
|
|
489
|
+
title: 'Execute Mutation',
|
|
490
|
+
desc: 'Run any permitted write operation by name (see list_mutations). variables is the mutation input — pass { input: {...} } or the input fields directly (auto-wrapped). Optional return_fields overrides the returned selection set. WARNING: performs writes; delete/bulk operations are destructive — confirm intent first.',
|
|
491
|
+
schema: z.object({
|
|
784
492
|
name: z.string(),
|
|
785
493
|
variables: z.record(z.string(), z.any()).optional(),
|
|
786
494
|
return_fields: z.string().optional(),
|
|
787
495
|
}),
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
);
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* MUTATIONS — curated typed shortcuts for the most common writes
|
|
807
|
-
*/
|
|
808
|
-
|
|
809
|
-
server.registerTool(
|
|
810
|
-
'change_issue_status',
|
|
811
|
-
{
|
|
812
|
-
description: `Change the status of an issue/vulnerability. status must be one of CREATED, DRAFT, IDENTIFIED, IN_PROGRESS, AWAITING_VALIDATION, FIX_ACCEPTED, RISK_ACCEPTED, FALSE_POSITIVE, SUPPRESSED. Pass 'extra' for advanced ChangeIssueStatusInput fields (e.g. riskAcceptedUntil, externalAuthorIdentifier).`,
|
|
813
|
-
inputSchema: z.object({
|
|
496
|
+
write: true,
|
|
497
|
+
destructive: true,
|
|
498
|
+
}, ({ name, variables = {}, return_fields = null }) =>
|
|
499
|
+
gql.executeMutation(name, variables, return_fields));
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* MUTATIONS — typed shortcuts for the most common writes
|
|
503
|
+
*/
|
|
504
|
+
|
|
505
|
+
tool('change_issue_status', {
|
|
506
|
+
title: 'Change Issue Status',
|
|
507
|
+
desc: `Change an issue's status. status: one of ${ISSUE_STATUSES}. Optional reason; extra = advanced ChangeIssueStatusInput fields (e.g. riskAcceptedUntil).`,
|
|
508
|
+
schema: z.object({
|
|
814
509
|
issue_id: z.number(),
|
|
815
510
|
status: z.string(),
|
|
816
511
|
reason: z.string().optional(),
|
|
817
512
|
extra: z.record(z.string(), z.any()).optional(),
|
|
818
513
|
}),
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
readOnlyHint: false,
|
|
822
|
-
destructiveHint: false,
|
|
823
|
-
idempotentHint: false,
|
|
824
|
-
openWorldHint: true,
|
|
825
|
-
},
|
|
826
|
-
},
|
|
827
|
-
async ({ issue_id, status, reason, extra }) => {
|
|
828
|
-
try {
|
|
829
|
-
return ok(await gateway.change_issue_status({ issue_id, status, reason, extra }));
|
|
830
|
-
} catch (err) {
|
|
831
|
-
return fail(err, 'Failed to change issue status');
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
);
|
|
514
|
+
write: true,
|
|
515
|
+
}, (a) => gql.change_issue_status(a));
|
|
835
516
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
inputSchema: z.object({
|
|
517
|
+
tool('create_source_code_vulnerability', {
|
|
518
|
+
title: 'Create Source Code Vulnerability',
|
|
519
|
+
desc: `Create a manual source-code (SAST-style) vulnerability on an asset. severity: ${SEVERITIES}. impact_level/probability_level: LOW, MEDIUM, HIGH (default MEDIUM). status defaults to DRAFT. extra = any other CreateSourceCodeVulnerabilityInput field.`,
|
|
520
|
+
schema: z.object({
|
|
841
521
|
asset_id: z.number(),
|
|
842
522
|
title: z.string(),
|
|
843
523
|
description: z.string(),
|
|
@@ -862,28 +542,13 @@ server.registerTool(
|
|
|
862
542
|
patterns: z.array(z.string()).optional(),
|
|
863
543
|
extra: z.record(z.string(), z.any()).optional(),
|
|
864
544
|
}),
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
readOnlyHint: false,
|
|
868
|
-
destructiveHint: false,
|
|
869
|
-
idempotentHint: false,
|
|
870
|
-
openWorldHint: true,
|
|
871
|
-
},
|
|
872
|
-
},
|
|
873
|
-
async (args) => {
|
|
874
|
-
try {
|
|
875
|
-
return ok(await gateway.create_source_code_vulnerability(args));
|
|
876
|
-
} catch (err) {
|
|
877
|
-
return fail(err, 'Failed to create source code vulnerability');
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
);
|
|
545
|
+
write: true,
|
|
546
|
+
}, (a) => gql.create_source_code_vulnerability(a));
|
|
881
547
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
inputSchema: z.object({
|
|
548
|
+
tool('create_project', {
|
|
549
|
+
title: 'Create Project',
|
|
550
|
+
desc: 'Create a project. Required: company_id, type_id (project type id), label, goal, scope, start_date (YYYY-MM-DD). Optional: end_date; extra = advanced CreateProjectInput fields (assetsIds, tags, allocatedPortalUserEmails...).',
|
|
551
|
+
schema: z.object({
|
|
887
552
|
company_id: z.number(),
|
|
888
553
|
type_id: z.number(),
|
|
889
554
|
label: z.string(),
|
|
@@ -893,28 +558,13 @@ server.registerTool(
|
|
|
893
558
|
end_date: z.string().optional(),
|
|
894
559
|
extra: z.record(z.string(), z.any()).optional(),
|
|
895
560
|
}),
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
readOnlyHint: false,
|
|
899
|
-
destructiveHint: false,
|
|
900
|
-
idempotentHint: false,
|
|
901
|
-
openWorldHint: true,
|
|
902
|
-
},
|
|
903
|
-
},
|
|
904
|
-
async (args) => {
|
|
905
|
-
try {
|
|
906
|
-
return ok(await gateway.create_project(args));
|
|
907
|
-
} catch (err) {
|
|
908
|
-
return fail(err, 'Failed to create project');
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
);
|
|
561
|
+
write: true,
|
|
562
|
+
}, (a) => gql.create_project(a));
|
|
912
563
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
inputSchema: z.object({
|
|
564
|
+
tool('create_asset', {
|
|
565
|
+
title: 'Create Asset',
|
|
566
|
+
desc: 'Create an asset. Required: company_id, name. Optional: asset_type, url, description, business_impact (LOW, MEDIUM, HIGH, NOT_DEFINED), exploitability (INTERNET_FACING, INTERNAL, NOT_DEFINED), tags; extra = advanced CreateAssetInput fields.',
|
|
567
|
+
schema: z.object({
|
|
918
568
|
company_id: z.number(),
|
|
919
569
|
name: z.string(),
|
|
920
570
|
asset_type: z.string().optional(),
|
|
@@ -925,28 +575,13 @@ server.registerTool(
|
|
|
925
575
|
tags: z.array(z.string()).optional(),
|
|
926
576
|
extra: z.record(z.string(), z.any()).optional(),
|
|
927
577
|
}),
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
readOnlyHint: false,
|
|
931
|
-
destructiveHint: false,
|
|
932
|
-
idempotentHint: false,
|
|
933
|
-
openWorldHint: true,
|
|
934
|
-
},
|
|
935
|
-
},
|
|
936
|
-
async (args) => {
|
|
937
|
-
try {
|
|
938
|
-
return ok(await gateway.create_asset(args));
|
|
939
|
-
} catch (err) {
|
|
940
|
-
return fail(err, 'Failed to create asset');
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
);
|
|
578
|
+
write: true,
|
|
579
|
+
}, (a) => gql.create_asset(a));
|
|
944
580
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
inputSchema: z.object({
|
|
581
|
+
tool('create_ticket', {
|
|
582
|
+
title: 'Create Ticket',
|
|
583
|
+
desc: 'Open a ticket. Required: company_id, type (BUG, FEATURE_REQUEST, PROJECT_REQUEST, SUPPORT_REQUEST), title, description. Optional: priority (P1, P2, P3), impact (LOW, MEDIUM, HIGH); extra = advanced CreateTicketInput fields.',
|
|
584
|
+
schema: z.object({
|
|
950
585
|
company_id: z.number(),
|
|
951
586
|
type: z.string(),
|
|
952
587
|
title: z.string(),
|
|
@@ -955,64 +590,27 @@ server.registerTool(
|
|
|
955
590
|
impact: z.string().optional(),
|
|
956
591
|
extra: z.record(z.string(), z.any()).optional(),
|
|
957
592
|
}),
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
},
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
);
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
server.registerTool(
|
|
980
|
-
'run_dast',
|
|
981
|
-
{
|
|
982
|
-
description: 'Start a Conviso DAST scan on an asset (startConvisoDast). Required: asset_id.',
|
|
983
|
-
inputSchema: z.object({ asset_id: z.number(), extra: z.record(z.string(), z.any()).optional() }),
|
|
984
|
-
annotations: { title: 'Run DAST', ...WRITE },
|
|
985
|
-
},
|
|
986
|
-
async ({ asset_id, extra }) => {
|
|
987
|
-
try {
|
|
988
|
-
return ok(await gateway.run_dast({ asset_id, extra }));
|
|
989
|
-
} catch (err) {
|
|
990
|
-
return fail(err, 'Failed to start DAST');
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
);
|
|
994
|
-
|
|
995
|
-
server.registerTool(
|
|
996
|
-
'trigger_pentest',
|
|
997
|
-
{
|
|
998
|
-
description: 'Trigger an AI-Pentest execution from an existing pentest artifact (createPentestExecution). Required: artifact_id.',
|
|
999
|
-
inputSchema: z.object({ artifact_id: z.number(), extra: z.record(z.string(), z.any()).optional() }),
|
|
1000
|
-
annotations: { title: 'Trigger AI-Pentest', ...WRITE },
|
|
1001
|
-
},
|
|
1002
|
-
async ({ artifact_id, extra }) => {
|
|
1003
|
-
try {
|
|
1004
|
-
return ok(await gateway.trigger_pentest({ artifact_id, extra }));
|
|
1005
|
-
} catch (err) {
|
|
1006
|
-
return fail(err, 'Failed to trigger pentest');
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
);
|
|
1010
|
-
|
|
1011
|
-
server.registerTool(
|
|
1012
|
-
'create_pentest_artifact',
|
|
1013
|
-
{
|
|
1014
|
-
description: 'Create an AI-Pentest artifact (the scope/config a pentest runs against). Required: company_id, application_id, label, pentest_type. Optional: description, scope_text, assignee_email, domains, in_scope, out_scope. Pass \'extra\' for advanced fields (scheduling, repositories, documentation, size/depth).',
|
|
1015
|
-
inputSchema: z.object({
|
|
593
|
+
write: true,
|
|
594
|
+
}, (a) => gql.create_ticket(a));
|
|
595
|
+
|
|
596
|
+
tool('run_dast', {
|
|
597
|
+
title: 'Run DAST',
|
|
598
|
+
desc: 'Start a Conviso DAST scan on an asset (startConvisoDast). Required: asset_id.',
|
|
599
|
+
schema: z.object({ asset_id: z.number(), extra: z.record(z.string(), z.any()).optional() }),
|
|
600
|
+
write: true,
|
|
601
|
+
}, (a) => gql.run_dast(a));
|
|
602
|
+
|
|
603
|
+
tool('trigger_pentest', {
|
|
604
|
+
title: 'Trigger AI-Pentest',
|
|
605
|
+
desc: 'Trigger an AI-Pentest execution from an existing pentest artifact (createPentestExecution). Required: artifact_id.',
|
|
606
|
+
schema: z.object({ artifact_id: z.number(), extra: z.record(z.string(), z.any()).optional() }),
|
|
607
|
+
write: true,
|
|
608
|
+
}, (a) => gql.trigger_pentest(a));
|
|
609
|
+
|
|
610
|
+
tool('create_pentest_artifact', {
|
|
611
|
+
title: 'Create Pentest Artifact',
|
|
612
|
+
desc: 'Create an AI-Pentest artifact (the scope/config a pentest runs against). Required: company_id, application_id, label, pentest_type. Optional: description, scope_text, assignee_email, domains, in_scope, out_scope; extra = advanced fields (scheduling, repositories, documentation, size/depth).',
|
|
613
|
+
schema: z.object({
|
|
1016
614
|
company_id: z.number(),
|
|
1017
615
|
application_id: z.number(),
|
|
1018
616
|
label: z.string(),
|
|
@@ -1025,303 +623,11 @@ server.registerTool(
|
|
|
1025
623
|
out_scope: z.array(z.string()).optional(),
|
|
1026
624
|
extra: z.record(z.string(), z.any()).optional(),
|
|
1027
625
|
}),
|
|
1028
|
-
|
|
1029
|
-
},
|
|
1030
|
-
async (args) => {
|
|
1031
|
-
try {
|
|
1032
|
-
return ok(await gateway.create_pentest_artifact(args));
|
|
1033
|
-
} catch (err) {
|
|
1034
|
-
return fail(err, 'Failed to create pentest artifact');
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
);
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* READS — Tickets, Requirements, Applications, Scans, Supply chain, AI-Pentest, Threat Modeling
|
|
1041
|
-
*/
|
|
1042
|
-
|
|
1043
|
-
server.registerTool(
|
|
1044
|
-
'get_tickets',
|
|
1045
|
-
{
|
|
1046
|
-
description: 'List tickets for a company (paginated). Optional: search (title/description), sort_by, descending, and params (TicketSearch: types, statuses, priorities, impacts, tags, mineOnly...).',
|
|
1047
|
-
inputSchema: z.object({
|
|
1048
|
-
company_id: z.number(),
|
|
1049
|
-
page: z.number().optional(),
|
|
1050
|
-
limit: z.number().optional(),
|
|
1051
|
-
search: z.string().optional(),
|
|
1052
|
-
sort_by: z.string().optional(),
|
|
1053
|
-
descending: z.boolean().optional(),
|
|
1054
|
-
params: z.record(z.string(), z.any()).optional(),
|
|
1055
|
-
}),
|
|
1056
|
-
annotations: { title: 'List Tickets', ...READ },
|
|
1057
|
-
},
|
|
1058
|
-
async ({ company_id, page, limit, search, sort_by, descending, params }) => {
|
|
1059
|
-
try {
|
|
1060
|
-
return ok(await gateway.get_tickets(company_id, { page, limit, search, sort_by, descending, params }));
|
|
1061
|
-
} catch (err) {
|
|
1062
|
-
return fail(err, 'Failed to list tickets');
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
);
|
|
1066
|
-
|
|
1067
|
-
server.registerTool(
|
|
1068
|
-
'get_ticket',
|
|
1069
|
-
{
|
|
1070
|
-
description: 'Get a single ticket by id, including status, priority, impact and assignee.',
|
|
1071
|
-
inputSchema: z.object({ company_id: z.number(), ticket_id: z.number() }),
|
|
1072
|
-
annotations: { title: 'Ticket Details', ...READ },
|
|
1073
|
-
},
|
|
1074
|
-
async ({ company_id, ticket_id }) => {
|
|
1075
|
-
try {
|
|
1076
|
-
return ok(await gateway.get_ticket(company_id, ticket_id));
|
|
1077
|
-
} catch (err) {
|
|
1078
|
-
return fail(err, 'Failed to get ticket');
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
);
|
|
626
|
+
write: true,
|
|
627
|
+
}, (a) => gql.create_pentest_artifact(a));
|
|
1082
628
|
|
|
1083
|
-
server
|
|
1084
|
-
|
|
1085
|
-
{
|
|
1086
|
-
description: 'List security requirements/checklists for a scope (company) id, paginated. Optional: filters (RequirementsFilterInput).',
|
|
1087
|
-
inputSchema: z.object({
|
|
1088
|
-
scope_id: z.number(),
|
|
1089
|
-
page: z.number().optional(),
|
|
1090
|
-
limit: z.number().optional(),
|
|
1091
|
-
filters: z.record(z.string(), z.any()).optional(),
|
|
1092
|
-
}),
|
|
1093
|
-
annotations: { title: 'List Requirements', ...READ },
|
|
1094
|
-
},
|
|
1095
|
-
async ({ scope_id, page, limit, filters }) => {
|
|
1096
|
-
try {
|
|
1097
|
-
return ok(await gateway.get_requirements(scope_id, { page, limit, filters }));
|
|
1098
|
-
} catch (err) {
|
|
1099
|
-
return fail(err, 'Failed to list requirements');
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
);
|
|
1103
|
-
|
|
1104
|
-
server.registerTool(
|
|
1105
|
-
'get_requirement',
|
|
1106
|
-
{
|
|
1107
|
-
description: 'Get a single requirement/checklist by id.',
|
|
1108
|
-
inputSchema: z.object({ company_id: z.number(), requirement_id: z.number() }),
|
|
1109
|
-
annotations: { title: 'Requirement Details', ...READ },
|
|
1110
|
-
},
|
|
1111
|
-
async ({ company_id, requirement_id }) => {
|
|
1112
|
-
try {
|
|
1113
|
-
return ok(await gateway.get_requirement(company_id, requirement_id));
|
|
1114
|
-
} catch (err) {
|
|
1115
|
-
return fail(err, 'Failed to get requirement');
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
);
|
|
1119
|
-
|
|
1120
|
-
server.registerTool(
|
|
1121
|
-
'get_project_requirements',
|
|
1122
|
-
{
|
|
1123
|
-
description: 'List the requirements/checklists attached to a specific project.',
|
|
1124
|
-
inputSchema: z.object({ project_id: z.number() }),
|
|
1125
|
-
annotations: { title: 'Project Requirements', ...READ },
|
|
1126
|
-
},
|
|
1127
|
-
async ({ project_id }) => {
|
|
1128
|
-
try {
|
|
1129
|
-
return ok(await gateway.get_project_requirements(project_id));
|
|
1130
|
-
} catch (err) {
|
|
1131
|
-
return fail(err, 'Failed to list project requirements');
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
);
|
|
1135
|
-
|
|
1136
|
-
server.registerTool(
|
|
1137
|
-
'get_applications',
|
|
1138
|
-
{
|
|
1139
|
-
description: 'List applications for a company (id, name, url, riskScore, assetsCount). Optional: search by name.',
|
|
1140
|
-
inputSchema: z.object({ company_id: z.number(), search: z.string().optional() }),
|
|
1141
|
-
annotations: { title: 'List Applications', ...READ },
|
|
1142
|
-
},
|
|
1143
|
-
async ({ company_id, search }) => {
|
|
1144
|
-
try {
|
|
1145
|
-
return ok(await gateway.get_applications(company_id, search));
|
|
1146
|
-
} catch (err) {
|
|
1147
|
-
return fail(err, 'Failed to list applications');
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
);
|
|
1151
|
-
|
|
1152
|
-
server.registerTool(
|
|
1153
|
-
'get_application',
|
|
1154
|
-
{
|
|
1155
|
-
description: 'Get a single application by id, including its linked assets.',
|
|
1156
|
-
inputSchema: z.object({ company_id: z.number(), application_id: z.number() }),
|
|
1157
|
-
annotations: { title: 'Application Details', ...READ },
|
|
1158
|
-
},
|
|
1159
|
-
async ({ company_id, application_id }) => {
|
|
1160
|
-
try {
|
|
1161
|
-
return ok(await gateway.get_application(company_id, application_id));
|
|
1162
|
-
} catch (err) {
|
|
1163
|
-
return fail(err, 'Failed to get application');
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
);
|
|
1167
|
-
|
|
1168
|
-
server.registerTool(
|
|
1169
|
-
'get_scan_histories',
|
|
1170
|
-
{
|
|
1171
|
-
description: 'List scan execution histories for a company (status, integration, durations, vulnerability counts). Optional: asset_ids, page, limit, filters (ScansHistoriesFiltersInput).',
|
|
1172
|
-
inputSchema: z.object({
|
|
1173
|
-
company_id: z.number(),
|
|
1174
|
-
asset_ids: z.array(z.number()).optional(),
|
|
1175
|
-
page: z.number().optional(),
|
|
1176
|
-
limit: z.number().optional(),
|
|
1177
|
-
filters: z.record(z.string(), z.any()).optional(),
|
|
1178
|
-
}),
|
|
1179
|
-
annotations: { title: 'List Scan Histories', ...READ },
|
|
1180
|
-
},
|
|
1181
|
-
async ({ company_id, asset_ids, page, limit, filters }) => {
|
|
1182
|
-
try {
|
|
1183
|
-
return ok(await gateway.get_scan_histories(company_id, { assetIds: asset_ids, page, limit, filters }));
|
|
1184
|
-
} catch (err) {
|
|
1185
|
-
return fail(err, 'Failed to list scan histories');
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
);
|
|
1189
|
-
|
|
1190
|
-
server.registerTool(
|
|
1191
|
-
'get_asset_scans_count',
|
|
1192
|
-
{
|
|
1193
|
-
description: 'Get scan coverage counts for a company (assets with scans, without scans, and which scans are considered).',
|
|
1194
|
-
inputSchema: z.object({ company_id: z.number() }),
|
|
1195
|
-
annotations: { title: 'Asset Scans Count', ...READ },
|
|
1196
|
-
},
|
|
1197
|
-
async ({ company_id }) => {
|
|
1198
|
-
try {
|
|
1199
|
-
return ok(await gateway.get_asset_scans_count(company_id));
|
|
1200
|
-
} catch (err) {
|
|
1201
|
-
return fail(err, 'Failed to get asset scans count');
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
);
|
|
1205
|
-
|
|
1206
|
-
server.registerTool(
|
|
1207
|
-
'get_sbom_components',
|
|
1208
|
-
{
|
|
1209
|
-
description: 'List Software Bill of Materials (SBOM) / supply-chain components for a company (name, version, technology, package manager, license, issues by severity). Optional: search (SbomComponentSearchInput).',
|
|
1210
|
-
inputSchema: z.object({
|
|
1211
|
-
company_id: z.number(),
|
|
1212
|
-
page: z.number().optional(),
|
|
1213
|
-
limit: z.number().optional(),
|
|
1214
|
-
search: z.record(z.string(), z.any()).optional(),
|
|
1215
|
-
}),
|
|
1216
|
-
annotations: { title: 'List SBOM Components', ...READ },
|
|
1217
|
-
},
|
|
1218
|
-
async ({ company_id, page, limit, search }) => {
|
|
1219
|
-
try {
|
|
1220
|
-
return ok(await gateway.get_sbom_components(company_id, { page, limit, search }));
|
|
1221
|
-
} catch (err) {
|
|
1222
|
-
return fail(err, 'Failed to list SBOM components');
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
);
|
|
1226
|
-
|
|
1227
|
-
server.registerTool(
|
|
1228
|
-
'get_pentest_artifacts',
|
|
1229
|
-
{
|
|
1230
|
-
description: 'List AI-Pentest artifacts for a company (label, type, scheduling, latest execution). Optional: search, assignee_email, pentest_type, application_id.',
|
|
1231
|
-
inputSchema: z.object({
|
|
1232
|
-
company_id: z.number(),
|
|
1233
|
-
page: z.number().optional(),
|
|
1234
|
-
limit: z.number().optional(),
|
|
1235
|
-
search: z.string().optional(),
|
|
1236
|
-
assignee_email: z.string().optional(),
|
|
1237
|
-
pentest_type: z.string().optional(),
|
|
1238
|
-
application_id: z.number().optional(),
|
|
1239
|
-
}),
|
|
1240
|
-
annotations: { title: 'List Pentest Artifacts', ...READ },
|
|
1241
|
-
},
|
|
1242
|
-
async ({ company_id, page, limit, search, assignee_email, pentest_type, application_id }) => {
|
|
1243
|
-
try {
|
|
1244
|
-
return ok(await gateway.get_pentest_artifacts(company_id, {
|
|
1245
|
-
page, limit, search, assigneeEmail: assignee_email, pentestType: pentest_type, applicationId: application_id,
|
|
1246
|
-
}));
|
|
1247
|
-
} catch (err) {
|
|
1248
|
-
return fail(err, 'Failed to list pentest artifacts');
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
);
|
|
1252
|
-
|
|
1253
|
-
server.registerTool(
|
|
1254
|
-
'get_pentest_artifact',
|
|
1255
|
-
{
|
|
1256
|
-
description: 'Get a single AI-Pentest artifact by id, including scope and its executions.',
|
|
1257
|
-
inputSchema: z.object({ artifact_id: z.number() }),
|
|
1258
|
-
annotations: { title: 'Pentest Artifact Details', ...READ },
|
|
1259
|
-
},
|
|
1260
|
-
async ({ artifact_id }) => {
|
|
1261
|
-
try {
|
|
1262
|
-
return ok(await gateway.get_pentest_artifact(artifact_id));
|
|
1263
|
-
} catch (err) {
|
|
1264
|
-
return fail(err, 'Failed to get pentest artifact');
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
);
|
|
1268
|
-
|
|
1269
|
-
server.registerTool(
|
|
1270
|
-
'get_pentest_execution',
|
|
1271
|
-
{
|
|
1272
|
-
description: 'Get the result/status of a single AI-Pentest execution by id (status, vulnerability count, severity breakdown, retest progress).',
|
|
1273
|
-
inputSchema: z.object({ execution_id: z.number() }),
|
|
1274
|
-
annotations: { title: 'Pentest Execution Result', ...READ },
|
|
1275
|
-
},
|
|
1276
|
-
async ({ execution_id }) => {
|
|
1277
|
-
try {
|
|
1278
|
-
return ok(await gateway.get_pentest_execution(execution_id));
|
|
1279
|
-
} catch (err) {
|
|
1280
|
-
return fail(err, 'Failed to get pentest execution');
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
);
|
|
1284
|
-
|
|
1285
|
-
server.registerTool(
|
|
1286
|
-
'get_threat_model_artifacts',
|
|
1287
|
-
{
|
|
1288
|
-
description: 'List Threat Modeling artifacts for a company (label, scope, latest version). Optional: search, assignee_email, has_version.',
|
|
1289
|
-
inputSchema: z.object({
|
|
1290
|
-
company_id: z.number(),
|
|
1291
|
-
page: z.number().optional(),
|
|
1292
|
-
limit: z.number().optional(),
|
|
1293
|
-
search: z.string().optional(),
|
|
1294
|
-
assignee_email: z.string().optional(),
|
|
1295
|
-
has_version: z.boolean().optional(),
|
|
1296
|
-
}),
|
|
1297
|
-
annotations: { title: 'List Threat Model Artifacts', ...READ },
|
|
1298
|
-
},
|
|
1299
|
-
async ({ company_id, page, limit, search, assignee_email, has_version }) => {
|
|
1300
|
-
try {
|
|
1301
|
-
return ok(await gateway.get_threat_model_artifacts(company_id, {
|
|
1302
|
-
page, limit, search, assigneeEmail: assignee_email, hasVersion: has_version,
|
|
1303
|
-
}));
|
|
1304
|
-
} catch (err) {
|
|
1305
|
-
return fail(err, 'Failed to list threat model artifacts');
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
);
|
|
1309
|
-
|
|
1310
|
-
server.registerTool(
|
|
1311
|
-
'get_threat_model_artifact',
|
|
1312
|
-
{
|
|
1313
|
-
description: 'Get a single Threat Modeling artifact by id, including its versions (diagrams, notes, scope).',
|
|
1314
|
-
inputSchema: z.object({ artifact_id: z.number() }),
|
|
1315
|
-
annotations: { title: 'Threat Model Artifact Details', ...READ },
|
|
1316
|
-
},
|
|
1317
|
-
async ({ artifact_id }) => {
|
|
1318
|
-
try {
|
|
1319
|
-
return ok(await gateway.get_threat_model_artifact(artifact_id));
|
|
1320
|
-
} catch (err) {
|
|
1321
|
-
return fail(err, 'Failed to get threat model artifact');
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
);
|
|
629
|
+
return server;
|
|
630
|
+
}
|
|
1325
631
|
|
|
1326
632
|
/**
|
|
1327
633
|
* START
|
|
@@ -1335,7 +641,14 @@ if (PORT) {
|
|
|
1335
641
|
res.writeHead(405).end();
|
|
1336
642
|
return;
|
|
1337
643
|
}
|
|
644
|
+
// Stateless HTTP: a fresh server + transport per request (SDK pattern). Reusing one
|
|
645
|
+
// McpServer across concurrent transports leaks state between requests.
|
|
646
|
+
const server = buildServer();
|
|
1338
647
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
648
|
+
res.on('close', () => {
|
|
649
|
+
transport.close();
|
|
650
|
+
server.close();
|
|
651
|
+
});
|
|
1339
652
|
await server.connect(transport);
|
|
1340
653
|
await transport.handleRequest(req, res);
|
|
1341
654
|
});
|
|
@@ -1344,7 +657,8 @@ if (PORT) {
|
|
|
1344
657
|
console.error(`Conviso MCP Server running on HTTP port ${PORT}`);
|
|
1345
658
|
});
|
|
1346
659
|
} else {
|
|
660
|
+
const server = buildServer();
|
|
1347
661
|
const transport = new StdioServerTransport();
|
|
1348
662
|
await server.connect(transport);
|
|
1349
663
|
console.error('Conviso MCP Server running on stdio');
|
|
1350
|
-
}
|
|
664
|
+
}
|