codeant-cli 0.5.8 → 0.5.10
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/findings.md +2 -2
- package/mcp.md +34 -18
- package/package.json +2 -1
- package/scans.md +16 -5
- package/src/commands/scans/dismissed.js +46 -6
- package/src/commands/scans/index.js +24 -6
- package/src/commands/scans/lib/categories.js +2 -0
- package/src/commands/scans/lib/normalize.js +14 -0
- package/src/commands/scans/results.js +3 -1
- package/src/index.js +17 -13
- package/src/mcp/server.js +203 -100
- package/src/mcp/shape.js +83 -0
- package/src/scans/fetchAdvancedScanResults.js +81 -4
- package/src/scans/fetchDismissedAlerts.js +15 -1
- package/src/scans/fetchIssueOverrides.js +36 -0
- package/src/scans/fetchScanResults.js +0 -1
- package/src/scm/azure/client.js +1 -1
- package/src/utils/intOption.js +18 -0
- package/src/utils/loginFlow.js +25 -19
package/src/mcp/server.js
CHANGED
|
@@ -8,18 +8,18 @@ import { runRepos } from '../commands/scans/repos.js';
|
|
|
8
8
|
import { runHistory } from '../commands/scans/history.js';
|
|
9
9
|
import { runGet } from '../commands/scans/get.js';
|
|
10
10
|
import { runResults } from '../commands/scans/results.js';
|
|
11
|
-
import { runDismissed } from '../commands/scans/dismissed.js';
|
|
11
|
+
import { runDismissed, runOverrides } from '../commands/scans/dismissed.js';
|
|
12
12
|
import { runStartScan } from '../commands/scans/start-scan.js';
|
|
13
13
|
import { runReviewHeadless } from '../reviewHeadless.js';
|
|
14
14
|
import * as scm from '../scm/index.js';
|
|
15
|
-
import { isAlreadyLoggedIn,
|
|
16
|
-
import { getConfigValue } from '../utils/config.js';
|
|
15
|
+
import { awaitLoginCompletion, isAlreadyLoggedIn, startLoginFlow } from '../utils/loginFlow.js';
|
|
17
16
|
import { runHotlistGet, runHotlistList } from '../hotlist/client.js';
|
|
18
17
|
import { logoutCodeAnt } from '../utils/logout.js';
|
|
19
18
|
import { runApiRequest } from '../commands/api/request.js';
|
|
20
19
|
import { runOrganizationAntipatterns } from '../findings/antipatterns.js';
|
|
21
20
|
import { runCloudFindingGet, runCloudFindings, runCloudHistory } from '../findings/cloud.js';
|
|
22
21
|
import { runPentestHistory, runPentestIssues, runPentestReport } from '../findings/pentest.js';
|
|
22
|
+
import { maxResultChars, shapeCloudFindings, shapeCloudHistory, shapePentestHistory, shapeRepos, tooLarge } from './shape.js';
|
|
23
23
|
|
|
24
24
|
const require = createRequire(import.meta.url);
|
|
25
25
|
const pkg = require('../../package.json');
|
|
@@ -34,28 +34,48 @@ function isReadOnly() {
|
|
|
34
34
|
return v !== '0' && v.toLowerCase() !== 'false';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const INSTRUCTIONS = `CodeAnt AI tools for repository scans, organization-wide security findings, cloud security, pentests, pull requests, and local code review.
|
|
38
|
+
|
|
39
|
+
- Start with codeant_scans_orgs. When it lists more than one connection, pass org and service to the Hotlist, anti-pattern, cloud, pentest, and API tools.
|
|
40
|
+
- Repository findings: codeant_scans_repos (use full_name) -> codeant_scans_history or codeant_scans_get -> codeant_scans_results, once per repository. Parallel calls are safe.
|
|
41
|
+
- Prioritized findings across the organization: codeant_hotlist_list, then codeant_hotlist_get for one finding.
|
|
42
|
+
- Cloud security: codeant_cloud_scan_history -> codeant_cloud_findings_list (pass status "FAIL" to skip passing checks) -> codeant_cloud_finding_get. Pentests: codeant_pentest_history -> codeant_pentest_issues or codeant_pentest_report.
|
|
43
|
+
- List tools return one page with a total. Page with limit and offset (cursor and next_cursor for the Hotlist), and narrow with filters instead of requesting everything. Results over the size limit are refused with a hint.
|
|
44
|
+
- codeant_review_local and the pull request tools work on the git repository in this server's working directory. Outside a repository, pass name and remote to the pull request tools; local review is unavailable.
|
|
45
|
+
- Pull request and comment tools call GitHub, GitLab, Bitbucket, or Azure DevOps directly and need a token for that provider.
|
|
46
|
+
- On an authentication error ("Missing API key", "Invalid API key", or access denied), call codeant_login with force: true (a configured token may be stale), show the returned loginUrl to the user, and after they finish signing in call codeant_login again without force to confirm.`;
|
|
47
|
+
|
|
48
|
+
function textResult(text, hint) {
|
|
49
|
+
if (text.length > maxResultChars()) return fail(tooLarge(text.length, hint));
|
|
50
|
+
return { content: [{ type: 'text', text }] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ok(value, hint) {
|
|
54
|
+
return textResult(JSON.stringify(value), hint);
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
function fail(err) {
|
|
42
|
-
const
|
|
58
|
+
const body = err instanceof Error ? { error: err.message } : err && typeof err === 'object' ? err : { error: String(err) };
|
|
43
59
|
return {
|
|
44
60
|
isError: true,
|
|
45
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
61
|
+
content: [{ type: 'text', text: JSON.stringify(body) }],
|
|
46
62
|
};
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
function maskToken(token) {
|
|
66
|
+
return token ? `${token.slice(0, 8)}…` : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
function resolveRepoOpts(input) {
|
|
50
70
|
const remote = input.remote || scm.detectRemote();
|
|
51
71
|
const name = input.name || scm.detectRepoName();
|
|
52
|
-
const defaultBranch = input.defaultBranch || scm.detectDefaultBranch();
|
|
53
72
|
if (!remote) throw new Error('Could not detect remote. Pass `remote` (github|gitlab|bitbucket|azure).');
|
|
54
73
|
if (!name) throw new Error('Could not detect repo name. Pass `name` (owner/repo).');
|
|
55
|
-
return { ...input, remote, name
|
|
74
|
+
return { ...input, remote, name };
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
// Capture stdout from a function that writes JSON to stdout (
|
|
77
|
+
// Capture stdout from a function that writes JSON to stdout (only `scans start-scan`, a write tool).
|
|
78
|
+
// It swaps the global stdout writer, so overlapping calls must not use it.
|
|
59
79
|
async function captureStdout(fn) {
|
|
60
80
|
const chunks = [];
|
|
61
81
|
const origWrite = process.stdout.write.bind(process.stdout);
|
|
@@ -71,22 +91,25 @@ async function captureStdout(fn) {
|
|
|
71
91
|
return chunks.join('');
|
|
72
92
|
}
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (isAlreadyLoggedIn()) {
|
|
78
|
-
process.env.CODEANT_API_TOKEN = getConfigValue('apiKeyV2');
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
94
|
+
function hasEnvToken() {
|
|
95
|
+
return !!process.env.CODEANT_API_TOKEN?.trim();
|
|
96
|
+
}
|
|
81
97
|
|
|
98
|
+
// The saved token is read from ~/.codeant/config.json on every request, so a
|
|
99
|
+
// `codeant login` in another terminal takes effect without restarting the server.
|
|
100
|
+
function warnIfUnauthenticated() {
|
|
101
|
+
if (hasEnvToken() || isAlreadyLoggedIn()) return;
|
|
82
102
|
console.error('[codeant-mcp] No API token configured. Call the codeant_login tool to sign in, or set CODEANT_API_TOKEN.');
|
|
83
103
|
}
|
|
84
104
|
|
|
85
|
-
export
|
|
86
|
-
|
|
105
|
+
export function createMcpServer() {
|
|
106
|
+
warnIfUnauthenticated();
|
|
87
107
|
|
|
88
|
-
const server = new McpServer({ name: 'codeant', version: pkg.version });
|
|
108
|
+
const server = new McpServer({ name: 'codeant', version: pkg.version }, { instructions: INSTRUCTIONS });
|
|
89
109
|
const readOnly = isReadOnly();
|
|
110
|
+
let login = null;
|
|
111
|
+
// codeant_login calls run one at a time so concurrent calls share one sign-in.
|
|
112
|
+
let loginQueue = Promise.resolve();
|
|
90
113
|
|
|
91
114
|
// ─── Scans: discovery ────────────────────────────────────────────────────
|
|
92
115
|
server.registerTool(
|
|
@@ -106,14 +129,18 @@ export async function startMcpServer() {
|
|
|
106
129
|
'codeant_scans_repos',
|
|
107
130
|
{
|
|
108
131
|
title: 'List repositories in a CodeAnt org',
|
|
109
|
-
description: 'List repositories connected to CodeAnt for a given organization. Use this to enumerate repos before fanning out org-wide queries (e.g. "secrets across all repos"). If `org` is omitted and the user has exactly one org, it is auto-picked.',
|
|
132
|
+
description: 'List repositories connected to CodeAnt for a given organization, most recently pushed first. Use this to enumerate repos before fanning out org-wide queries (e.g. "secrets across all repos"). Returns one page of slim records (full_name, default_branch, language, pushed_at, ...) with total and next_offset. If `org` is omitted and the user has exactly one org, it is auto-picked.',
|
|
110
133
|
inputSchema: {
|
|
111
134
|
org: z.string().optional().describe('Organization name. Optional when only one org is authenticated.'),
|
|
135
|
+
search: z.string().optional().describe('Case-insensitive substring match on the repository name.'),
|
|
136
|
+
limit: z.number().int().positive().max(1000).optional().describe('Max repositories returned (default 100).'),
|
|
137
|
+
offset: z.number().int().nonnegative().optional().describe('Pagination offset (default 0).'),
|
|
138
|
+
full: z.boolean().optional().describe('Return the complete provider records instead of slim ones. Default false.'),
|
|
112
139
|
},
|
|
113
140
|
annotations: READ,
|
|
114
141
|
},
|
|
115
|
-
async ({ org }) => {
|
|
116
|
-
try { return ok(await runRepos({ org })); } catch (err) { return fail(err); }
|
|
142
|
+
async ({ org, ...shape }) => {
|
|
143
|
+
try { return ok(shapeRepos(await runRepos({ org }), shape), 'Use search, or a smaller limit with offset.'); } catch (err) { return fail(err); }
|
|
117
144
|
}
|
|
118
145
|
);
|
|
119
146
|
|
|
@@ -164,41 +191,37 @@ export async function startMcpServer() {
|
|
|
164
191
|
repo: z.string().describe('Repository in owner/repo form.'),
|
|
165
192
|
scan: z.string().optional().describe('Specific commit SHA.'),
|
|
166
193
|
branch: z.string().optional().describe('Resolve the latest scan on this branch.'),
|
|
167
|
-
types: z.string().optional().describe('Comma-separated types: sast,sca,secrets,iac,dead_code,sbom,anti_patterns,docstring,complex_functions,all (default "all").'),
|
|
194
|
+
types: z.string().optional().describe('Comma-separated types: sast,sca,secrets,iac,dead_code,duplicate_code,sbom,anti_patterns,docstring,complex_functions,all (default "all").'),
|
|
168
195
|
severity: z.string().optional().describe('Comma-separated severities (e.g. "critical,high").'),
|
|
169
196
|
path: z.string().optional().describe('File path glob filter.'),
|
|
170
197
|
check: z.string().optional().describe('Filter by check ID or name (regex).'),
|
|
171
|
-
filterDismissed: z.boolean().optional().describe('Exclude dismissed findings (default false).'),
|
|
172
|
-
includeFalsePositives: z.boolean().optional().describe('Include false positives (default true).'),
|
|
198
|
+
filterDismissed: z.boolean().optional().describe('Exclude dismissed findings (default false). When false, dismissed findings carry metadata.dismissed.'),
|
|
199
|
+
includeFalsePositives: z.boolean().optional().describe('Include false positives, including user-marked ones (default true). When true, they carry metadata.false_positive.'),
|
|
173
200
|
fields: z.string().optional().describe('Project findings to a subset of fields (comma-separated).'),
|
|
174
|
-
limit: z.number().int().positive().max(500).optional().describe('Max findings per page (default
|
|
201
|
+
limit: z.number().int().positive().max(500).optional().describe('Max findings per page (default 50).'),
|
|
175
202
|
offset: z.number().int().nonnegative().optional().describe('Pagination offset (default 0).'),
|
|
176
203
|
},
|
|
177
204
|
annotations: READ,
|
|
178
205
|
},
|
|
179
206
|
async (input) => {
|
|
180
207
|
try {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
})
|
|
199
|
-
);
|
|
200
|
-
// runResults already emits JSON; pass it through unparsed to preserve shape.
|
|
201
|
-
return { content: [{ type: 'text', text: text || '{}' }] };
|
|
208
|
+
const envelope = await runResults({
|
|
209
|
+
repo: input.repo,
|
|
210
|
+
scan: input.scan,
|
|
211
|
+
branch: input.branch,
|
|
212
|
+
types: input.types ?? 'all',
|
|
213
|
+
severity: input.severity,
|
|
214
|
+
path: input.path,
|
|
215
|
+
check: input.check,
|
|
216
|
+
filterDismissed: input.filterDismissed ?? false,
|
|
217
|
+
includeFalsePositives: input.includeFalsePositives ?? true,
|
|
218
|
+
fields: input.fields,
|
|
219
|
+
limit: input.limit ?? 50,
|
|
220
|
+
offset: input.offset ?? 0,
|
|
221
|
+
failFast: false,
|
|
222
|
+
returnEnvelope: true,
|
|
223
|
+
});
|
|
224
|
+
return ok(envelope, 'Use a smaller limit with offset, filter by types, severity, path, or check, or project fields.');
|
|
202
225
|
} catch (err) {
|
|
203
226
|
return fail(err);
|
|
204
227
|
}
|
|
@@ -212,7 +235,7 @@ export async function startMcpServer() {
|
|
|
212
235
|
description: 'List dismissed alerts (false positives, accepted risk, etc.) for a repository. Useful when triaging to avoid re-surfacing already-handled findings.',
|
|
213
236
|
inputSchema: {
|
|
214
237
|
repo: z.string().describe('Repository in owner/repo form.'),
|
|
215
|
-
analysisType: z.enum(['security', 'secrets']).optional().describe('Analysis type (default "security").'),
|
|
238
|
+
analysisType: z.enum(['security', 'sast', 'secrets', 'sca', 'iac', 'antipatterns', 'anti_patterns', 'docstring', 'complex_functions', 'dead_code', 'duplicate_code']).optional().describe('Analysis type (default "security"; sast and anti_patterns are aliases).'),
|
|
216
239
|
},
|
|
217
240
|
annotations: READ,
|
|
218
241
|
},
|
|
@@ -221,6 +244,22 @@ export async function startMcpServer() {
|
|
|
221
244
|
}
|
|
222
245
|
);
|
|
223
246
|
|
|
247
|
+
server.registerTool(
|
|
248
|
+
'codeant_scans_overrides',
|
|
249
|
+
{
|
|
250
|
+
title: 'List user issue overrides',
|
|
251
|
+
description: 'List per-finding overrides users set in the CodeAnt app for a repository: Mark/Unmark false positive (security, iac), secrets confidence, and Change Severity (security, sca). Results from codeant_scans_results already apply them; use this to explain why a finding is hidden or re-rated.',
|
|
252
|
+
inputSchema: {
|
|
253
|
+
repo: z.string().describe('Repository in owner/repo form.'),
|
|
254
|
+
analysisType: z.enum(['security', 'secrets', 'iac', 'sca']).optional().describe('Analysis type (default "security").'),
|
|
255
|
+
},
|
|
256
|
+
annotations: READ,
|
|
257
|
+
},
|
|
258
|
+
async ({ repo, analysisType }) => {
|
|
259
|
+
try { return ok(await runOverrides({ repo, analysisType: analysisType ?? 'security' })); } catch (err) { return fail(err); }
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
|
|
224
263
|
// ─── Organization Hotlist findings (read-only) ──────────────────────────
|
|
225
264
|
server.registerTool(
|
|
226
265
|
'codeant_hotlist_list',
|
|
@@ -238,15 +277,17 @@ export async function startMcpServer() {
|
|
|
238
277
|
ticketStatuses: z.array(z.enum(['created', 'not_created'])).optional(),
|
|
239
278
|
compliance: z.array(z.string()).optional(),
|
|
240
279
|
validation: z.array(z.enum(['exploit_confirmed'])).optional(),
|
|
241
|
-
limit: z.number().int().positive().max(100).optional(),
|
|
242
|
-
cursor: z.string().optional(),
|
|
280
|
+
limit: z.number().int().positive().max(100).optional().describe('Page size (default 10).'),
|
|
281
|
+
cursor: z.string().optional().describe('next_cursor from a previous page.'),
|
|
243
282
|
all: z.boolean().optional().describe('Fetch every matching page. Default false.'),
|
|
244
283
|
maxWaitSeconds: z.number().int().nonnegative().max(600).optional(),
|
|
245
284
|
},
|
|
246
285
|
annotations: READ,
|
|
247
286
|
},
|
|
248
287
|
async (input) => {
|
|
249
|
-
try {
|
|
288
|
+
try {
|
|
289
|
+
return ok(await runHotlistList({ ...input, limit: input.limit ?? 10 }), 'Page with limit and cursor instead of all, or add filters such as severities or types.');
|
|
290
|
+
} catch (err) { return fail(err); }
|
|
250
291
|
}
|
|
251
292
|
);
|
|
252
293
|
|
|
@@ -279,14 +320,16 @@ export async function startMcpServer() {
|
|
|
279
320
|
service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
|
|
280
321
|
providerBaseUrl: z.string().url().optional(),
|
|
281
322
|
repos: z.array(z.string()).optional().describe('Repositories in owner/repo form. Omit to query every repository.'),
|
|
282
|
-
limit: z.union([z.literal(25), z.literal(100), z.literal(500)]).optional().describe('Page size (default
|
|
323
|
+
limit: z.union([z.literal(25), z.literal(100), z.literal(500)]).optional().describe('Page size (default 25).'),
|
|
283
324
|
offset: z.number().int().nonnegative().optional().describe('Start offset, a multiple of limit (default 0).'),
|
|
284
325
|
all: z.boolean().optional().describe('Fetch every page from offset on instead of one page.'),
|
|
285
326
|
},
|
|
286
327
|
annotations: READ,
|
|
287
328
|
},
|
|
288
329
|
async (input) => {
|
|
289
|
-
try {
|
|
330
|
+
try {
|
|
331
|
+
return ok(await runOrganizationAntipatterns({ ...input, limit: input.limit ?? 25 }), 'Page with limit and offset instead of all, or pass fewer repos.');
|
|
332
|
+
} catch (err) { return fail(err); }
|
|
290
333
|
}
|
|
291
334
|
);
|
|
292
335
|
|
|
@@ -302,11 +345,16 @@ export async function startMcpServer() {
|
|
|
302
345
|
provider: z.enum(['aws', 'azure', 'gcp', 'all']).optional().describe('Default all.'),
|
|
303
346
|
kind: z.enum(['cspm', 'vm', 'container']).optional().describe('Default cspm.'),
|
|
304
347
|
latest: z.boolean().optional().describe('Return latest scans instead of complete history. CSPM only.'),
|
|
348
|
+
limit: z.number().int().positive().max(200).optional().describe('Max scans returned per provider, newest first (default 10).'),
|
|
349
|
+
offset: z.number().int().nonnegative().optional().describe('Pagination offset within each provider (default 0).'),
|
|
350
|
+
full: z.boolean().optional().describe('Keep per-service, compliance, and region rollups on each scan. Default false.'),
|
|
305
351
|
},
|
|
306
352
|
annotations: READ,
|
|
307
353
|
},
|
|
308
|
-
async (input) => {
|
|
309
|
-
try {
|
|
354
|
+
async ({ limit, offset, full, ...input }) => {
|
|
355
|
+
try {
|
|
356
|
+
return ok(shapeCloudHistory(await runCloudHistory(input), { limit: limit ?? 10, offset: offset ?? 0, full }), 'Pass a single provider, a smaller limit, or latest: true.');
|
|
357
|
+
} catch (err) { return fail(err); }
|
|
310
358
|
}
|
|
311
359
|
);
|
|
312
360
|
|
|
@@ -314,7 +362,7 @@ export async function startMcpServer() {
|
|
|
314
362
|
'codeant_cloud_findings_list',
|
|
315
363
|
{
|
|
316
364
|
title: 'List cloud security findings',
|
|
317
|
-
description: 'Fetch findings for one AWS, Azure, or GCP CSPM, VM, or container scan.
|
|
365
|
+
description: 'Fetch findings for one AWS, Azure, or GCP CSPM, VM, or container scan. Results are paged (limit/offset/all) and carry total; pass status "FAIL" to skip passing CSPM checks. Compliance mappings are omitted unless full is true.',
|
|
318
366
|
inputSchema: {
|
|
319
367
|
org: z.string().optional(),
|
|
320
368
|
service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
|
|
@@ -332,14 +380,22 @@ export async function startMcpServer() {
|
|
|
332
380
|
subscriptionId: z.string().optional(),
|
|
333
381
|
exploitAttemptedOnly: z.boolean().optional(),
|
|
334
382
|
minDaysUnused: z.number().int().nonnegative().optional(),
|
|
335
|
-
limit: z.union([z.literal(25), z.literal(100), z.literal(500)]).optional().describe('Page size (default
|
|
383
|
+
limit: z.union([z.literal(25), z.literal(100), z.literal(500)]).optional().describe('Page size (default 25).'),
|
|
336
384
|
offset: z.number().int().nonnegative().optional().describe('Start offset, a multiple of limit (default 0).'),
|
|
337
385
|
all: z.boolean().optional().describe('Fetch every page from offset on instead of one page.'),
|
|
386
|
+
full: z.boolean().optional().describe('Keep compliance mappings on each finding. Default false.'),
|
|
338
387
|
},
|
|
339
388
|
annotations: READ,
|
|
340
389
|
},
|
|
341
|
-
async (input) => {
|
|
342
|
-
try {
|
|
390
|
+
async ({ full, ...input }) => {
|
|
391
|
+
try {
|
|
392
|
+
const limit = input.limit ?? 25;
|
|
393
|
+
const result = await runCloudFindings({ ...input, limit });
|
|
394
|
+
return ok(
|
|
395
|
+
shapeCloudFindings(result, { kind: input.kind ?? 'cspm', limit, offset: input.offset ?? 0, all: input.all, full }),
|
|
396
|
+
'Filter by status (for example "FAIL"), severity, or cloudService, or page with limit and offset instead of all.',
|
|
397
|
+
);
|
|
398
|
+
} catch (err) { return fail(err); }
|
|
343
399
|
}
|
|
344
400
|
);
|
|
345
401
|
|
|
@@ -372,16 +428,21 @@ export async function startMcpServer() {
|
|
|
372
428
|
'codeant_pentest_history',
|
|
373
429
|
{
|
|
374
430
|
title: 'List pentest engagements',
|
|
375
|
-
description: 'List
|
|
431
|
+
description: 'List pentest engagements visible in the CodeAnt Pentesting UI, newest first, including status and finding counts. Returns one page with total and next_offset.',
|
|
376
432
|
inputSchema: {
|
|
377
433
|
org: z.string().optional(),
|
|
378
434
|
service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
|
|
379
435
|
providerBaseUrl: z.string().url().optional(),
|
|
436
|
+
limit: z.number().int().positive().max(500).optional().describe('Max engagements returned (default 25).'),
|
|
437
|
+
offset: z.number().int().nonnegative().optional().describe('Pagination offset (default 0).'),
|
|
438
|
+
full: z.boolean().optional().describe('Keep credit and billing details on each engagement. Default false.'),
|
|
380
439
|
},
|
|
381
440
|
annotations: READ,
|
|
382
441
|
},
|
|
383
|
-
async (input) => {
|
|
384
|
-
try {
|
|
442
|
+
async ({ limit, offset, full, ...input }) => {
|
|
443
|
+
try {
|
|
444
|
+
return ok(shapePentestHistory(await runPentestHistory(input), { limit: limit ?? 25, offset: offset ?? 0, full }), 'Use a smaller limit with offset.');
|
|
445
|
+
} catch (err) { return fail(err); }
|
|
385
446
|
}
|
|
386
447
|
);
|
|
387
448
|
|
|
@@ -429,7 +490,7 @@ export async function startMcpServer() {
|
|
|
429
490
|
'codeant_api_get',
|
|
430
491
|
{
|
|
431
492
|
title: 'Call a CodeAnt GET API',
|
|
432
|
-
description: 'Call any authenticated GET endpoint on the configured CodeAnt API host. The path must be relative
|
|
493
|
+
description: 'Call any authenticated GET endpoint on the configured CodeAnt API host, for read APIs no dedicated tool covers. The path must be relative and start with a single "/"; absolute URLs are rejected. Returns { ok, status, tenant, data }; non-2xx responses come back with ok: false.',
|
|
433
494
|
inputSchema: {
|
|
434
495
|
path: z.string().startsWith('/'),
|
|
435
496
|
org: z.string().optional().describe('Organization name. Required when the login has multiple matching connections.'),
|
|
@@ -454,12 +515,11 @@ export async function startMcpServer() {
|
|
|
454
515
|
inputSchema: {
|
|
455
516
|
name: z.string().optional().describe('Repository in owner/repo form. Auto-detected if omitted.'),
|
|
456
517
|
remote: z.enum(['github', 'gitlab', 'bitbucket', 'azure']).optional().describe('Auto-detected if omitted.'),
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
author: z.string().optional().describe('Filter by author login (fuzzy).'),
|
|
518
|
+
sourceBranch: z.string().optional().describe('Source branch. Exact match, except substring on Bitbucket.'),
|
|
519
|
+
author: z.string().optional().describe('Author. Substring of the login on GitHub and Bitbucket, exact username on GitLab, identity ID on Azure DevOps.'),
|
|
460
520
|
state: z.enum(['open', 'closed']).optional().describe('Default "open".'),
|
|
461
|
-
limit: z.number().int().positive().max(100).optional(),
|
|
462
|
-
offset: z.number().int().nonnegative().optional(),
|
|
521
|
+
limit: z.number().int().positive().max(100).optional().describe('Max results (default 20).'),
|
|
522
|
+
offset: z.number().int().nonnegative().optional().describe('Pagination offset, rounded down to a multiple of limit on GitHub and GitLab; ignored on Bitbucket and Azure DevOps.'),
|
|
463
523
|
},
|
|
464
524
|
annotations: READ,
|
|
465
525
|
},
|
|
@@ -470,7 +530,6 @@ export async function startMcpServer() {
|
|
|
470
530
|
await scm.listPullRequests({
|
|
471
531
|
name: opts.name,
|
|
472
532
|
remote: opts.remote,
|
|
473
|
-
defaultBranch: opts.defaultBranch,
|
|
474
533
|
sourceBranch: opts.sourceBranch,
|
|
475
534
|
authorLogin: opts.author,
|
|
476
535
|
state: opts.state ?? 'open',
|
|
@@ -486,12 +545,11 @@ export async function startMcpServer() {
|
|
|
486
545
|
'codeant_pr_get',
|
|
487
546
|
{
|
|
488
547
|
title: 'Get pull request details',
|
|
489
|
-
description: 'Fetch
|
|
548
|
+
description: 'Fetch one PR/MR: provider metadata (title, state, branches, author, dates) and reviewer approval states.',
|
|
490
549
|
inputSchema: {
|
|
491
550
|
prNumber: z.number().int().positive(),
|
|
492
551
|
name: z.string().optional(),
|
|
493
552
|
remote: z.enum(['github', 'gitlab', 'bitbucket', 'azure']).optional(),
|
|
494
|
-
defaultBranch: z.string().optional(),
|
|
495
553
|
},
|
|
496
554
|
annotations: READ,
|
|
497
555
|
},
|
|
@@ -502,7 +560,6 @@ export async function startMcpServer() {
|
|
|
502
560
|
await scm.getPullRequest({
|
|
503
561
|
name: opts.name,
|
|
504
562
|
remote: opts.remote,
|
|
505
|
-
defaultBranch: opts.defaultBranch,
|
|
506
563
|
prNumber: input.prNumber,
|
|
507
564
|
})
|
|
508
565
|
);
|
|
@@ -514,14 +571,12 @@ export async function startMcpServer() {
|
|
|
514
571
|
'codeant_pr_comments',
|
|
515
572
|
{
|
|
516
573
|
title: 'List PR comments',
|
|
517
|
-
description: 'List comments on a PR/MR
|
|
574
|
+
description: 'List comments on a PR/MR, optionally only CodeAnt-authored ones or those in a date range. GitLab, Bitbucket, and Azure DevOps comments carry a resolved flag; GitHub comments do not.',
|
|
518
575
|
inputSchema: {
|
|
519
576
|
prNumber: z.number().int().positive(),
|
|
520
577
|
name: z.string().optional(),
|
|
521
578
|
remote: z.enum(['github', 'gitlab', 'bitbucket', 'azure']).optional(),
|
|
522
|
-
defaultBranch: z.string().optional(),
|
|
523
579
|
codeantGenerated: z.boolean().optional().describe('Only return comments authored by CodeAnt.'),
|
|
524
|
-
addressed: z.boolean().optional().describe('Filter by addressed/resolved status.'),
|
|
525
580
|
createdAfter: z.string().optional().describe('ISO 8601.'),
|
|
526
581
|
createdBefore: z.string().optional().describe('ISO 8601.'),
|
|
527
582
|
},
|
|
@@ -534,10 +589,8 @@ export async function startMcpServer() {
|
|
|
534
589
|
await scm.listPullRequestComments({
|
|
535
590
|
name: opts.name,
|
|
536
591
|
remote: opts.remote,
|
|
537
|
-
defaultBranch: opts.defaultBranch,
|
|
538
592
|
prNumber: input.prNumber,
|
|
539
593
|
codeantGenerated: input.codeantGenerated,
|
|
540
|
-
addressed: input.addressed,
|
|
541
594
|
createdAfter: input.createdAfter,
|
|
542
595
|
createdBefore: input.createdBefore,
|
|
543
596
|
})
|
|
@@ -550,14 +603,12 @@ export async function startMcpServer() {
|
|
|
550
603
|
'codeant_comments_search',
|
|
551
604
|
{
|
|
552
605
|
title: 'Search CodeAnt review comments',
|
|
553
|
-
description: '
|
|
606
|
+
description: 'Case-insensitive text search over review comments on the 10 most recently updated PRs/MRs of one repository (open ones only on Bitbucket and Azure DevOps). Matches comments from every author; each result has isCodeantComment, prNumber, path, and line.',
|
|
554
607
|
inputSchema: {
|
|
555
608
|
query: z.string(),
|
|
556
609
|
name: z.string().optional(),
|
|
557
610
|
remote: z.enum(['github', 'gitlab', 'bitbucket', 'azure']).optional(),
|
|
558
|
-
limit: z.number().int().positive().max(50).optional(),
|
|
559
|
-
includeAddressed: z.boolean().optional(),
|
|
560
|
-
createdAfter: z.string().optional().describe('ISO 8601.'),
|
|
611
|
+
limit: z.number().int().positive().max(50).optional().describe('Max matching comments returned (default 10).'),
|
|
561
612
|
},
|
|
562
613
|
annotations: READ,
|
|
563
614
|
},
|
|
@@ -570,8 +621,6 @@ export async function startMcpServer() {
|
|
|
570
621
|
remote: opts.remote,
|
|
571
622
|
query: input.query,
|
|
572
623
|
limit: input.limit ?? 10,
|
|
573
|
-
includeAddressed: input.includeAddressed ?? false,
|
|
574
|
-
createdAfter: input.createdAfter,
|
|
575
624
|
})
|
|
576
625
|
);
|
|
577
626
|
} catch (err) { return fail(err); }
|
|
@@ -583,7 +632,7 @@ export async function startMcpServer() {
|
|
|
583
632
|
'codeant_review_local',
|
|
584
633
|
{
|
|
585
634
|
title: 'Review local working-copy changes',
|
|
586
|
-
description: 'Run a CodeAnt AI review on local working-copy changes and return the findings as JSON. Does not modify files — pair with editor tools to apply fixes. Use this for "review my changes" / "check my staged files" prompts.',
|
|
635
|
+
description: 'Run a CodeAnt AI review on local working-copy changes and return the findings as JSON. Reviews the git repository in the server\'s working directory, so it needs a client that starts the server inside the project. Does not modify files — pair with editor tools to apply fixes. Use this for "review my changes" / "check my staged files" prompts.',
|
|
587
636
|
inputSchema: {
|
|
588
637
|
scope: z
|
|
589
638
|
.enum(['all', 'uncommitted', 'staged-only', 'committed', 'last-commit', 'last-n-commits', 'base-branch', 'base-commit'])
|
|
@@ -610,32 +659,79 @@ export async function startMcpServer() {
|
|
|
610
659
|
onProgress: () => {},
|
|
611
660
|
onFilesReady: () => {},
|
|
612
661
|
});
|
|
613
|
-
return ok(result);
|
|
662
|
+
return result?.error ? fail(result) : ok(result);
|
|
614
663
|
} catch (err) { return fail(err); }
|
|
615
664
|
}
|
|
616
665
|
);
|
|
617
666
|
|
|
618
667
|
// ─── Auth (always registered — login is needed even in read-only mode) ───
|
|
668
|
+
// Sign-in runs in the background so the login URL reaches the user right away,
|
|
669
|
+
// even when no browser can be opened (SSH, containers).
|
|
670
|
+
async function beginLogin() {
|
|
671
|
+
const controller = new AbortController();
|
|
672
|
+
const { token, loginUrl, pollUrl, browserOpened } = await startLoginFlow();
|
|
673
|
+
const attempt = { status: 'pending', loginUrl, browserOpened, controller };
|
|
674
|
+
awaitLoginCompletion({ token, pollUrl, signal: controller.signal })
|
|
675
|
+
.then(() => {
|
|
676
|
+
// A cancelled login (logout or a newer sign-in aborted this one) must not
|
|
677
|
+
// restore its token, even if the in-flight poll had already succeeded.
|
|
678
|
+
if (controller.signal.aborted) {
|
|
679
|
+
attempt.status = 'aborted';
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
attempt.status = 'success';
|
|
683
|
+
attempt.token = token;
|
|
684
|
+
// An explicit CODEANT_API_TOKEN would otherwise keep taking precedence over the new login.
|
|
685
|
+
if (hasEnvToken()) process.env.CODEANT_API_TOKEN = token;
|
|
686
|
+
})
|
|
687
|
+
.catch((err) => {
|
|
688
|
+
attempt.status = controller.signal.aborted ? 'aborted' : 'failed';
|
|
689
|
+
attempt.error = err.message;
|
|
690
|
+
});
|
|
691
|
+
return attempt;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function pendingLogin(attempt) {
|
|
695
|
+
return {
|
|
696
|
+
status: 'pending',
|
|
697
|
+
loginUrl: attempt.loginUrl,
|
|
698
|
+
browserOpened: attempt.browserOpened,
|
|
699
|
+
message: attempt.browserOpened
|
|
700
|
+
? 'A browser window opened for CodeAnt sign-in. If it did not appear, open loginUrl. After signing in, call codeant_login again to confirm; sign-in is detected within about 10 seconds.'
|
|
701
|
+
: 'Open loginUrl in a browser to sign in to CodeAnt, then call codeant_login again to confirm; sign-in is detected within about 10 seconds. The link expires in 10 minutes.',
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function handleLogin({ force }) {
|
|
706
|
+
try {
|
|
707
|
+
if (login && !force) {
|
|
708
|
+
if (login.status === 'pending') return ok(pendingLogin(login));
|
|
709
|
+
const finished = login;
|
|
710
|
+
login = null;
|
|
711
|
+
if (finished.status === 'success') return ok({ status: 'success', token: maskToken(finished.token) });
|
|
712
|
+
return fail(new Error(`${finished.error} Call codeant_login again to start a new sign-in.`));
|
|
713
|
+
}
|
|
714
|
+
if (!force && (hasEnvToken() || isAlreadyLoggedIn())) return ok({ alreadyLoggedIn: true });
|
|
715
|
+
login?.controller.abort();
|
|
716
|
+
login = await beginLogin();
|
|
717
|
+
return ok(pendingLogin(login));
|
|
718
|
+
} catch (err) { return fail(err); }
|
|
719
|
+
}
|
|
720
|
+
|
|
619
721
|
server.registerTool(
|
|
620
722
|
'codeant_login',
|
|
621
723
|
{
|
|
622
724
|
title: 'Sign in to CodeAnt AI',
|
|
623
|
-
description: '
|
|
725
|
+
description: 'Start browser sign-in to CodeAnt AI. Returns immediately with { status: "pending", loginUrl, browserOpened }: show loginUrl to the user (the browser may not open, for example over SSH), then call codeant_login again after they finish to get { status: "success" }. The link expires after 10 minutes. The token is saved to ~/.codeant/config.json (apiKeyV2), which the CodeAnt CLI shares, and later calls use it without a restart. Returns { alreadyLoggedIn: true } if a token is already configured; pass force: true when tools reject that token.',
|
|
624
726
|
inputSchema: {
|
|
625
|
-
force: z.boolean().optional().describe('
|
|
727
|
+
force: z.boolean().optional().describe('Start a new sign-in even if a token is already configured, for example when tools reject it as invalid. Default false.'),
|
|
626
728
|
},
|
|
627
729
|
annotations: { ...WRITE_NON_DESTRUCTIVE, idempotentHint: true },
|
|
628
730
|
},
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return ok({ alreadyLoggedIn: true });
|
|
634
|
-
}
|
|
635
|
-
const { token, loginUrl } = await runLoginFlow();
|
|
636
|
-
const masked = token ? `${token.slice(0, 8)}…` : null;
|
|
637
|
-
return ok({ status: 'success', loginUrl, token: masked });
|
|
638
|
-
} catch (err) { return fail(err); }
|
|
731
|
+
({ force }) => {
|
|
732
|
+
const run = loginQueue.then(() => handleLogin({ force }));
|
|
733
|
+
loginQueue = run.catch(() => {});
|
|
734
|
+
return run;
|
|
639
735
|
}
|
|
640
736
|
);
|
|
641
737
|
|
|
@@ -643,12 +739,14 @@ export async function startMcpServer() {
|
|
|
643
739
|
'codeant_logout',
|
|
644
740
|
{
|
|
645
741
|
title: 'Sign out of CodeAnt AI',
|
|
646
|
-
description: '
|
|
742
|
+
description: 'Revoke the API token on the server, clear it from ~/.codeant/config.json (which also signs out the CodeAnt CLI), unset CODEANT_API_TOKEN on the running MCP process, and cancel a pending codeant_login. Returns { wasLoggedIn: false } immediately if no token was configured.',
|
|
647
743
|
inputSchema: {},
|
|
648
744
|
annotations: { ...WRITE_NON_DESTRUCTIVE, idempotentHint: true },
|
|
649
745
|
},
|
|
650
746
|
async () => {
|
|
651
747
|
try {
|
|
748
|
+
login?.controller.abort();
|
|
749
|
+
login = null;
|
|
652
750
|
const result = await logoutCodeAnt();
|
|
653
751
|
return ok({
|
|
654
752
|
...result,
|
|
@@ -699,7 +797,7 @@ export async function startMcpServer() {
|
|
|
699
797
|
async (input) => {
|
|
700
798
|
try {
|
|
701
799
|
const text = await captureStdout(() => runStartScan(input));
|
|
702
|
-
return
|
|
800
|
+
return textResult(text || '{}');
|
|
703
801
|
} catch (err) { return fail(err); }
|
|
704
802
|
}
|
|
705
803
|
);
|
|
@@ -737,6 +835,11 @@ export async function startMcpServer() {
|
|
|
737
835
|
);
|
|
738
836
|
}
|
|
739
837
|
|
|
838
|
+
return server;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export async function startMcpServer() {
|
|
842
|
+
const server = createMcpServer();
|
|
740
843
|
const transport = new StdioServerTransport();
|
|
741
844
|
await server.connect(transport);
|
|
742
845
|
}
|