@securityreviewai/security-review-mcp 0.2.9

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.
@@ -0,0 +1,104 @@
1
+ import * as z from "zod/v4";
2
+ import { getApiClient } from "../api/client.js";
3
+ import { runTool } from "./common.js";
4
+ export function registerProjectTools(server) {
5
+ server.registerTool("list_projects", {
6
+ description: "List all projects in the SRAI platform. Returns project IDs, names, and descriptions. Use this to discover available projects before operating on them.",
7
+ }, async () => runTool(async () => getApiClient().listProjects()));
8
+ server.registerTool("get_project", {
9
+ description: "Get detailed information about a specific SRAI project by its ID. Returns project name, description, settings, and metadata.",
10
+ inputSchema: {
11
+ project_id: z.number().int(),
12
+ },
13
+ }, async ({ project_id }) => runTool(async () => getApiClient().getProject(project_id)));
14
+ server.registerTool("create_project", {
15
+ description: "Create a new project in SRAI. Provide a name and description. Returns the created project with its ID. Use this before uploading documents or creating reviews.",
16
+ inputSchema: {
17
+ name: z.string(),
18
+ description: z.string(),
19
+ },
20
+ }, async ({ name, description }) => runTool(async () => getApiClient().createProject(name, description)));
21
+ server.registerTool("get_project_settings", {
22
+ description: "Get settings and configuration for a specific SRAI project. Includes integration settings, compliance frameworks, etc.",
23
+ inputSchema: {
24
+ project_id: z.number().int(),
25
+ },
26
+ }, async ({ project_id }) => runTool(async () => getApiClient().getProjectSettings(project_id)));
27
+ server.registerTool("get_project_profile", {
28
+ description: "Get the project profile hub summary for a project, including profile and high-level counts.",
29
+ inputSchema: {
30
+ project_id: z.number().int(),
31
+ },
32
+ }, async ({ project_id }) => runTool(async () => getApiClient().getProjectProfile(project_id)));
33
+ server.registerTool("get_full_project_profile", {
34
+ description: "Get the full project profile graph payload for a project, including all profile sub-objects.",
35
+ inputSchema: {
36
+ project_id: z.number().int(),
37
+ },
38
+ }, async ({ project_id }) => runTool(async () => getApiClient().getFullProjectProfile(project_id)));
39
+ server.registerTool("get_project_profile_description", {
40
+ description: "Get the project profile description node (description and purpose) for a project.",
41
+ inputSchema: {
42
+ project_id: z.number().int(),
43
+ },
44
+ }, async ({ project_id }) => runTool(async () => getApiClient().getProjectProfileDescription(project_id)));
45
+ server.registerTool("list_profile_technology_categories", {
46
+ description: "List all project profile technology categories for a project.",
47
+ inputSchema: {
48
+ project_id: z.number().int(),
49
+ },
50
+ }, async ({ project_id }) => runTool(async () => getApiClient().listProjectProfileTechnologyCategories(project_id)));
51
+ server.registerTool("get_project_profile_technology_category", {
52
+ description: "Get a single project profile technology category by category name.",
53
+ inputSchema: {
54
+ project_id: z.number().int(),
55
+ category_name: z.string(),
56
+ },
57
+ }, async ({ project_id, category_name }) => runTool(async () => getApiClient().getProjectProfileTechnologyCategory(project_id, category_name)));
58
+ server.registerTool("list_project_profile_architecture_notes", {
59
+ description: "List project profile architecture notes for a project.",
60
+ inputSchema: {
61
+ project_id: z.number().int(),
62
+ },
63
+ }, async ({ project_id }) => runTool(async () => getApiClient().listProjectProfileArchitectureNotes(project_id)));
64
+ server.registerTool("list_project_profile_user_groups", {
65
+ description: "List project profile user groups, optionally filtered by group_type.",
66
+ inputSchema: {
67
+ project_id: z.number().int(),
68
+ group_type: z.string().optional(),
69
+ },
70
+ }, async ({ project_id, group_type }) => runTool(async () => getApiClient().listProjectProfileUserGroups(project_id, group_type)));
71
+ server.registerTool("list_project_profile_language_stacks", {
72
+ description: "List project profile language stacks for a project.",
73
+ inputSchema: {
74
+ project_id: z.number().int(),
75
+ },
76
+ }, async ({ project_id }) => runTool(async () => getApiClient().listProjectProfileLanguageStacks(project_id)));
77
+ server.registerTool("list_project_profile_security_controls", {
78
+ description: "List project profile security controls, optionally filtered by status.",
79
+ inputSchema: {
80
+ project_id: z.number().int(),
81
+ status: z.string().optional(),
82
+ },
83
+ }, async ({ project_id, status }) => runTool(async () => getApiClient().listProjectProfileSecurityControls(project_id, status)));
84
+ server.registerTool("get_project_profile_security_control", {
85
+ description: "Get a single project profile security control by control ID.",
86
+ inputSchema: {
87
+ project_id: z.number().int(),
88
+ control_id: z.number().int(),
89
+ },
90
+ }, async ({ project_id, control_id }) => runTool(async () => getApiClient().getProjectProfileSecurityControl(project_id, control_id)));
91
+ server.registerTool("list_profile_compliance_requirements", {
92
+ description: "List project profile compliance requirements for a project.",
93
+ inputSchema: {
94
+ project_id: z.number().int(),
95
+ },
96
+ }, async ({ project_id }) => runTool(async () => getApiClient().listProjectProfileComplianceRequirements(project_id)));
97
+ server.registerTool("get_profile_compliance_requirement", {
98
+ description: "Get a single project profile compliance requirement by requirement ID.",
99
+ inputSchema: {
100
+ project_id: z.number().int(),
101
+ requirement_id: z.number().int(),
102
+ },
103
+ }, async ({ project_id, requirement_id }) => runTool(async () => getApiClient().getProjectProfileComplianceRequirement(project_id, requirement_id)));
104
+ }
@@ -0,0 +1,61 @@
1
+ import * as z from "zod/v4";
2
+ import { getApiClient } from "../api/client.js";
3
+ import { runTool } from "./common.js";
4
+ export function registerReviewArtifactTools(server) {
5
+ server.registerTool("get_security_objectives", {
6
+ description: "Get all security objectives from a completed review. Returns objectives categorized as Regulatory, Internal, or Contractual requirements, with names, descriptions, and compliance framework mappings.",
7
+ inputSchema: {
8
+ project_id: z.number().int(),
9
+ review_id: z.number().int(),
10
+ },
11
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getSecurityObjectives(project_id, review_id)));
12
+ server.registerTool("get_threat_scenarios", {
13
+ description: "Get all threat scenarios from a completed review. Returns threats with names, descriptions, severity (Critical/High/Medium/Low), CVSS scores, CWE IDs, PWN-ISMS categories (Product, Workload, Network, IAM, Secrets, Logging, Supply Chain), and STRIDE classifications.",
14
+ inputSchema: {
15
+ project_id: z.number().int(),
16
+ review_id: z.number().int(),
17
+ },
18
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getThreatScenarios(project_id, review_id)));
19
+ server.registerTool("get_countermeasures", {
20
+ description: "Get all countermeasures/mitigations from a completed review. Returns mitigations with names, descriptions, implementation steps, status (Pending/Implemented/Partially Implemented), mapped threat IDs, and compliance framework references.",
21
+ inputSchema: {
22
+ project_id: z.number().int(),
23
+ review_id: z.number().int(),
24
+ },
25
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getCountermeasures(project_id, review_id)));
26
+ server.registerTool("get_components", {
27
+ description: "Get all system components/entities identified in a review. Returns component names, descriptions, purposes, connections, and security objective mappings.",
28
+ inputSchema: {
29
+ project_id: z.number().int(),
30
+ review_id: z.number().int(),
31
+ },
32
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getComponents(project_id, review_id)));
33
+ server.registerTool("get_data_dictionaries", {
34
+ description: "Get the data dictionary from a completed review. Returns identified sensitive data assets (PII, financial, health data) and stepping stones (API keys, credentials, tokens) with descriptions and reasoning.",
35
+ inputSchema: {
36
+ project_id: z.number().int(),
37
+ review_id: z.number().int(),
38
+ },
39
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getDataDictionaries(project_id, review_id)));
40
+ server.registerTool("get_questions", {
41
+ description: "Get security questions generated for a review. These are contextual questions about the system that help refine the security analysis.",
42
+ inputSchema: {
43
+ project_id: z.number().int(),
44
+ review_id: z.number().int(),
45
+ },
46
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getQuestions(project_id, review_id)));
47
+ server.registerTool("get_findings", {
48
+ description: "Get findings from a completed review. Findings are aggregated security insights derived from the analysis.",
49
+ inputSchema: {
50
+ project_id: z.number().int(),
51
+ review_id: z.number().int(),
52
+ },
53
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getFindings(project_id, review_id)));
54
+ server.registerTool("get_security_test_cases", {
55
+ description: "Get security test cases generated for a review. These are actionable test cases derived from the threat scenarios and countermeasures.",
56
+ inputSchema: {
57
+ project_id: z.number().int(),
58
+ review_id: z.number().int(),
59
+ },
60
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getSecurityTestCases(project_id, review_id)));
61
+ }
@@ -0,0 +1,405 @@
1
+ import * as z from "zod/v4";
2
+ import { getApiClient, SraiApiError } from "../api/client.js";
3
+ import { errorToolResult, textToolResult } from "../utils/serialization.js";
4
+ import { runTool } from "./common.js";
5
+ export function registerReviewTools(server) {
6
+ server.registerTool("find_project_by_name", {
7
+ description: "Find SRAI projects by exact, partial, or fuzzy name match. Use this when the user provides a project name instead of project_id.",
8
+ inputSchema: {
9
+ project_name: z.string(),
10
+ max_results: z.number().int().default(5),
11
+ },
12
+ }, async ({ project_name, max_results }) => runTool(async () => {
13
+ const projectsPayload = await getApiClient().listProjects();
14
+ const projects = extractItems(projectsPayload, ["projects", "items", "data"]);
15
+ const matches = rankByName(projects, project_name).slice(0, Math.max(1, max_results));
16
+ return {
17
+ query: project_name,
18
+ total_projects: projects.length,
19
+ match_count: matches.length,
20
+ matches: matches.map((item) => ({
21
+ id: item.id,
22
+ name: item.name,
23
+ description: item.description,
24
+ match_score: item._match_score,
25
+ })),
26
+ };
27
+ }));
28
+ server.registerTool("list_reviews", {
29
+ description: "List all security reviews in an SRAI project. Returns review IDs, names, descriptions, status, and review type. Use this to find reviews to inspect or to check workflow progress.",
30
+ inputSchema: {
31
+ project_id: z.number().int(),
32
+ },
33
+ }, async ({ project_id }) => runTool(async () => getApiClient().listReviews(project_id)));
34
+ server.registerTool("list_reviews_by_project_name", {
35
+ description: "List reviews for a project identified by name (supports fuzzy matches). This resolves the project first, then returns reviews only for that project.",
36
+ inputSchema: {
37
+ project_name: z.string(),
38
+ review_name: z.string().optional(),
39
+ max_results: z.number().int().default(10),
40
+ },
41
+ }, async ({ project_name, review_name, max_results }) => runTool(async () => {
42
+ const client = getApiClient();
43
+ const projectsPayload = await client.listProjects();
44
+ const projects = extractItems(projectsPayload, ["projects", "items", "data"]);
45
+ const rankedProjects = rankByName(projects, project_name);
46
+ if (rankedProjects.length === 0) {
47
+ return {
48
+ error: `No project found matching '${project_name}'`,
49
+ query: project_name,
50
+ };
51
+ }
52
+ const selectedProject = rankedProjects[0];
53
+ const projectId = intOrZero(selectedProject.id);
54
+ const reviewsPayload = await client.listReviews(projectId);
55
+ let reviews = extractItems(reviewsPayload, ["reviews", "items", "data"]);
56
+ if (review_name) {
57
+ reviews = rankByName(reviews, review_name);
58
+ }
59
+ reviews = reviews.slice(0, Math.max(1, max_results));
60
+ return {
61
+ project: {
62
+ id: selectedProject.id,
63
+ name: selectedProject.name,
64
+ match_score: selectedProject._match_score,
65
+ },
66
+ review_query: review_name,
67
+ review_count: reviews.length,
68
+ reviews: reviews.map((review) => ({
69
+ id: review.id,
70
+ name: review.name,
71
+ description: review.description,
72
+ status: review.status,
73
+ review_type: review.review_type,
74
+ updated_at: review.updated_at,
75
+ match_score: review._match_score,
76
+ })),
77
+ };
78
+ }));
79
+ server.registerTool("get_review", {
80
+ description: "Get detailed information about a specific security review. Returns review configuration, status, associated documents, compliance frameworks, and workflow progress.",
81
+ inputSchema: {
82
+ project_id: z.number().int(),
83
+ review_id: z.number().int(),
84
+ },
85
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getReview(project_id, review_id)));
86
+ server.registerTool("get_review_overview", {
87
+ description: "Get a focused overview of a review based on user intent. Accepts project by ID or fuzzy name and review by ID or fuzzy name. If review is not provided, selects the latest review inside the resolved project.",
88
+ inputSchema: {
89
+ user_request: z.string(),
90
+ project_id: z.number().int().optional(),
91
+ project_name: z.string().optional(),
92
+ review_id: z.number().int().optional(),
93
+ review_name: z.string().optional(),
94
+ },
95
+ }, async ({ user_request, project_id, project_name, review_id, review_name }) => {
96
+ try {
97
+ const client = getApiClient();
98
+ if (project_id === undefined && !project_name) {
99
+ return errorToolResult("Provide either project_id or project_name.");
100
+ }
101
+ let selectedProject;
102
+ let resolvedProjectId = project_id;
103
+ let projectResolutionMode = "id";
104
+ if (resolvedProjectId !== undefined) {
105
+ const projectPayload = await client.getProject(resolvedProjectId);
106
+ selectedProject = asItem(projectPayload);
107
+ }
108
+ else {
109
+ projectResolutionMode = "name";
110
+ const projectsPayload = await client.listProjects();
111
+ const projects = extractItems(projectsPayload, ["projects", "items", "data"]);
112
+ const rankedProjects = rankByName(projects, String(project_name));
113
+ if (rankedProjects.length === 0) {
114
+ return errorToolResult(`No project found matching '${project_name}'`);
115
+ }
116
+ selectedProject = rankedProjects[0];
117
+ resolvedProjectId = intOrZero(selectedProject?.id);
118
+ }
119
+ const reviewsPayload = await client.listReviews(resolvedProjectId);
120
+ const projectReviews = extractItems(reviewsPayload, ["reviews", "items", "data"]);
121
+ if (projectReviews.length === 0) {
122
+ return errorToolResult("No reviews found in this project.");
123
+ }
124
+ let selectedReview;
125
+ let reviewResolutionMode = "latest_in_project";
126
+ if (review_id !== undefined) {
127
+ selectedReview = projectReviews.find((review) => intOrZero(review.id) === review_id);
128
+ reviewResolutionMode = "id";
129
+ if (!selectedReview) {
130
+ return errorToolResult(`Review ${review_id} is not in project ${resolvedProjectId}.`);
131
+ }
132
+ }
133
+ else if (review_name) {
134
+ const rankedReviews = rankByName(projectReviews, review_name);
135
+ selectedReview = rankedReviews[0];
136
+ reviewResolutionMode = "name";
137
+ if (!selectedReview) {
138
+ return errorToolResult(`No review in project ${resolvedProjectId} matched '${review_name}'.`);
139
+ }
140
+ }
141
+ else {
142
+ selectedReview = pickLatestReview(projectReviews);
143
+ }
144
+ if (!selectedReview) {
145
+ return errorToolResult("Unable to resolve review for overview.");
146
+ }
147
+ const selectedReviewId = intOrZero(selectedReview.id);
148
+ const focus = focusFlags(user_request);
149
+ const reviewDetails = await client.getReview(resolvedProjectId, selectedReviewId);
150
+ const sections = {};
151
+ const counts = {};
152
+ if (focus.objectives) {
153
+ const objectives = await client.getSecurityObjectives(resolvedProjectId, selectedReviewId);
154
+ sections.security_objectives = objectives;
155
+ counts.security_objectives = artifactCount(objectives);
156
+ }
157
+ if (focus.components) {
158
+ const components = await client.getComponents(resolvedProjectId, selectedReviewId);
159
+ sections.components = components;
160
+ counts.components = artifactCount(components);
161
+ }
162
+ if (focus.threats) {
163
+ const threats = await client.getThreatScenarios(resolvedProjectId, selectedReviewId);
164
+ sections.threat_scenarios = threats;
165
+ counts.threat_scenarios = artifactCount(threats);
166
+ }
167
+ if (focus.countermeasures) {
168
+ const countermeasures = await client.getCountermeasures(resolvedProjectId, selectedReviewId);
169
+ sections.countermeasures = countermeasures;
170
+ counts.countermeasures = artifactCount(countermeasures);
171
+ }
172
+ return textToolResult({
173
+ request: user_request,
174
+ resolution: {
175
+ project_mode: projectResolutionMode,
176
+ review_mode: reviewResolutionMode,
177
+ },
178
+ project: {
179
+ id: resolvedProjectId,
180
+ name: selectedProject?.name,
181
+ },
182
+ review: {
183
+ id: selectedReviewId,
184
+ name: selectedReview.name,
185
+ status: selectedReview.status,
186
+ },
187
+ focus: Object.fromEntries(Object.entries(focus).filter(([, enabled]) => enabled)),
188
+ overview: {
189
+ review_details: reviewDetails,
190
+ artifact_counts: counts,
191
+ artifacts: sections,
192
+ },
193
+ });
194
+ }
195
+ catch (error) {
196
+ if (error instanceof SraiApiError) {
197
+ return errorToolResult(error.detail, error.statusCode);
198
+ }
199
+ return errorToolResult(error instanceof Error ? error.message : String(error));
200
+ }
201
+ });
202
+ server.registerTool("create_review", {
203
+ description: "Create a new security review in an SRAI project. This sets up a review with associated documents and configuration.\n\nRequired parameters:\n- project_id: The project to create the review in\n- name: Review name\n- description: Review description\n- document_ids: List of document IDs to include in the review\n\nOptional parameters:\n- review_type: 'system' (default) or 'incremental'\n- generate_questions: Whether to generate security questions (default: false)\n- frameworks: List of compliance framework names to include (e.g. 'PCI-DSS', 'HIPAA', 'GDPR')\n- component_diagram_ids: List of component diagram document IDs\n- diagram_ids: List of diagram document IDs\n\nAfter creating a review, use start_workflow to begin the security analysis.",
204
+ inputSchema: {
205
+ project_id: z.number().int(),
206
+ name: z.string(),
207
+ description: z.string(),
208
+ document_ids: z.array(z.number().int()),
209
+ review_type: z.string().default("system"),
210
+ generate_questions: z.boolean().default(false),
211
+ frameworks: z.array(z.string()).optional(),
212
+ component_diagram_ids: z.array(z.number().int()).optional(),
213
+ diagram_ids: z.array(z.number().int()).optional(),
214
+ },
215
+ }, async ({ project_id, name, description, document_ids, review_type, generate_questions, frameworks, component_diagram_ids, diagram_ids, }) => runTool(async () => {
216
+ const reviewConfig = {
217
+ step1: {
218
+ name,
219
+ description,
220
+ review_type,
221
+ generate_questions,
222
+ },
223
+ step2: {
224
+ document_ids,
225
+ },
226
+ step3: {
227
+ component_diagram_ids: component_diagram_ids || [],
228
+ diagram_ids: diagram_ids || [],
229
+ is_finalized: true,
230
+ },
231
+ step4: {
232
+ framework: frameworks || [],
233
+ security_policy: [],
234
+ },
235
+ step5: {
236
+ existing_review: null,
237
+ end_date: null,
238
+ exclude_security_objectives: [],
239
+ exclude_threat_scenarios: [],
240
+ exclude_countermeasures: [],
241
+ include_security_objectives: [],
242
+ include_threat_scenarios: [],
243
+ include_countermeasures: [],
244
+ },
245
+ };
246
+ return getApiClient().createReview(project_id, reviewConfig);
247
+ }));
248
+ server.registerTool("get_review_resource_documents", {
249
+ description: "Get the list of documents available as resources for creating a review in a project. Use this to find which document IDs to pass when creating a review.",
250
+ inputSchema: {
251
+ project_id: z.number().int(),
252
+ },
253
+ }, async ({ project_id }) => runTool(async () => getApiClient().getReviewResourceDocuments(project_id)));
254
+ server.registerTool("get_review_resource_compliance", {
255
+ description: "Get the list of compliance frameworks available for a review in a project. Returns framework names that can be passed when creating a review.",
256
+ inputSchema: {
257
+ project_id: z.number().int(),
258
+ },
259
+ }, async ({ project_id }) => runTool(async () => getApiClient().getReviewResourceCompliance(project_id)));
260
+ }
261
+ function extractItems(payload, keys) {
262
+ if (Array.isArray(payload)) {
263
+ return payload.filter(isItem);
264
+ }
265
+ if (isItem(payload)) {
266
+ for (const key of keys) {
267
+ const value = payload[key];
268
+ if (Array.isArray(value)) {
269
+ return value.filter(isItem);
270
+ }
271
+ }
272
+ }
273
+ return [];
274
+ }
275
+ function normalize(value) {
276
+ if (value === undefined || value === null) {
277
+ return "";
278
+ }
279
+ return String(value).trim().toLowerCase();
280
+ }
281
+ function similarityScore(query, candidate) {
282
+ if (!query || !candidate) {
283
+ return 0;
284
+ }
285
+ if (query === candidate) {
286
+ return 1;
287
+ }
288
+ if (candidate.startsWith(query)) {
289
+ return 0.95;
290
+ }
291
+ if (candidate.includes(query)) {
292
+ return 0.9;
293
+ }
294
+ return diceCoefficient(query, candidate);
295
+ }
296
+ function rankByName(items, query) {
297
+ const normalizedQuery = normalize(query);
298
+ const queryTokens = normalizedQuery.split(/\s+/u).filter((token) => token.length >= 3);
299
+ const scored = [];
300
+ for (const item of items) {
301
+ const name = normalize(item.name);
302
+ if (!name) {
303
+ continue;
304
+ }
305
+ const score = similarityScore(normalizedQuery, name);
306
+ const hasTokenOverlap = queryTokens.some((token) => name.includes(token));
307
+ if (score >= 0.6 || hasTokenOverlap) {
308
+ scored.push({
309
+ ...item,
310
+ _match_score: Number(score.toFixed(3)),
311
+ });
312
+ }
313
+ }
314
+ scored.sort((a, b) => Number(b._match_score ?? 0) - Number(a._match_score ?? 0));
315
+ return scored;
316
+ }
317
+ function pickLatestReview(reviews) {
318
+ return reviews
319
+ .slice()
320
+ .sort((a, b) => {
321
+ const aUpdated = String(a.updated_at ?? "");
322
+ const bUpdated = String(b.updated_at ?? "");
323
+ if (aUpdated !== bUpdated) {
324
+ return bUpdated.localeCompare(aUpdated);
325
+ }
326
+ const aCreated = String(a.created_at ?? "");
327
+ const bCreated = String(b.created_at ?? "");
328
+ if (aCreated !== bCreated) {
329
+ return bCreated.localeCompare(aCreated);
330
+ }
331
+ return intOrZero(b.id) - intOrZero(a.id);
332
+ })[0];
333
+ }
334
+ function artifactCount(payload) {
335
+ if (Array.isArray(payload)) {
336
+ return payload.length;
337
+ }
338
+ if (isItem(payload)) {
339
+ for (const key of ["items", "data", "results", "findings", "questions"]) {
340
+ const value = payload[key];
341
+ if (Array.isArray(value)) {
342
+ return value.length;
343
+ }
344
+ }
345
+ }
346
+ return 0;
347
+ }
348
+ function focusFlags(question) {
349
+ const text = normalize(question);
350
+ if (!text) {
351
+ return {
352
+ objectives: true,
353
+ components: true,
354
+ threats: true,
355
+ countermeasures: true,
356
+ };
357
+ }
358
+ const mapping = {
359
+ objectives: ["objective", "objectives", "requirement", "requirements", "compliance"],
360
+ components: ["component", "components", "entity", "entities", "architecture"],
361
+ threats: ["threat", "threats", "risk", "risks"],
362
+ countermeasures: ["countermeasure", "countermeasures", "mitigation", "mitigations", "control", "controls"],
363
+ };
364
+ const flags = Object.fromEntries(Object.entries(mapping).map(([key, terms]) => [key, terms.some((term) => text.includes(term))]));
365
+ if (!Object.values(flags).some(Boolean) ||
366
+ ["overview", "summary", "analy", "review"].some((term) => text.includes(term))) {
367
+ return {
368
+ objectives: true,
369
+ components: true,
370
+ threats: true,
371
+ countermeasures: true,
372
+ };
373
+ }
374
+ return flags;
375
+ }
376
+ function diceCoefficient(a, b) {
377
+ if (a.length < 2 || b.length < 2) {
378
+ return a === b ? 1 : 0;
379
+ }
380
+ const aBigrams = new Map();
381
+ for (let i = 0; i < a.length - 1; i += 1) {
382
+ const gram = a.slice(i, i + 2);
383
+ aBigrams.set(gram, (aBigrams.get(gram) ?? 0) + 1);
384
+ }
385
+ let intersection = 0;
386
+ for (let i = 0; i < b.length - 1; i += 1) {
387
+ const gram = b.slice(i, i + 2);
388
+ const count = aBigrams.get(gram) ?? 0;
389
+ if (count > 0) {
390
+ aBigrams.set(gram, count - 1);
391
+ intersection += 1;
392
+ }
393
+ }
394
+ return (2 * intersection) / (a.length - 1 + (b.length - 1));
395
+ }
396
+ function intOrZero(value) {
397
+ const parsed = Number(value);
398
+ return Number.isFinite(parsed) ? parsed : 0;
399
+ }
400
+ function isItem(value) {
401
+ return typeof value === "object" && value !== null;
402
+ }
403
+ function asItem(value) {
404
+ return isItem(value) ? value : {};
405
+ }
@@ -0,0 +1,105 @@
1
+ import * as z from "zod/v4";
2
+ import { getApiClient } from "../api/client.js";
3
+ import { runTool } from "./common.js";
4
+ export function registerWorkflowTools(server) {
5
+ server.registerTool("list_ai_ide_workflows", {
6
+ description: "List AI IDE workflows for a project with pagination. Returns workflow records and pagination metadata.",
7
+ inputSchema: {
8
+ project_id: z.number().int(),
9
+ page: z.number().int().min(1).default(1),
10
+ page_size: z.number().int().min(1).max(100).default(10),
11
+ },
12
+ }, async ({ project_id, page, page_size }) => runTool(async () => getApiClient().listAiIdeWorkflows(project_id, page, page_size)));
13
+ server.registerTool("get_ai_ide_workflow", {
14
+ description: "Get a specific AI IDE workflow by workflow ID within a project. Returns workflow metadata and linked AI IDE data source.",
15
+ inputSchema: {
16
+ project_id: z.number().int(),
17
+ workflow_id: z.number().int(),
18
+ },
19
+ }, async ({ project_id, workflow_id }) => runTool(async () => getApiClient().getAiIdeWorkflow(project_id, workflow_id)));
20
+ server.registerTool("create_ai_ide_workflow", {
21
+ description: "Create an AI IDE workflow for a project. This creates a workflow and its AI IDE data source in SRAI.",
22
+ inputSchema: {
23
+ project_id: z.number().int(),
24
+ name: z.string(),
25
+ description: z.string().optional(),
26
+ },
27
+ }, async ({ project_id, name, description }) => runTool(async () => getApiClient().createAiIdeWorkflow(project_id, name, description)));
28
+ server.registerTool("create_ai_ide_event", {
29
+ description: "Create an AI IDE event under an existing AI IDE workflow. Include summary, developer details, mitigated threats (each threat must include severity: critical/high/medium/low), best practices, secure snippets, and optional event metadata.",
30
+ inputSchema: {
31
+ project_id: z.number().int(),
32
+ workflow_id: z.number().int(),
33
+ external_id: z.string(),
34
+ title: z.string(),
35
+ summary: z.string(),
36
+ developer_name: z.string(),
37
+ developer_email: z.string(),
38
+ threats_mitigated: z
39
+ .array(z
40
+ .object({
41
+ severity: z.enum(["critical", "high", "medium", "low"]),
42
+ })
43
+ .catchall(z.unknown()))
44
+ .default([]),
45
+ best_practices_achieved: z.array(z.object({}).catchall(z.unknown())).default([]),
46
+ secure_code_snippets: z.array(z.object({}).catchall(z.unknown())).default([]),
47
+ event_metadata: z.record(z.string(), z.unknown()).default({}),
48
+ },
49
+ }, async ({ project_id, workflow_id, external_id, title, summary, developer_name, developer_email, threats_mitigated, best_practices_achieved, secure_code_snippets, event_metadata, }) => runTool(async () => getApiClient().createAiIdeEvent(project_id, workflow_id, {
50
+ external_id,
51
+ title,
52
+ summary,
53
+ developer_name,
54
+ developer_email,
55
+ threats_mitigated,
56
+ best_practices_achieved,
57
+ secure_code_snippets,
58
+ event_metadata,
59
+ })));
60
+ server.registerTool("start_workflow", {
61
+ description: "Start the security review workflow for a review. This triggers the SRAI AI agents to begin generating security objectives, components, data dictionaries, threat scenarios, and countermeasures. The workflow runs asynchronously. Use get_workflow_status to monitor progress.",
62
+ inputSchema: {
63
+ project_id: z.number().int(),
64
+ review_id: z.number().int(),
65
+ },
66
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().startWorkflow(project_id, review_id)));
67
+ server.registerTool("get_workflow_status", {
68
+ description: "Get the current status and progress of a review workflow. Returns the list of workflow jobs with their status (pending, in_progress, completed, failed) and any pending job IDs.",
69
+ inputSchema: {
70
+ project_id: z.number().int(),
71
+ review_id: z.number().int(),
72
+ },
73
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getWorkflowStatus(project_id, review_id)));
74
+ server.registerTool("get_workflow_job_status", {
75
+ description: "Get the status of a specific workflow job. Returns detailed job information including status, logs, and timestamps.",
76
+ inputSchema: {
77
+ project_id: z.number().int(),
78
+ review_id: z.number().int(),
79
+ job_id: z.number().int(),
80
+ },
81
+ }, async ({ project_id, review_id, job_id }) => runTool(async () => getApiClient().getWorkflowJobStatus(project_id, review_id, job_id)));
82
+ server.registerTool("start_next_workflow_job", {
83
+ description: "Advance the workflow by starting the next pending job. Use this to manually progress through workflow steps after a job completes.",
84
+ inputSchema: {
85
+ project_id: z.number().int(),
86
+ review_id: z.number().int(),
87
+ },
88
+ }, async ({ project_id, review_id }) => runTool(async () => getApiClient().getNextWorkflowJob(project_id, review_id)));
89
+ server.registerTool("start_workflow_job", {
90
+ description: "Start a specific workflow job by ID. Use this to manually trigger a particular step in the workflow.",
91
+ inputSchema: {
92
+ project_id: z.number().int(),
93
+ review_id: z.number().int(),
94
+ job_id: z.number().int(),
95
+ },
96
+ }, async ({ project_id, review_id, job_id }) => runTool(async () => getApiClient().startWorkflowJob(project_id, review_id, job_id)));
97
+ server.registerTool("retry_workflow_job", {
98
+ description: "Retry a failed workflow job. Use this when a workflow step has failed and you want to re-attempt it.",
99
+ inputSchema: {
100
+ project_id: z.number().int(),
101
+ review_id: z.number().int(),
102
+ job_id: z.number().int(),
103
+ },
104
+ }, async ({ project_id, review_id, job_id }) => runTool(async () => getApiClient().retryWorkflowJob(project_id, review_id, job_id)));
105
+ }