tracecat-mcp-community 1.0.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.
@@ -0,0 +1,318 @@
1
+ import { z } from "zod";
2
+ export function registerWorkflowTools(server, client) {
3
+ server.tool("tracecat_list_workflows", "List all workflows in the current workspace", {}, async () => {
4
+ const workflows = await client.get("/workflows");
5
+ return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2) }] };
6
+ });
7
+ server.tool("tracecat_create_workflow", "Create a new workflow", {
8
+ title: z.string().describe("Workflow title"),
9
+ description: z.string().optional().describe("Workflow description"),
10
+ }, async ({ title, description }) => {
11
+ const workflow = await client.post("/workflows", { title, description: description ?? "" });
12
+ return { content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }] };
13
+ });
14
+ server.tool("tracecat_get_workflow", "Get details of a specific workflow by ID", {
15
+ workflow_id: z.string().describe("Workflow ID"),
16
+ }, async ({ workflow_id }) => {
17
+ const workflow = await client.get(`/workflows/${workflow_id}`);
18
+ return { content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }] };
19
+ });
20
+ server.tool("tracecat_update_workflow", "Update an existing workflow", {
21
+ workflow_id: z.string().describe("Workflow ID"),
22
+ title: z.string().optional().describe("New title"),
23
+ description: z.string().optional().describe("New description"),
24
+ status: z.string().optional().describe("New status (online/offline)"),
25
+ }, async ({ workflow_id, ...updates }) => {
26
+ const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
27
+ const workflow = await client.patch(`/workflows/${workflow_id}`, body);
28
+ return { content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }] };
29
+ });
30
+ server.tool("tracecat_deploy_workflow", "Deploy (commit) a workflow to make it active", {
31
+ workflow_id: z.string().describe("Workflow ID"),
32
+ }, async ({ workflow_id }) => {
33
+ const result = await client.post(`/workflows/${workflow_id}/commit`);
34
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
35
+ });
36
+ server.tool("tracecat_export_workflow", "Export a workflow as YAML definition", {
37
+ workflow_id: z.string().describe("Workflow ID"),
38
+ }, async ({ workflow_id }) => {
39
+ const result = await client.get(`/workflows/${workflow_id}/definition`);
40
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
41
+ });
42
+ server.tool("tracecat_delete_workflow", "Delete a workflow permanently", {
43
+ workflow_id: z.string().describe("Workflow ID"),
44
+ }, async ({ workflow_id }) => {
45
+ const result = await client.delete(`/workflows/${workflow_id}`);
46
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
47
+ });
48
+ server.tool("tracecat_validate_workflow", "Validate a workflow WITHOUT deploying it. Checks actions, inputs, expressions, graph connectivity, and trigger connections. Returns errors and warnings.", {
49
+ workflow_id: z.string().describe("Workflow ID"),
50
+ }, async ({ workflow_id }) => {
51
+ const errors = [];
52
+ const warnings = [];
53
+ // 1. Fetch workflow
54
+ let workflow;
55
+ try {
56
+ workflow = await client.get(`/workflows/${workflow_id}`);
57
+ }
58
+ catch (e) {
59
+ return {
60
+ content: [{ type: "text", text: JSON.stringify({ valid: false, errors: [`Workflow not found: ${e}`], warnings: [] }, null, 2) }],
61
+ isError: true,
62
+ };
63
+ }
64
+ // 2. Fetch actions
65
+ const actions = await client.get(`/actions`, { workflow_id });
66
+ if (actions.length === 0) {
67
+ errors.push("Workflow has no actions");
68
+ return {
69
+ content: [{ type: "text", text: JSON.stringify({ valid: false, errors, warnings }, null, 2) }],
70
+ };
71
+ }
72
+ // Build slug set for cross-referencing
73
+ const slugs = new Set(actions.map((a) => a.ref ?? a.title.toLowerCase().replace(/\s+/g, "_")));
74
+ // 3. Check each action
75
+ for (const action of actions) {
76
+ const label = action.title || action.id;
77
+ // Check inputs
78
+ if (!action.inputs || Object.keys(action.inputs).length === 0) {
79
+ warnings.push(`Action "${label}" has empty inputs`);
80
+ }
81
+ else {
82
+ // Check for unclosed expressions in input values
83
+ const inputStr = JSON.stringify(action.inputs);
84
+ const openCount = (inputStr.match(/\$\{\{/g) || []).length;
85
+ const closeCount = (inputStr.match(/\}\}/g) || []).length;
86
+ if (openCount !== closeCount) {
87
+ errors.push(`Action "${label}" has unclosed expression(s): ${openCount} opening vs ${closeCount} closing`);
88
+ }
89
+ }
90
+ // Check run_if references
91
+ if (action.control_flow?.run_if) {
92
+ const runIf = action.control_flow.run_if;
93
+ const refs = runIf.match(/ACTIONS\.([a-zA-Z0-9_]+)/g);
94
+ if (refs) {
95
+ for (const ref of refs) {
96
+ const slug = ref.replace("ACTIONS.", "");
97
+ if (!slugs.has(slug)) {
98
+ errors.push(`Action "${label}" run_if references unknown slug "${slug}"`);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ // Check for_each references
104
+ if (action.control_flow?.for_each) {
105
+ const forEach = action.control_flow.for_each;
106
+ const refs = forEach.match(/ACTIONS\.([a-zA-Z0-9_]+)/g);
107
+ if (refs) {
108
+ for (const ref of refs) {
109
+ const slug = ref.replace("ACTIONS.", "");
110
+ if (!slugs.has(slug)) {
111
+ errors.push(`Action "${label}" for_each references unknown slug "${slug}"`);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ // 4. Fetch graph
118
+ let graph;
119
+ try {
120
+ graph = await client.get(`/workflows/${workflow_id}/graph`);
121
+ }
122
+ catch (e) {
123
+ errors.push(`Could not fetch graph: ${e}`);
124
+ return {
125
+ content: [{ type: "text", text: JSON.stringify({ valid: errors.length === 0, errors, warnings }, null, 2) }],
126
+ };
127
+ }
128
+ const edges = graph.edges ?? [];
129
+ if (edges.length === 0) {
130
+ errors.push("Graph has no edges — actions are not connected");
131
+ }
132
+ else {
133
+ // Check trigger connection
134
+ const triggerId = graph.trigger?.id;
135
+ if (triggerId) {
136
+ const triggerEdges = edges.filter((e) => e.source_id === triggerId);
137
+ if (triggerEdges.length === 0) {
138
+ errors.push("Trigger is not connected to any action");
139
+ }
140
+ }
141
+ // Check for orphan nodes (not source or target of any edge)
142
+ const connectedIds = new Set();
143
+ for (const edge of edges) {
144
+ connectedIds.add(edge.source_id);
145
+ connectedIds.add(edge.target_id);
146
+ }
147
+ for (const action of actions) {
148
+ if (!connectedIds.has(action.id)) {
149
+ warnings.push(`Action "${action.title || action.id}" is orphaned (no edges)`);
150
+ }
151
+ }
152
+ }
153
+ const valid = errors.length === 0;
154
+ return {
155
+ content: [{ type: "text", text: JSON.stringify({ valid, errors, warnings }, null, 2) }],
156
+ };
157
+ });
158
+ server.tool("tracecat_autofix_workflow", "Validate a workflow and automatically fix common issues: orphan nodes (position + connect), disconnected trigger (connect to first action), unclosed expressions (close }}). Returns a report of fixes applied.", {
159
+ workflow_id: z.string().describe("Workflow ID"),
160
+ dry_run: z.boolean().optional().describe("If true, only report what would be fixed without applying changes (default: false)"),
161
+ }, async ({ workflow_id, dry_run }) => {
162
+ const fixes = [];
163
+ const unfixable = [];
164
+ const dryMode = dry_run ?? false;
165
+ // 1. Fetch workflow, actions, graph
166
+ let actions;
167
+ let graph;
168
+ try {
169
+ actions = await client.get(`/actions`, { workflow_id });
170
+ graph = await client.get(`/workflows/${workflow_id}/graph`);
171
+ }
172
+ catch (e) {
173
+ return {
174
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: `Failed to fetch workflow data: ${e}` }, null, 2) }],
175
+ isError: true,
176
+ };
177
+ }
178
+ if (actions.length === 0) {
179
+ return {
180
+ content: [{ type: "text", text: JSON.stringify({ success: true, fixes: [], message: "No actions to fix" }, null, 2) }],
181
+ };
182
+ }
183
+ const edges = graph.edges ?? [];
184
+ const triggerId = graph.trigger?.id;
185
+ const graphOps = [];
186
+ // 2. Check trigger connection
187
+ if (triggerId) {
188
+ const triggerEdges = edges.filter((e) => e.source_id === triggerId);
189
+ if (triggerEdges.length === 0 && actions.length > 0) {
190
+ fixes.push(`FIX: Connect trigger to first action "${actions[0].title}"`);
191
+ graphOps.push({
192
+ type: "add_edge",
193
+ payload: {
194
+ source_id: triggerId,
195
+ source_type: "trigger",
196
+ target_id: actions[0].id,
197
+ source_handle: null,
198
+ },
199
+ });
200
+ }
201
+ }
202
+ // 3. Find orphan nodes
203
+ const connectedIds = new Set();
204
+ for (const edge of edges) {
205
+ connectedIds.add(edge.source_id);
206
+ connectedIds.add(edge.target_id);
207
+ }
208
+ // Find last connected action (to chain orphans after it)
209
+ const targetIds = new Set(edges.map((e) => e.target_id));
210
+ const sourceIds = new Set(edges.map((e) => e.source_id));
211
+ let lastNodeId = "";
212
+ let lastNodeType = "udf";
213
+ // Find leaf node (connected but not a source of any edge, excluding trigger)
214
+ for (const action of actions) {
215
+ if (connectedIds.has(action.id) && !sourceIds.has(action.id)) {
216
+ lastNodeId = action.id;
217
+ }
218
+ }
219
+ if (!lastNodeId && triggerId && edges.length > 0) {
220
+ // Fallback: use last target in edge list
221
+ lastNodeId = edges[edges.length - 1].target_id;
222
+ }
223
+ if (!lastNodeId && triggerId) {
224
+ lastNodeId = triggerId;
225
+ lastNodeType = "trigger";
226
+ }
227
+ // Calculate next Y position based on connected actions count
228
+ const connectedCount = actions.filter((a) => connectedIds.has(a.id)).length;
229
+ let nextY = 300 + connectedCount * 160;
230
+ const positions = [];
231
+ for (const action of actions) {
232
+ if (!connectedIds.has(action.id)) {
233
+ fixes.push(`FIX: Connect orphan "${action.title}" after last node, position at (500, ${nextY})`);
234
+ if (lastNodeId) {
235
+ graphOps.push({
236
+ type: "add_edge",
237
+ payload: {
238
+ source_id: lastNodeId,
239
+ source_type: lastNodeType,
240
+ target_id: action.id,
241
+ source_handle: null,
242
+ },
243
+ });
244
+ }
245
+ positions.push({ action_id: action.id, x: 500, y: nextY });
246
+ lastNodeId = action.id;
247
+ lastNodeType = "udf";
248
+ nextY += 160;
249
+ }
250
+ }
251
+ if (positions.length > 0) {
252
+ graphOps.push({
253
+ type: "move_nodes",
254
+ payload: { positions },
255
+ });
256
+ }
257
+ // 4. Check unclosed expressions in action inputs
258
+ for (const action of actions) {
259
+ if (!action.inputs)
260
+ continue;
261
+ const inputStr = JSON.stringify(action.inputs);
262
+ const openCount = (inputStr.match(/\$\{\{/g) || []).length;
263
+ const closeCount = (inputStr.match(/\}\}/g) || []).length;
264
+ if (openCount > closeCount) {
265
+ // Try to fix by adding missing closing braces
266
+ const diff = openCount - closeCount;
267
+ let fixedStr = inputStr;
268
+ // Find unclosed ${{ and add }} before the next quote or end
269
+ for (let i = 0; i < diff; i++) {
270
+ fixedStr = fixedStr.replace(/(\$\{\{[^}]*?)(")/g, "$1 }}$2");
271
+ }
272
+ try {
273
+ const fixedInputs = JSON.parse(fixedStr);
274
+ // Rebuild as YAML string for update
275
+ const yamlLines = [];
276
+ for (const [key, val] of Object.entries(fixedInputs)) {
277
+ yamlLines.push(`${key}: ${typeof val === "string" ? val : JSON.stringify(val)}`);
278
+ }
279
+ if (!dryMode) {
280
+ await client.post(`/actions/${action.id}`, { inputs: yamlLines.join("\n") }, { workflow_id });
281
+ }
282
+ fixes.push(`FIX: Closed ${diff} unclosed expression(s) in "${action.title}"`);
283
+ }
284
+ catch {
285
+ unfixable.push(`Cannot auto-fix unclosed expressions in "${action.title}" — manual fix needed`);
286
+ }
287
+ }
288
+ }
289
+ // 5. Apply graph operations
290
+ if (graphOps.length > 0 && !dryMode) {
291
+ try {
292
+ await client.patchGraph(workflow_id, graphOps);
293
+ }
294
+ catch (e) {
295
+ return {
296
+ content: [{ type: "text", text: JSON.stringify({
297
+ success: false,
298
+ error: `Graph operations failed: ${e}`,
299
+ planned_fixes: fixes,
300
+ }, null, 2) }],
301
+ isError: true,
302
+ };
303
+ }
304
+ }
305
+ return {
306
+ content: [{ type: "text", text: JSON.stringify({
307
+ success: true,
308
+ dry_run: dryMode,
309
+ fixes_applied: fixes.length,
310
+ fixes,
311
+ unfixable,
312
+ message: dryMode
313
+ ? `${fixes.length} fix(es) identified (dry run — no changes applied)`
314
+ : `${fixes.length} fix(es) applied successfully`,
315
+ }, null, 2) }],
316
+ };
317
+ });
318
+ }
@@ -0,0 +1,78 @@
1
+ export interface Workflow {
2
+ id: string;
3
+ title: string;
4
+ description: string;
5
+ status: string;
6
+ version: number | null;
7
+ entrypoint: string | null;
8
+ static_inputs: Record<string, unknown>;
9
+ returns: unknown;
10
+ config: Record<string, unknown>;
11
+ owner_id: string;
12
+ created_at: string;
13
+ updated_at: string;
14
+ }
15
+ export interface WorkflowExecution {
16
+ id: string;
17
+ workflow_id: string;
18
+ status: string;
19
+ start_time: string;
20
+ end_time: string | null;
21
+ result: unknown;
22
+ error: string | null;
23
+ }
24
+ export interface Case {
25
+ id: string;
26
+ workflow_id: string;
27
+ case_title: string;
28
+ payload: Record<string, unknown>;
29
+ malice: string;
30
+ status: string;
31
+ priority: string;
32
+ action: string;
33
+ context: Record<string, unknown>;
34
+ suppression: Record<string, unknown>[];
35
+ tags: Record<string, unknown>;
36
+ assigned_to: string | null;
37
+ owner_id: string;
38
+ created_at: string;
39
+ updated_at: string;
40
+ }
41
+ export interface CaseComment {
42
+ id: string;
43
+ case_id: string;
44
+ content: string;
45
+ created_by: string;
46
+ created_at: string;
47
+ }
48
+ export interface Action {
49
+ id: string;
50
+ type: string;
51
+ title: string;
52
+ description: string;
53
+ status: string;
54
+ inputs: Record<string, unknown>;
55
+ workflow_id: string;
56
+ }
57
+ export interface Secret {
58
+ id: string;
59
+ type: string;
60
+ name: string;
61
+ description: string | null;
62
+ keys: string[];
63
+ owner_id: string;
64
+ created_at: string;
65
+ updated_at: string;
66
+ }
67
+ export interface Table {
68
+ id: string;
69
+ name: string;
70
+ description: string | null;
71
+ owner_id: string;
72
+ created_at: string;
73
+ updated_at: string;
74
+ }
75
+ export interface TracecatApiError {
76
+ detail: string;
77
+ status: number;
78
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "tracecat-mcp-community",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Tracecat SOAR platform — 49 tools for workflows, actions, cases, secrets, tables, and more",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "tracecat-mcp-community": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE",
14
+ ".env.example"
15
+ ],
16
+ "scripts": {
17
+ "start": "node dist/index.js",
18
+ "build": "tsc",
19
+ "dev": "tsx --watch src/index.ts",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "node --test test/"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "tracecat",
26
+ "soar",
27
+ "security",
28
+ "automation",
29
+ "model-context-protocol",
30
+ "claude",
31
+ "ai"
32
+ ],
33
+ "author": "adrojis",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/adrojis/tracecat-mcp-community.git"
38
+ },
39
+ "homepage": "https://github.com/adrojis/tracecat-mcp-community",
40
+ "bugs": {
41
+ "url": "https://github.com/adrojis/tracecat-mcp-community/issues"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.12.1",
48
+ "dotenv": "^16.4.7",
49
+ "zod": "^3.24.2"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.13.4",
53
+ "tsx": "^4.19.3",
54
+ "typescript": "^5.7.3"
55
+ }
56
+ }