@convisoappsec/mcp 0.3.1 → 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 +28 -18
- package/package.json +8 -4
- package/src/conviso_mcp/filters.js +54 -0
- package/src/conviso_mcp/graphql_client.js +538 -162
- package/src/conviso_mcp/mutations.js +126 -0
- package/src/conviso_mcp/mutations_catalog.json +3174 -0
- package/src/conviso_mcp/operation_allowlist.js +74 -0
- package/src/conviso_mcp/server.js +552 -326
- package/src/conviso_mcp/feed_gateway.js +0 -80
|
@@ -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.3.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)}`;
|
|
@@ -39,7 +36,16 @@ function sanitizeError(err, message = 'Request failed') {
|
|
|
39
36
|
status,
|
|
40
37
|
});
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
const result = { error: message, status, error_id };
|
|
40
|
+
// GraphQL errors describe the caller's own request (e.g. a missing required field) — pass
|
|
41
|
+
// them through so the model can fix the input on the next attempt.
|
|
42
|
+
if (Array.isArray(err?.graphqlErrors) && err.graphqlErrors.length) {
|
|
43
|
+
result.details = err.graphqlErrors;
|
|
44
|
+
}
|
|
45
|
+
if (err?.authHint) {
|
|
46
|
+
result.hint = err.authHint;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
function ok(data) {
|
|
@@ -53,284 +59,243 @@ function ok(data) {
|
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
|
|
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
|
+
});
|
|
76
|
+
|
|
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
|
+
}
|
|
59
101
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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({
|
|
65
110
|
page: z.number().optional(),
|
|
66
111
|
limit: z.number().optional(),
|
|
67
112
|
search: z.string().optional(),
|
|
113
|
+
label_eq: z.string().optional(),
|
|
68
114
|
}),
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
);
|
|
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));
|
|
107
123
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
inputSchema: z.object({
|
|
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({
|
|
113
128
|
id: z.number(),
|
|
114
129
|
return_vulnerable_data: z.boolean().optional(),
|
|
115
130
|
}),
|
|
116
|
-
|
|
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
|
-
);
|
|
131
|
+
}, ({ id, return_vulnerable_data }) => gql.get_issue_by_id(id, return_vulnerable_data));
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
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({
|
|
138
137
|
company_id: z.number(),
|
|
139
138
|
page: z.number().optional(),
|
|
140
139
|
limit: z.number().optional(),
|
|
141
140
|
project_id: z.number().optional(),
|
|
141
|
+
asset_id: z.number().optional(),
|
|
142
|
+
search: z.string().optional(),
|
|
143
|
+
severities: z.array(z.string()).optional(),
|
|
144
|
+
statuses: z.array(z.string()).optional(),
|
|
145
|
+
sla_states: z.array(z.string()).optional(),
|
|
146
|
+
created_after: z.string().optional(),
|
|
147
|
+
created_before: z.string().optional(),
|
|
148
|
+
assignee_emails: z.array(z.string()).optional(),
|
|
149
|
+
sort_by: z.string().optional(),
|
|
150
|
+
order: z.string().optional(),
|
|
151
|
+
extra_filters: z.record(z.string(), z.any()).optional(),
|
|
142
152
|
}),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
);
|
|
153
|
+
}, ({
|
|
154
|
+
company_id, page = 1, limit = 10, project_id, asset_id, search = '',
|
|
155
|
+
severities, statuses, sla_states, created_after, created_before,
|
|
156
|
+
assignee_emails, sort_by, order = 'DESC', extra_filters,
|
|
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
|
+
}));
|
|
181
173
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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({
|
|
178
|
+
company_id: z.number(),
|
|
179
|
+
severities: z.array(z.string()).optional(),
|
|
180
|
+
statuses: z.array(z.string()).optional(),
|
|
181
|
+
asset_ids: z.array(z.number()).optional(),
|
|
182
|
+
asset_tags: z.array(z.string()).optional(),
|
|
183
|
+
created_after: z.string().optional(),
|
|
184
|
+
created_before: z.string().optional(),
|
|
185
|
+
}),
|
|
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({
|
|
187
200
|
company_id: z.number(),
|
|
188
201
|
page: z.number().optional(),
|
|
189
202
|
limit: z.number().optional(),
|
|
190
203
|
search: z.string().optional(),
|
|
204
|
+
statuses: z.array(z.string()).optional(),
|
|
205
|
+
project_types: z.array(z.string()).optional(),
|
|
206
|
+
created_after: z.string().optional(),
|
|
207
|
+
created_before: z.string().optional(),
|
|
208
|
+
tags: z.array(z.string()).optional(),
|
|
209
|
+
analyst_emails: z.array(z.string()).optional(),
|
|
210
|
+
sort_by: z.string().optional(),
|
|
211
|
+
descending: z.boolean().optional(),
|
|
191
212
|
}),
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
);
|
|
213
|
+
}, ({
|
|
214
|
+
company_id, page = 1, limit = 25, search = '', statuses, project_types,
|
|
215
|
+
created_after, created_before, tags, analyst_emails, sort_by = 'createdAt',
|
|
216
|
+
descending = true,
|
|
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));
|
|
252
233
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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({
|
|
258
244
|
company_id: z.number(),
|
|
259
245
|
page: z.number().optional(),
|
|
260
246
|
limit: z.number().optional(),
|
|
247
|
+
name: z.string().optional(),
|
|
248
|
+
search: z.string().optional(),
|
|
249
|
+
tags: z.array(z.string()).optional(),
|
|
250
|
+
technology: z.array(z.string()).optional(),
|
|
251
|
+
business_impact: z.array(z.string()).optional(),
|
|
252
|
+
exploitability: z.array(z.string()).optional(),
|
|
253
|
+
asset_type: z.string().optional(),
|
|
254
|
+
environment_compromised: z.boolean().optional(),
|
|
255
|
+
covered_by_scan: z.boolean().optional(),
|
|
256
|
+
sort_by: z.string().optional(),
|
|
257
|
+
order: z.string().optional(),
|
|
258
|
+
extra_filters: z.record(z.string(), z.any()).optional(),
|
|
261
259
|
}),
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
260
|
+
}, ({
|
|
261
|
+
company_id, page = 1, limit = 25, name, search, tags, technology,
|
|
262
|
+
business_impact, exploitability, asset_type, environment_compromised,
|
|
263
|
+
covered_by_scan, sort_by, order, extra_filters,
|
|
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
278
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
);
|
|
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}`);
|
|
303
286
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
);
|
|
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}`);
|
|
328
294
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
inputSchema: z.object({
|
|
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({
|
|
334
299
|
company_id: z.number(),
|
|
335
300
|
start_date: z.string(),
|
|
336
301
|
end_date: z.string(),
|
|
@@ -339,77 +304,330 @@ server.registerTool(
|
|
|
339
304
|
asset_ids: z.array(z.number()).optional(),
|
|
340
305
|
asset_tags: z.array(z.string()).optional(),
|
|
341
306
|
}),
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
);
|
|
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
|
+
*/
|
|
366
328
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
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({
|
|
372
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(),
|
|
373
340
|
}),
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
},
|
|
381
|
-
},
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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 }));
|
|
414
|
+
|
|
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(),
|
|
422
|
+
search: z.string().optional(),
|
|
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(),
|
|
450
|
+
limit: z.number().optional(),
|
|
451
|
+
search: z.string().optional(),
|
|
452
|
+
assignee_email: z.string().optional(),
|
|
453
|
+
has_version: z.boolean().optional(),
|
|
454
|
+
}),
|
|
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(),
|
|
477
|
+
}),
|
|
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({
|
|
492
|
+
name: z.string(),
|
|
493
|
+
variables: z.record(z.string(), z.any()).optional(),
|
|
494
|
+
return_fields: z.string().optional(),
|
|
495
|
+
}),
|
|
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({
|
|
509
|
+
issue_id: z.number(),
|
|
510
|
+
status: z.string(),
|
|
511
|
+
reason: z.string().optional(),
|
|
512
|
+
extra: z.record(z.string(), z.any()).optional(),
|
|
513
|
+
}),
|
|
514
|
+
write: true,
|
|
515
|
+
}, (a) => gql.change_issue_status(a));
|
|
516
|
+
|
|
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({
|
|
521
|
+
asset_id: z.number(),
|
|
522
|
+
title: z.string(),
|
|
523
|
+
description: z.string(),
|
|
524
|
+
solution: z.string(),
|
|
525
|
+
severity: z.string(),
|
|
526
|
+
code_snippet: z.string(),
|
|
527
|
+
file_name: z.string(),
|
|
528
|
+
first_line: z.number(),
|
|
529
|
+
vulnerable_line: z.number(),
|
|
530
|
+
project_id: z.number().optional(),
|
|
531
|
+
impact_level: z.string().optional(),
|
|
532
|
+
probability_level: z.string().optional(),
|
|
533
|
+
status: z.string().optional(),
|
|
534
|
+
category: z.string().optional(),
|
|
535
|
+
reference: z.string().optional(),
|
|
536
|
+
summary: z.string().optional(),
|
|
537
|
+
impact_description: z.string().optional(),
|
|
538
|
+
steps_to_reproduce: z.string().optional(),
|
|
539
|
+
compromised_environment: z.boolean().optional(),
|
|
540
|
+
source: z.string().optional(),
|
|
541
|
+
sink: z.string().optional(),
|
|
542
|
+
patterns: z.array(z.string()).optional(),
|
|
543
|
+
extra: z.record(z.string(), z.any()).optional(),
|
|
544
|
+
}),
|
|
545
|
+
write: true,
|
|
546
|
+
}, (a) => gql.create_source_code_vulnerability(a));
|
|
547
|
+
|
|
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({
|
|
552
|
+
company_id: z.number(),
|
|
553
|
+
type_id: z.number(),
|
|
554
|
+
label: z.string(),
|
|
555
|
+
goal: z.string(),
|
|
556
|
+
scope: z.string(),
|
|
557
|
+
start_date: z.string(),
|
|
558
|
+
end_date: z.string().optional(),
|
|
559
|
+
extra: z.record(z.string(), z.any()).optional(),
|
|
560
|
+
}),
|
|
561
|
+
write: true,
|
|
562
|
+
}, (a) => gql.create_project(a));
|
|
563
|
+
|
|
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({
|
|
568
|
+
company_id: z.number(),
|
|
569
|
+
name: z.string(),
|
|
570
|
+
asset_type: z.string().optional(),
|
|
571
|
+
url: z.string().optional(),
|
|
572
|
+
description: z.string().optional(),
|
|
573
|
+
business_impact: z.string().optional(),
|
|
574
|
+
exploitability: z.string().optional(),
|
|
575
|
+
tags: z.array(z.string()).optional(),
|
|
576
|
+
extra: z.record(z.string(), z.any()).optional(),
|
|
577
|
+
}),
|
|
578
|
+
write: true,
|
|
579
|
+
}, (a) => gql.create_asset(a));
|
|
580
|
+
|
|
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({
|
|
585
|
+
company_id: z.number(),
|
|
586
|
+
type: z.string(),
|
|
587
|
+
title: z.string(),
|
|
588
|
+
description: z.string(),
|
|
589
|
+
priority: z.string().optional(),
|
|
590
|
+
impact: z.string().optional(),
|
|
591
|
+
extra: z.record(z.string(), z.any()).optional(),
|
|
592
|
+
}),
|
|
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({
|
|
614
|
+
company_id: z.number(),
|
|
615
|
+
application_id: z.number(),
|
|
616
|
+
label: z.string(),
|
|
617
|
+
pentest_type: z.string(),
|
|
618
|
+
description: z.string().optional(),
|
|
619
|
+
scope_text: z.string().optional(),
|
|
620
|
+
assignee_email: z.string().optional(),
|
|
621
|
+
domains: z.array(z.string()).optional(),
|
|
622
|
+
in_scope: z.array(z.string()).optional(),
|
|
623
|
+
out_scope: z.array(z.string()).optional(),
|
|
624
|
+
extra: z.record(z.string(), z.any()).optional(),
|
|
625
|
+
}),
|
|
626
|
+
write: true,
|
|
627
|
+
}, (a) => gql.create_pentest_artifact(a));
|
|
628
|
+
|
|
629
|
+
return server;
|
|
630
|
+
}
|
|
413
631
|
|
|
414
632
|
/**
|
|
415
633
|
* START
|
|
@@ -423,7 +641,14 @@ if (PORT) {
|
|
|
423
641
|
res.writeHead(405).end();
|
|
424
642
|
return;
|
|
425
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();
|
|
426
647
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
648
|
+
res.on('close', () => {
|
|
649
|
+
transport.close();
|
|
650
|
+
server.close();
|
|
651
|
+
});
|
|
427
652
|
await server.connect(transport);
|
|
428
653
|
await transport.handleRequest(req, res);
|
|
429
654
|
});
|
|
@@ -432,7 +657,8 @@ if (PORT) {
|
|
|
432
657
|
console.error(`Conviso MCP Server running on HTTP port ${PORT}`);
|
|
433
658
|
});
|
|
434
659
|
} else {
|
|
660
|
+
const server = buildServer();
|
|
435
661
|
const transport = new StdioServerTransport();
|
|
436
662
|
await server.connect(transport);
|
|
437
663
|
console.error('Conviso MCP Server running on stdio');
|
|
438
|
-
}
|
|
664
|
+
}
|