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.
- package/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +147 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +32 -0
- package/dist/minimal.d.ts +1 -0
- package/dist/minimal.js +8 -0
- package/dist/minimal.js.backup +8 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +32 -0
- package/dist/tools/actions.d.ts +3 -0
- package/dist/tools/actions.js +54 -0
- package/dist/tools/cases.d.ts +3 -0
- package/dist/tools/cases.js +73 -0
- package/dist/tools/docs.d.ts +3 -0
- package/dist/tools/docs.js +389 -0
- package/dist/tools/executions.d.ts +3 -0
- package/dist/tools/executions.js +40 -0
- package/dist/tools/graph.d.ts +3 -0
- package/dist/tools/graph.js +82 -0
- package/dist/tools/schedules.d.ts +3 -0
- package/dist/tools/schedules.js +52 -0
- package/dist/tools/secrets.d.ts +3 -0
- package/dist/tools/secrets.js +56 -0
- package/dist/tools/system.d.ts +3 -0
- package/dist/tools/system.js +16 -0
- package/dist/tools/tables.d.ts +3 -0
- package/dist/tools/tables.js +102 -0
- package/dist/tools/templates.d.ts +3 -0
- package/dist/tools/templates.js +547 -0
- package/dist/tools/webhooks.d.ts +3 -0
- package/dist/tools/webhooks.js +9 -0
- package/dist/tools/workflows.d.ts +3 -0
- package/dist/tools/workflows.js +318 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerCaseTools(server, client) {
|
|
3
|
+
server.tool("tracecat_list_cases", "List all cases in the current workspace", {
|
|
4
|
+
limit: z.number().optional().describe("Maximum number of results"),
|
|
5
|
+
status: z.string().optional().describe("Filter by status (new, in_progress, resolved, closed)"),
|
|
6
|
+
}, async ({ limit, status }) => {
|
|
7
|
+
const params = {};
|
|
8
|
+
if (limit)
|
|
9
|
+
params.limit = limit.toString();
|
|
10
|
+
if (status)
|
|
11
|
+
params.status = status;
|
|
12
|
+
const cases = await client.get("/cases", params);
|
|
13
|
+
return { content: [{ type: "text", text: JSON.stringify(cases, null, 2) }] };
|
|
14
|
+
});
|
|
15
|
+
server.tool("tracecat_create_case", "Create a new case", {
|
|
16
|
+
workflow_id: z.string().describe("Workflow ID to associate with"),
|
|
17
|
+
case_title: z.string().describe("Title of the case"),
|
|
18
|
+
payload: z.record(z.unknown()).optional().describe("Case payload data"),
|
|
19
|
+
malice: z.string().optional().describe("Malice level (benign, malicious, unknown)"),
|
|
20
|
+
status: z.string().optional().describe("Initial status"),
|
|
21
|
+
priority: z.string().optional().describe("Priority level (low, medium, high, critical)"),
|
|
22
|
+
action: z.string().optional().describe("Recommended action"),
|
|
23
|
+
}, async ({ workflow_id, case_title, payload, malice, status, priority, action }) => {
|
|
24
|
+
const body = {
|
|
25
|
+
workflow_id,
|
|
26
|
+
case_title,
|
|
27
|
+
payload: payload ?? {},
|
|
28
|
+
malice: malice ?? "unknown",
|
|
29
|
+
status: status ?? "new",
|
|
30
|
+
priority: priority ?? "medium",
|
|
31
|
+
action: action ?? "",
|
|
32
|
+
};
|
|
33
|
+
const result = await client.post("/cases", body);
|
|
34
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
35
|
+
});
|
|
36
|
+
server.tool("tracecat_update_case", "Update an existing case", {
|
|
37
|
+
case_id: z.string().describe("Case ID"),
|
|
38
|
+
case_title: z.string().optional().describe("New title"),
|
|
39
|
+
status: z.string().optional().describe("New status"),
|
|
40
|
+
priority: z.string().optional().describe("New priority"),
|
|
41
|
+
malice: z.string().optional().describe("New malice level"),
|
|
42
|
+
action: z.string().optional().describe("New action"),
|
|
43
|
+
}, async ({ case_id, ...updates }) => {
|
|
44
|
+
const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
|
|
45
|
+
const result = await client.patch(`/cases/${case_id}`, body);
|
|
46
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
47
|
+
});
|
|
48
|
+
server.tool("tracecat_add_comment", "Add a comment to a case", {
|
|
49
|
+
case_id: z.string().describe("Case ID"),
|
|
50
|
+
content: z.string().describe("Comment content"),
|
|
51
|
+
}, async ({ case_id, content }) => {
|
|
52
|
+
const result = await client.post(`/cases/${case_id}/comments`, { content });
|
|
53
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
54
|
+
});
|
|
55
|
+
server.tool("tracecat_get_case", "Get details of a specific case by ID", {
|
|
56
|
+
case_id: z.string().describe("Case ID"),
|
|
57
|
+
}, async ({ case_id }) => {
|
|
58
|
+
const caseData = await client.get(`/cases/${case_id}`);
|
|
59
|
+
return { content: [{ type: "text", text: JSON.stringify(caseData, null, 2) }] };
|
|
60
|
+
});
|
|
61
|
+
server.tool("tracecat_delete_case", "Delete a case permanently", {
|
|
62
|
+
case_id: z.string().describe("Case ID"),
|
|
63
|
+
}, async ({ case_id }) => {
|
|
64
|
+
const result = await client.delete(`/cases/${case_id}`);
|
|
65
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
66
|
+
});
|
|
67
|
+
server.tool("tracecat_list_comments", "List all comments on a case", {
|
|
68
|
+
case_id: z.string().describe("Case ID"),
|
|
69
|
+
}, async ({ case_id }) => {
|
|
70
|
+
const comments = await client.get(`/cases/${case_id}/comments`);
|
|
71
|
+
return { content: [{ type: "text", text: JSON.stringify(comments, null, 2) }] };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const DOCS = {
|
|
3
|
+
action_types: `# Tracecat Action Types Reference
|
|
4
|
+
|
|
5
|
+
## Core Actions
|
|
6
|
+
| Type | Description | Key Inputs |
|
|
7
|
+
|------|-------------|------------|
|
|
8
|
+
| \`core.transform.reshape\` | Transform/reshape data | \`value\`: any expression |
|
|
9
|
+
| \`core.http_request\` | Make HTTP requests | \`url\`, \`method\`, \`headers\`, \`payload\` |
|
|
10
|
+
| \`core.send_email\` | Send email via SMTP | \`recipients\`, \`subject\`, \`body\` |
|
|
11
|
+
| \`core.workflow.execute\` | Execute another workflow | \`workflow_id\`, \`trigger_inputs\` |
|
|
12
|
+
| \`core.script.run_python\` | Run Python script | \`script\` (NOT \`code\`), \`inputs\`, \`dependencies\`, \`timeout_seconds\`, \`allow_network\` |
|
|
13
|
+
| \`core.cases.create_case\` | Create a case | \`workflow_id\`, \`case_title\`, \`payload\`, \`malice\`, \`status\`, \`priority\`, \`action\` |
|
|
14
|
+
| \`core.cases.update_case\` | Update a case | \`case_id\`, \`status\`, \`priority\`, \`malice\`, \`action\` |
|
|
15
|
+
| \`core.cases.create_comment\` | Add comment to case | \`case_id\`, \`content\` |
|
|
16
|
+
|
|
17
|
+
## Integration Actions (tools.*)
|
|
18
|
+
Format: \`tools.<provider>.<action>\`
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
- \`tools.virustotal.get_file_report\` — VirusTotal file analysis
|
|
22
|
+
- \`tools.crowdstrike.list_detections\` — CrowdStrike detections
|
|
23
|
+
- \`tools.slack.post_message\` — Slack notification
|
|
24
|
+
- \`tools.abuseipdb.check_ip\` — AbuseIPDB lookup
|
|
25
|
+
- \`tools.greynoise.lookup_ip\` — GreyNoise IP context
|
|
26
|
+
|
|
27
|
+
## IMPORTANT
|
|
28
|
+
- HTTP request type is \`core.http_request\` (underscore, NOT \`core.http.request\`)
|
|
29
|
+
- Python script field is \`script\` (NOT \`code\`)
|
|
30
|
+
- Always use native action types (core.cases.*) before falling back to core.http_request`,
|
|
31
|
+
expressions: `# Tracecat Expressions Reference
|
|
32
|
+
|
|
33
|
+
## Syntax
|
|
34
|
+
All expressions use: \`\${{ CONTEXT.path }}\`
|
|
35
|
+
|
|
36
|
+
## Contexts
|
|
37
|
+
| Context | Description | Example |
|
|
38
|
+
|---------|-------------|---------|
|
|
39
|
+
| \`TRIGGER\` | Webhook/schedule input data | \`\${{ TRIGGER.alert.source_ip }}\` |
|
|
40
|
+
| \`ACTIONS\` | Results from previous actions | \`\${{ ACTIONS.my_action.result }}\` |
|
|
41
|
+
| \`ACTIONS.*.error\` | Error from failed action | \`\${{ ACTIONS.my_action.error }}\` |
|
|
42
|
+
| \`SECRETS\` | Secret values | \`\${{ SECRETS.my_secret.API_KEY }}\` |
|
|
43
|
+
| \`ENV\` | Environment variables | \`\${{ ENV.MY_VAR }}\` |
|
|
44
|
+
| \`FN\` | Built-in functions | \`\${{ FN.is_empty(TRIGGER.data) }}\` |
|
|
45
|
+
|
|
46
|
+
## Nested Access
|
|
47
|
+
- Dot notation: \`\${{ ACTIONS.enrich.result.data.score }}\`
|
|
48
|
+
- Dynamic context: \`\${{ ACTIONS.enrich.result }}\` returns full result object
|
|
49
|
+
|
|
50
|
+
## In YAML Inputs
|
|
51
|
+
\`\`\`yaml
|
|
52
|
+
url: https://api.example.com/\${{ TRIGGER.ip }}
|
|
53
|
+
headers:
|
|
54
|
+
Authorization: Bearer \${{ SECRETS.api_creds.TOKEN }}
|
|
55
|
+
payload:
|
|
56
|
+
ip: \${{ TRIGGER.source_ip }}
|
|
57
|
+
previous_score: \${{ ACTIONS.score_ip.result.score }}
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
## In Control Flow (run_if)
|
|
61
|
+
\`\`\`yaml
|
|
62
|
+
# Truthy check (NOT == true)
|
|
63
|
+
run_if: \${{ ACTIONS.check_ip.result }}
|
|
64
|
+
# Falsy check (NOT == false)
|
|
65
|
+
run_if: \${{ not ACTIONS.check_ip.result }}
|
|
66
|
+
# Comparison
|
|
67
|
+
run_if: \${{ ACTIONS.score.result.score > 70 }}
|
|
68
|
+
# Logical operators: && and || (NOT 'and'/'or')
|
|
69
|
+
run_if: \${{ ACTIONS.is_malicious.result && ACTIONS.is_public.result }}
|
|
70
|
+
\`\`\``,
|
|
71
|
+
functions: `# Tracecat Built-in Functions (FN.*)
|
|
72
|
+
|
|
73
|
+
## String Functions
|
|
74
|
+
| Function | Description | Example |
|
|
75
|
+
|----------|-------------|---------|
|
|
76
|
+
| \`FN.upper(s)\` | Uppercase | \`\${{ FN.upper(TRIGGER.name) }}\` |
|
|
77
|
+
| \`FN.lower(s)\` | Lowercase | \`\${{ FN.lower(TRIGGER.name) }}\` |
|
|
78
|
+
| \`FN.contains(s, sub)\` | Check substring | \`\${{ FN.contains(TRIGGER.url, "malware") }}\` |
|
|
79
|
+
| \`FN.format(fmt, ...)\` | Format string | \`\${{ FN.format("IP: {}", TRIGGER.ip) }}\` |
|
|
80
|
+
| \`FN.regex_match(pattern, s)\` | Regex match | \`\${{ FN.regex_match("\\\\d+", TRIGGER.text) }}\` |
|
|
81
|
+
| \`FN.regex_not_match(pattern, s)\` | Regex no match | \`\${{ FN.regex_not_match("safe", TRIGGER.text) }}\` |
|
|
82
|
+
|
|
83
|
+
## Type / Conversion Functions
|
|
84
|
+
| Function | Description | Example |
|
|
85
|
+
|----------|-------------|---------|
|
|
86
|
+
| \`FN.is_empty(v)\` | Check if null/empty | \`\${{ FN.is_empty(ACTIONS.x.result) }}\` |
|
|
87
|
+
| \`FN.is_not_empty(v)\` | Check if not empty | \`\${{ FN.is_not_empty(ACTIONS.x.result) }}\` |
|
|
88
|
+
| \`FN.serialize_json(v)\` | Object → JSON string | \`\${{ FN.serialize_json(ACTIONS.x.result) }}\` |
|
|
89
|
+
| \`FN.deserialize_json(s)\` | JSON string → object | \`\${{ FN.deserialize_json(TRIGGER.raw) }}\` |
|
|
90
|
+
| \`FN.to_int(v)\` | Convert to integer | \`\${{ FN.to_int(TRIGGER.count) }}\` |
|
|
91
|
+
| \`FN.to_float(v)\` | Convert to float | \`\${{ FN.to_float(TRIGGER.score) }}\` |
|
|
92
|
+
|
|
93
|
+
## Network Functions
|
|
94
|
+
| Function | Description | Example |
|
|
95
|
+
|----------|-------------|---------|
|
|
96
|
+
| \`FN.ipv4_is_public(ip)\` | Check if IPv4 is public | \`\${{ FN.ipv4_is_public(TRIGGER.ip) }}\` |
|
|
97
|
+
| \`FN.ipv4_in_subnet(ip, cidr)\` | Check if in subnet | \`\${{ FN.ipv4_in_subnet(TRIGGER.ip, "10.0.0.0/8") }}\` |
|
|
98
|
+
|
|
99
|
+
## Collection Functions
|
|
100
|
+
| Function | Description | Example |
|
|
101
|
+
|----------|-------------|---------|
|
|
102
|
+
| \`FN.length(list)\` | Length of list | \`\${{ FN.length(ACTIONS.x.result) }}\` |
|
|
103
|
+
| \`FN.flatten(list)\` | Flatten nested list | \`\${{ FN.flatten(ACTIONS.x.result) }}\` |
|
|
104
|
+
| \`FN.unique(list)\` | Deduplicate list | \`\${{ FN.unique(ACTIONS.x.result) }}\` |
|
|
105
|
+
| \`FN.join(list, sep)\` | Join list to string | \`\${{ FN.join(ACTIONS.x.result, ", ") }}\` |
|
|
106
|
+
|
|
107
|
+
## Math Functions
|
|
108
|
+
| Function | Description | Example |
|
|
109
|
+
|----------|-------------|---------|
|
|
110
|
+
| \`FN.add(a, b)\` | Addition | \`\${{ FN.add(ACTIONS.x.result, 1) }}\` |
|
|
111
|
+
| \`FN.sub(a, b)\` | Subtraction | \`\${{ FN.sub(ACTIONS.x.result, 1) }}\` |
|
|
112
|
+
| \`FN.less_than(a, b)\` | a < b | \`\${{ FN.less_than(ACTIONS.x.result, 50) }}\` |
|
|
113
|
+
| \`FN.greater_than(a, b)\` | a > b | \`\${{ FN.greater_than(ACTIONS.x.result, 50) }}\` |
|
|
114
|
+
|
|
115
|
+
## Conditional
|
|
116
|
+
| Function | Description | Example |
|
|
117
|
+
|----------|-------------|---------|
|
|
118
|
+
| \`FN.conditional(cond, a, b)\` | Ternary | \`\${{ FN.conditional(ACTIONS.x.result, "yes", "no") }}\` |`,
|
|
119
|
+
control_flow: `# Tracecat Control Flow Reference
|
|
120
|
+
|
|
121
|
+
## Conditional Execution (run_if)
|
|
122
|
+
Only execute an action if the condition is truthy.
|
|
123
|
+
|
|
124
|
+
\`\`\`yaml
|
|
125
|
+
# In action update control_flow:
|
|
126
|
+
control_flow:
|
|
127
|
+
run_if: \${{ ACTIONS.check_ip.result }}
|
|
128
|
+
\`\`\`
|
|
129
|
+
|
|
130
|
+
### Important Rules
|
|
131
|
+
- Do NOT use \`== true\` or \`== false\`
|
|
132
|
+
- Use truthy: \`\${{ ACTIONS.x.result }}\`
|
|
133
|
+
- Use falsy: \`\${{ not ACTIONS.x.result }}\`
|
|
134
|
+
- Logical AND: \`\${{ A && B }}\` (NOT \`and\`)
|
|
135
|
+
- Logical OR: \`\${{ A || B }}\` (NOT \`or\`)
|
|
136
|
+
- Comparisons: \`\${{ ACTIONS.score.result > 70 }}\`
|
|
137
|
+
|
|
138
|
+
## Loops (for_each)
|
|
139
|
+
Iterate over a list, executing the action once per item.
|
|
140
|
+
|
|
141
|
+
\`\`\`yaml
|
|
142
|
+
control_flow:
|
|
143
|
+
for_each: \${{ ACTIONS.list_ips.result }}
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
The current item is available as \`\${{ ACTIONS.current_action.result }}\` within the loop body.
|
|
147
|
+
|
|
148
|
+
## Join Strategy
|
|
149
|
+
When multiple parent actions converge, control when the child executes.
|
|
150
|
+
|
|
151
|
+
\`\`\`yaml
|
|
152
|
+
control_flow:
|
|
153
|
+
join_strategy: all # Wait for ALL parents (default)
|
|
154
|
+
# or
|
|
155
|
+
join_strategy: any # Execute as soon as ANY parent completes
|
|
156
|
+
\`\`\`
|
|
157
|
+
|
|
158
|
+
## Retry Policy
|
|
159
|
+
Retry a failed action automatically.
|
|
160
|
+
|
|
161
|
+
\`\`\`yaml
|
|
162
|
+
control_flow:
|
|
163
|
+
retry_policy:
|
|
164
|
+
max_attempts: 3 # Total attempts (including first)
|
|
165
|
+
timeout: 30 # Timeout per attempt in seconds
|
|
166
|
+
retry_until: null # Optional expression that stops retries when truthy
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
## Start Delay
|
|
170
|
+
Delay execution by N seconds.
|
|
171
|
+
|
|
172
|
+
\`\`\`yaml
|
|
173
|
+
control_flow:
|
|
174
|
+
start_delay: 5 # Wait 5 seconds before executing
|
|
175
|
+
\`\`\`
|
|
176
|
+
|
|
177
|
+
## Error Paths
|
|
178
|
+
Branch on action failure using source_handle in edges:
|
|
179
|
+
|
|
180
|
+
- \`success\` — execute when parent succeeds
|
|
181
|
+
- \`error\` — execute when parent fails
|
|
182
|
+
|
|
183
|
+
Access error data: \`\${{ ACTIONS.failed_action.error }}\`
|
|
184
|
+
|
|
185
|
+
## Complete Example
|
|
186
|
+
\`\`\`yaml
|
|
187
|
+
# Action with all control flow options
|
|
188
|
+
control_flow:
|
|
189
|
+
run_if: \${{ ACTIONS.should_proceed.result }}
|
|
190
|
+
for_each: \${{ ACTIONS.get_ips.result }}
|
|
191
|
+
join_strategy: all
|
|
192
|
+
retry_policy:
|
|
193
|
+
max_attempts: 3
|
|
194
|
+
timeout: 60
|
|
195
|
+
start_delay: 2
|
|
196
|
+
\`\`\``,
|
|
197
|
+
common_mistakes: `# Tracecat Common Mistakes — Top 15
|
|
198
|
+
|
|
199
|
+
## 1. Wrong HTTP request action type
|
|
200
|
+
- **Wrong:** \`core.http.request\` (dot)
|
|
201
|
+
- **Right:** \`core.http_request\` (underscore)
|
|
202
|
+
|
|
203
|
+
## 2. Python script field name
|
|
204
|
+
- **Wrong:** \`code: "def main()..."\`
|
|
205
|
+
- **Right:** \`script: "def main()..."\`
|
|
206
|
+
|
|
207
|
+
## 3. Boolean comparisons in run_if
|
|
208
|
+
- **Wrong:** \`run_if: \${{ ACTIONS.x.result == true }}\`
|
|
209
|
+
- **Right:** \`run_if: \${{ ACTIONS.x.result }}\`
|
|
210
|
+
- **Wrong:** \`run_if: \${{ ACTIONS.x.result == false }}\`
|
|
211
|
+
- **Right:** \`run_if: \${{ not ACTIONS.x.result }}\`
|
|
212
|
+
|
|
213
|
+
## 4. Logical operators in expressions
|
|
214
|
+
- **Wrong:** \`\${{ A and B }}\` / \`\${{ A or B }}\`
|
|
215
|
+
- **Right:** \`\${{ A && B }}\` / \`\${{ A || B }}\`
|
|
216
|
+
|
|
217
|
+
## 5. Action inputs format
|
|
218
|
+
- **Wrong:** Sending inputs as JSON object
|
|
219
|
+
- **Right:** Sending inputs as YAML string
|
|
220
|
+
|
|
221
|
+
## 6. Action update HTTP method
|
|
222
|
+
- **Wrong:** \`PATCH /actions/{id}\`
|
|
223
|
+
- **Right:** \`POST /actions/{id}\` (with workflow_id query param)
|
|
224
|
+
|
|
225
|
+
## 7. Listing actions endpoint
|
|
226
|
+
- **Wrong:** \`GET /workflows/{id}/actions\`
|
|
227
|
+
- **Right:** \`GET /actions?workflow_id={id}\`
|
|
228
|
+
|
|
229
|
+
## 8. Missing workspace_id
|
|
230
|
+
- Must be a **query parameter** on every request
|
|
231
|
+
- NOT a header
|
|
232
|
+
- MCP tools handle this automatically
|
|
233
|
+
|
|
234
|
+
## 9. Orphaned graph nodes
|
|
235
|
+
- Creating actions without adding edges = invisible in UI
|
|
236
|
+
- Always add edges AND position nodes after creating actions
|
|
237
|
+
|
|
238
|
+
## 10. Unclosed expressions
|
|
239
|
+
- **Wrong:** \`\${{ ACTIONS.x.result\`
|
|
240
|
+
- **Right:** \`\${{ ACTIONS.x.result }}\`
|
|
241
|
+
|
|
242
|
+
## 11. Using core.http_request for native operations
|
|
243
|
+
- **Wrong:** \`core.http_request\` to create a case
|
|
244
|
+
- **Right:** \`core.cases.create_case\`
|
|
245
|
+
|
|
246
|
+
## 12. Secret/Schedule update method
|
|
247
|
+
- **Wrong:** \`PATCH /secrets/{id}\` or \`PATCH /schedules/{id}\`
|
|
248
|
+
- **Right:** \`POST /secrets/{id}\` / \`POST /schedules/{id}\`
|
|
249
|
+
|
|
250
|
+
## 13. FN.serialize_json for Python inputs
|
|
251
|
+
- When passing objects to run_python, serialize them:
|
|
252
|
+
\`\${{ FN.serialize_json(ACTIONS.x.result) }}\`
|
|
253
|
+
|
|
254
|
+
## 14. Trigger spacing in graph layout
|
|
255
|
+
- Trigger at y=0 takes ~200px height
|
|
256
|
+
- First action should be at y >= 300 (not y=200)
|
|
257
|
+
|
|
258
|
+
## 15. Batch insert rows format
|
|
259
|
+
- **Wrong:** \`{ data: [{...}] }\`
|
|
260
|
+
- **Right:** \`{ rows: [{...}] }\` (flat objects, keys = column names)`,
|
|
261
|
+
};
|
|
262
|
+
const TOOLS_CATALOG = {
|
|
263
|
+
workflows: [
|
|
264
|
+
{ name: "tracecat_list_workflows", description: "List all workflows in the workspace", params: "(none)", example: "tracecat_list_workflows()" },
|
|
265
|
+
{ name: "tracecat_create_workflow", description: "Create a new workflow", params: "title (required), description", example: 'tracecat_create_workflow({ title: "Alert Triage" })' },
|
|
266
|
+
{ name: "tracecat_get_workflow", description: "Get workflow details by ID", params: "workflow_id (required)", example: 'tracecat_get_workflow({ workflow_id: "wf_xxx" })' },
|
|
267
|
+
{ name: "tracecat_update_workflow", description: "Update workflow title/description/status", params: "workflow_id (required), title, description, status", example: 'tracecat_update_workflow({ workflow_id: "wf_xxx", status: "online" })' },
|
|
268
|
+
{ name: "tracecat_deploy_workflow", description: "Deploy (commit) a workflow", params: "workflow_id (required)", example: 'tracecat_deploy_workflow({ workflow_id: "wf_xxx" })' },
|
|
269
|
+
{ name: "tracecat_export_workflow", description: "Export workflow as YAML", params: "workflow_id (required)", example: 'tracecat_export_workflow({ workflow_id: "wf_xxx" })' },
|
|
270
|
+
{ name: "tracecat_delete_workflow", description: "Delete a workflow permanently", params: "workflow_id (required)", example: 'tracecat_delete_workflow({ workflow_id: "wf_xxx" })' },
|
|
271
|
+
{ name: "tracecat_validate_workflow", description: "Validate workflow (actions, inputs, expressions, graph)", params: "workflow_id (required)", example: 'tracecat_validate_workflow({ workflow_id: "wf_xxx" })' },
|
|
272
|
+
{ name: "tracecat_autofix_workflow", description: "Validate + auto-fix common issues (orphans, disconnected trigger, unclosed expressions)", params: "workflow_id (required), dry_run", example: 'tracecat_autofix_workflow({ workflow_id: "wf_xxx" })' },
|
|
273
|
+
],
|
|
274
|
+
actions: [
|
|
275
|
+
{ name: "tracecat_list_actions", description: "List all actions for a workflow", params: "workflow_id (required)", example: 'tracecat_list_actions({ workflow_id: "wf_xxx" })' },
|
|
276
|
+
{ name: "tracecat_create_action", description: "Create a new action in a workflow", params: "workflow_id (required), type (required), title (required)", example: 'tracecat_create_action({ workflow_id: "wf_xxx", type: "core.http_request", title: "Check IP" })' },
|
|
277
|
+
{ name: "tracecat_get_action", description: "Get action details", params: "action_id (required), workflow_id (required)", example: 'tracecat_get_action({ action_id: "act_xxx", workflow_id: "wf_xxx" })' },
|
|
278
|
+
{ name: "tracecat_update_action", description: "Update action inputs/control_flow. Inputs must be YAML string!", params: "action_id (required), workflow_id (required), title, description, inputs (YAML string), control_flow", example: 'tracecat_update_action({ action_id: "act_xxx", workflow_id: "wf_xxx", inputs: "url: https://...\\nmethod: GET" })' },
|
|
279
|
+
{ name: "tracecat_delete_action", description: "Delete an action", params: "action_id (required), workflow_id (required)", example: 'tracecat_delete_action({ action_id: "act_xxx", workflow_id: "wf_xxx" })' },
|
|
280
|
+
],
|
|
281
|
+
executions: [
|
|
282
|
+
{ name: "tracecat_run_workflow", description: "Execute a workflow with optional payload", params: "workflow_id (required), payload", example: 'tracecat_run_workflow({ workflow_id: "wf_xxx", payload: { ip: "1.2.3.4" } })' },
|
|
283
|
+
{ name: "tracecat_list_executions", description: "List executions, optionally by workflow", params: "workflow_id, limit", example: 'tracecat_list_executions({ workflow_id: "wf_xxx", limit: 5 })' },
|
|
284
|
+
{ name: "tracecat_get_execution", description: "Get full execution details", params: "execution_id (required)", example: 'tracecat_get_execution({ execution_id: "exec_xxx" })' },
|
|
285
|
+
{ name: "tracecat_get_execution_compact", description: "Get compact execution (action status, inputs, results, errors)", params: "execution_id (required)", example: 'tracecat_get_execution_compact({ execution_id: "exec_xxx" })' },
|
|
286
|
+
{ name: "tracecat_cancel_execution", description: "Cancel a running execution", params: "execution_id (required)", example: 'tracecat_cancel_execution({ execution_id: "exec_xxx" })' },
|
|
287
|
+
],
|
|
288
|
+
cases: [
|
|
289
|
+
{ name: "tracecat_list_cases", description: "List cases in workspace", params: "limit, status", example: 'tracecat_list_cases({ status: "new", limit: 10 })' },
|
|
290
|
+
{ name: "tracecat_create_case", description: "Create a new case", params: "workflow_id (required), case_title (required), payload, malice, status, priority, action", example: 'tracecat_create_case({ workflow_id: "wf_xxx", case_title: "Suspicious IP" })' },
|
|
291
|
+
{ name: "tracecat_get_case", description: "Get case details", params: "case_id (required)", example: 'tracecat_get_case({ case_id: "case_xxx" })' },
|
|
292
|
+
{ name: "tracecat_update_case", description: "Update case status/priority/malice", params: "case_id (required), case_title, status, priority, malice, action", example: 'tracecat_update_case({ case_id: "case_xxx", status: "resolved" })' },
|
|
293
|
+
{ name: "tracecat_delete_case", description: "Delete a case permanently", params: "case_id (required)", example: 'tracecat_delete_case({ case_id: "case_xxx" })' },
|
|
294
|
+
{ name: "tracecat_add_comment", description: "Add comment to a case", params: "case_id (required), content (required)", example: 'tracecat_add_comment({ case_id: "case_xxx", content: "Investigation started" })' },
|
|
295
|
+
{ name: "tracecat_list_comments", description: "List all comments on a case", params: "case_id (required)", example: 'tracecat_list_comments({ case_id: "case_xxx" })' },
|
|
296
|
+
],
|
|
297
|
+
secrets: [
|
|
298
|
+
{ name: "tracecat_search_secrets", description: "Search secrets by name", params: "name", example: 'tracecat_search_secrets({ name: "virustotal" })' },
|
|
299
|
+
{ name: "tracecat_create_secret", description: "Create a new secret", params: "name (required), keys (required), type, description", example: 'tracecat_create_secret({ name: "virustotal", keys: [{ key: "API_KEY", value: "xxx" }] })' },
|
|
300
|
+
{ name: "tracecat_get_secret", description: "Get secret metadata (not values)", params: "secret_name (required)", example: 'tracecat_get_secret({ secret_name: "virustotal" })' },
|
|
301
|
+
{ name: "tracecat_update_secret", description: "Update secret keys/description", params: "secret_id (required), name, description, keys", example: 'tracecat_update_secret({ secret_id: "uuid", keys: [{ key: "API_KEY", value: "new" }] })' },
|
|
302
|
+
{ name: "tracecat_delete_secret", description: "Delete a secret", params: "secret_id (required)", example: 'tracecat_delete_secret({ secret_id: "uuid" })' },
|
|
303
|
+
],
|
|
304
|
+
tables: [
|
|
305
|
+
{ name: "tracecat_list_tables", description: "List all tables", params: "(none)", example: "tracecat_list_tables()" },
|
|
306
|
+
{ name: "tracecat_create_table", description: "Create a new table", params: "name (required), description", example: 'tracecat_create_table({ name: "ioc_enrichment" })' },
|
|
307
|
+
{ name: "tracecat_get_table", description: "Get table details (includes columns)", params: "table_id (required)", example: 'tracecat_get_table({ table_id: "tbl_xxx" })' },
|
|
308
|
+
{ name: "tracecat_update_table", description: "Update table name/description", params: "table_id (required), name, description", example: 'tracecat_update_table({ table_id: "tbl_xxx", name: "new_name" })' },
|
|
309
|
+
{ name: "tracecat_delete_table", description: "Delete a table", params: "table_id (required)", example: 'tracecat_delete_table({ table_id: "tbl_xxx" })' },
|
|
310
|
+
{ name: "tracecat_create_column", description: "Add column to table", params: "table_id (required), name (required), type (required: TEXT, INTEGER, NUMERIC, DATE, BOOLEAN, TIMESTAMP, TIMESTAMPTZ, JSONB, UUID, SELECT, MULTI_SELECT)", example: 'tracecat_create_column({ table_id: "tbl_xxx", name: "ip_address", type: "TEXT" })' },
|
|
311
|
+
{ name: "tracecat_delete_column", description: "Delete column from table", params: "table_id (required), column_id (required)", example: 'tracecat_delete_column({ table_id: "tbl_xxx", column_id: "col_xxx" })' },
|
|
312
|
+
{ name: "tracecat_list_rows", description: "List rows with pagination", params: "table_id (required), limit, offset", example: 'tracecat_list_rows({ table_id: "tbl_xxx", limit: 50 })' },
|
|
313
|
+
{ name: "tracecat_get_row", description: "Get single row by ID", params: "table_id (required), row_id (required)", example: 'tracecat_get_row({ table_id: "tbl_xxx", row_id: "row_xxx" })' },
|
|
314
|
+
{ name: "tracecat_insert_row", description: "Insert a row (keys = column names)", params: "table_id (required), data (required)", example: 'tracecat_insert_row({ table_id: "tbl_xxx", data: { ip: "1.2.3.4", score: 85 } })' },
|
|
315
|
+
{ name: "tracecat_update_row", description: "Update a row", params: "table_id (required), row_id (required), data (required)", example: 'tracecat_update_row({ table_id: "tbl_xxx", row_id: "row_xxx", data: { score: 90 } })' },
|
|
316
|
+
{ name: "tracecat_delete_row", description: "Delete a row", params: "table_id (required), row_id (required)", example: 'tracecat_delete_row({ table_id: "tbl_xxx", row_id: "row_xxx" })' },
|
|
317
|
+
{ name: "tracecat_batch_insert_rows", description: "Insert multiple rows at once", params: "table_id (required), rows (required: array of flat objects)", example: 'tracecat_batch_insert_rows({ table_id: "tbl_xxx", rows: [{ ip: "1.2.3.4" }, { ip: "5.6.7.8" }] })' },
|
|
318
|
+
],
|
|
319
|
+
graph: [
|
|
320
|
+
{ name: "tracecat_get_graph", description: "Get workflow graph (nodes, edges, positions, version)", params: "workflow_id (required)", example: 'tracecat_get_graph({ workflow_id: "wf_xxx" })' },
|
|
321
|
+
{ name: "tracecat_add_edges", description: "Connect actions in the graph", params: "workflow_id (required), edges (required: [{source_id, source_type, target_id, source_handle}])", example: 'tracecat_add_edges({ workflow_id: "wf_xxx", edges: [{ source_id: "trigger_id", source_type: "trigger", target_id: "act_xxx" }] })' },
|
|
322
|
+
{ name: "tracecat_delete_edges", description: "Remove edges from graph", params: "workflow_id (required), edges (required)", example: 'tracecat_delete_edges({ workflow_id: "wf_xxx", edges: [{ source_id: "act_1", source_type: "udf", target_id: "act_2" }] })' },
|
|
323
|
+
{ name: "tracecat_move_nodes", description: "Reposition nodes. Layout: trigger at (500,0), first action y>=300, 160px vertical spacing", params: "workflow_id (required), positions (required: [{action_id, x, y}])", example: 'tracecat_move_nodes({ workflow_id: "wf_xxx", positions: [{ action_id: "act_xxx", x: 500, y: 300 }] })' },
|
|
324
|
+
{ name: "tracecat_update_trigger_position", description: "Reposition the trigger node", params: "workflow_id (required), x (required), y (required)", example: 'tracecat_update_trigger_position({ workflow_id: "wf_xxx", x: 500, y: 0 })' },
|
|
325
|
+
],
|
|
326
|
+
schedules: [
|
|
327
|
+
{ name: "tracecat_list_schedules", description: "List all schedules", params: "workflow_id", example: 'tracecat_list_schedules({ workflow_id: "wf_xxx" })' },
|
|
328
|
+
{ name: "tracecat_create_schedule", description: "Create schedule (cron or interval)", params: "workflow_id (required), cron, every, inputs", example: 'tracecat_create_schedule({ workflow_id: "wf_xxx", every: "1h" })' },
|
|
329
|
+
{ name: "tracecat_get_schedule", description: "Get schedule details", params: "schedule_id (required)", example: 'tracecat_get_schedule({ schedule_id: "sched_xxx" })' },
|
|
330
|
+
{ name: "tracecat_update_schedule", description: "Update schedule", params: "schedule_id (required), cron, every, inputs, status", example: 'tracecat_update_schedule({ schedule_id: "sched_xxx", status: "offline" })' },
|
|
331
|
+
{ name: "tracecat_delete_schedule", description: "Delete a schedule", params: "schedule_id (required)", example: 'tracecat_delete_schedule({ schedule_id: "sched_xxx" })' },
|
|
332
|
+
],
|
|
333
|
+
webhooks: [
|
|
334
|
+
{ name: "tracecat_create_webhook_key", description: "Generate/rotate webhook API key", params: "workflow_id (required)", example: 'tracecat_create_webhook_key({ workflow_id: "wf_xxx" })' },
|
|
335
|
+
],
|
|
336
|
+
system: [
|
|
337
|
+
{ name: "tracecat_health_check", description: "Check if Tracecat API is healthy", params: "(none)", example: "tracecat_health_check()" },
|
|
338
|
+
],
|
|
339
|
+
docs: [
|
|
340
|
+
{ name: "tracecat_docs", description: "Get inline documentation", params: "topic (required: action_types, expressions, functions, control_flow, common_mistakes)", example: 'tracecat_docs({ topic: "expressions" })' },
|
|
341
|
+
{ name: "tracecat_tools_documentation", description: "Get documentation of all MCP tools grouped by category", params: "category (optional)", example: 'tracecat_tools_documentation({ category: "workflows" })' },
|
|
342
|
+
],
|
|
343
|
+
templates: [
|
|
344
|
+
{ name: "tracecat_list_templates", description: "List available SOAR workflow templates", params: "(none)", example: "tracecat_list_templates()" },
|
|
345
|
+
{ name: "tracecat_get_template", description: "Get full YAML template for a workflow pattern", params: "template_id (required)", example: 'tracecat_get_template({ template_id: "alert_triage" })' },
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
export function registerDocTools(server, _client) {
|
|
349
|
+
server.tool("tracecat_docs", "Get inline documentation about Tracecat concepts. Topics: action_types, expressions, functions, control_flow, common_mistakes.", {
|
|
350
|
+
topic: z.enum(["action_types", "expressions", "functions", "control_flow", "common_mistakes"])
|
|
351
|
+
.describe("Documentation topic to retrieve"),
|
|
352
|
+
}, async ({ topic }) => {
|
|
353
|
+
const doc = DOCS[topic];
|
|
354
|
+
if (!doc) {
|
|
355
|
+
return {
|
|
356
|
+
content: [{ type: "text", text: `Unknown topic: ${topic}. Available: ${Object.keys(DOCS).join(", ")}` }],
|
|
357
|
+
isError: true,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return { content: [{ type: "text", text: doc }] };
|
|
361
|
+
});
|
|
362
|
+
const categoryEnum = z.enum([
|
|
363
|
+
"workflows", "actions", "executions", "cases", "secrets",
|
|
364
|
+
"tables", "graph", "schedules", "webhooks", "system", "docs", "templates",
|
|
365
|
+
]);
|
|
366
|
+
server.tool("tracecat_tools_documentation", "Get documentation of ALL Tracecat MCP tools grouped by category. Use this to discover available tools and learn how to use them. Optionally filter by category.", {
|
|
367
|
+
category: categoryEnum.optional().describe("Filter by category. Omit to get all tools."),
|
|
368
|
+
}, async ({ category }) => {
|
|
369
|
+
const categories = category ? [category] : Object.keys(TOOLS_CATALOG);
|
|
370
|
+
const lines = ["# Tracecat MCP Tools Reference\n"];
|
|
371
|
+
for (const cat of categories) {
|
|
372
|
+
const tools = TOOLS_CATALOG[cat];
|
|
373
|
+
if (!tools)
|
|
374
|
+
continue;
|
|
375
|
+
lines.push(`## ${cat.charAt(0).toUpperCase() + cat.slice(1)} (${tools.length} tools)\n`);
|
|
376
|
+
for (const t of tools) {
|
|
377
|
+
lines.push(`### ${t.name}`);
|
|
378
|
+
lines.push(`${t.description}`);
|
|
379
|
+
lines.push(`**Parameters:** ${t.params}`);
|
|
380
|
+
lines.push(`**Example:** \`${t.example}\`\n`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const total = Object.values(TOOLS_CATALOG).reduce((sum, arr) => sum + arr.length, 0);
|
|
384
|
+
if (!category) {
|
|
385
|
+
lines.push(`---\n**Total: ${total} tools across ${Object.keys(TOOLS_CATALOG).length} categories**`);
|
|
386
|
+
}
|
|
387
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
388
|
+
});
|
|
389
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerExecutionTools(server, client) {
|
|
3
|
+
server.tool("tracecat_run_workflow", "Execute a workflow by ID with optional input payload", {
|
|
4
|
+
workflow_id: z.string().describe("Workflow ID"),
|
|
5
|
+
payload: z.record(z.unknown()).optional().describe("Input payload for the workflow execution"),
|
|
6
|
+
}, async ({ workflow_id, payload }) => {
|
|
7
|
+
const result = await client.post(`/workflows/${workflow_id}/execute`, payload ?? {});
|
|
8
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
9
|
+
});
|
|
10
|
+
server.tool("tracecat_list_executions", "List workflow executions, optionally filtered by workflow ID", {
|
|
11
|
+
workflow_id: z.string().optional().describe("Filter by workflow ID"),
|
|
12
|
+
limit: z.number().optional().describe("Maximum number of results"),
|
|
13
|
+
}, async ({ workflow_id, limit }) => {
|
|
14
|
+
const params = {};
|
|
15
|
+
if (workflow_id)
|
|
16
|
+
params.workflow_id = workflow_id;
|
|
17
|
+
if (limit)
|
|
18
|
+
params.limit = limit.toString();
|
|
19
|
+
const executions = await client.get("/workflow-executions", params);
|
|
20
|
+
return { content: [{ type: "text", text: JSON.stringify(executions, null, 2) }] };
|
|
21
|
+
});
|
|
22
|
+
server.tool("tracecat_get_execution", "Get details of a specific workflow execution", {
|
|
23
|
+
execution_id: z.string().describe("Execution ID"),
|
|
24
|
+
}, async ({ execution_id }) => {
|
|
25
|
+
const execution = await client.get(`/workflow-executions/${execution_id}`);
|
|
26
|
+
return { content: [{ type: "text", text: JSON.stringify(execution, null, 2) }] };
|
|
27
|
+
});
|
|
28
|
+
server.tool("tracecat_cancel_execution", "Cancel a running workflow execution", {
|
|
29
|
+
execution_id: z.string().describe("Execution ID"),
|
|
30
|
+
}, async ({ execution_id }) => {
|
|
31
|
+
const result = await client.post(`/workflow-executions/${execution_id}/cancel`);
|
|
32
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
33
|
+
});
|
|
34
|
+
server.tool("tracecat_get_execution_compact", "Get compact execution details (lighter response with action-level status, inputs, results, errors)", {
|
|
35
|
+
execution_id: z.string().describe("Execution ID"),
|
|
36
|
+
}, async ({ execution_id }) => {
|
|
37
|
+
const execution = await client.get(`/workflow-executions/${execution_id}/compact`);
|
|
38
|
+
return { content: [{ type: "text", text: JSON.stringify(execution, null, 2) }] };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerGraphTools(server, client) {
|
|
3
|
+
server.tool("tracecat_get_graph", "Get the workflow graph (nodes, edges, positions, version). Use this to inspect the visual layout and connections between actions.", {
|
|
4
|
+
workflow_id: z.string().describe("Workflow ID"),
|
|
5
|
+
}, async ({ workflow_id }) => {
|
|
6
|
+
const graph = await client.get(`/workflows/${workflow_id}/graph`);
|
|
7
|
+
return { content: [{ type: "text", text: JSON.stringify(graph, null, 2) }] };
|
|
8
|
+
});
|
|
9
|
+
server.tool("tracecat_add_edges", "Add connections (edges) between actions in a workflow graph. source_type is 'trigger' for the trigger node or 'udf' for actions. source_handle can be 'success', 'error', or null (default).", {
|
|
10
|
+
workflow_id: z.string().describe("Workflow ID"),
|
|
11
|
+
edges: z.array(z.object({
|
|
12
|
+
source_id: z.string().describe("Source node ID (trigger ID or action ID)"),
|
|
13
|
+
source_type: z.enum(["trigger", "udf"]).describe("'trigger' for trigger node, 'udf' for action nodes"),
|
|
14
|
+
target_id: z.string().describe("Target action ID"),
|
|
15
|
+
source_handle: z.string().nullable().optional().describe("'success', 'error', or null (default)"),
|
|
16
|
+
})).describe("Array of edges to add"),
|
|
17
|
+
}, async ({ workflow_id, edges }) => {
|
|
18
|
+
const operations = edges.map((edge) => ({
|
|
19
|
+
type: "add_edge",
|
|
20
|
+
payload: {
|
|
21
|
+
source_id: edge.source_id,
|
|
22
|
+
source_type: edge.source_type,
|
|
23
|
+
target_id: edge.target_id,
|
|
24
|
+
source_handle: edge.source_handle ?? null,
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
const result = await client.patchGraph(workflow_id, operations);
|
|
28
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
29
|
+
});
|
|
30
|
+
server.tool("tracecat_delete_edges", "Remove connections (edges) between actions in a workflow graph.", {
|
|
31
|
+
workflow_id: z.string().describe("Workflow ID"),
|
|
32
|
+
edges: z.array(z.object({
|
|
33
|
+
source_id: z.string().describe("Source node ID"),
|
|
34
|
+
source_type: z.enum(["trigger", "udf"]).describe("'trigger' or 'udf'"),
|
|
35
|
+
target_id: z.string().describe("Target action ID"),
|
|
36
|
+
})).describe("Array of edges to delete"),
|
|
37
|
+
}, async ({ workflow_id, edges }) => {
|
|
38
|
+
const operations = edges.map((edge) => ({
|
|
39
|
+
type: "delete_edge",
|
|
40
|
+
payload: {
|
|
41
|
+
source_id: edge.source_id,
|
|
42
|
+
source_type: edge.source_type,
|
|
43
|
+
target_id: edge.target_id,
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
const result = await client.patchGraph(workflow_id, operations);
|
|
47
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
48
|
+
});
|
|
49
|
+
server.tool("tracecat_move_nodes", "Reposition action nodes in the workflow graph. Recommended layout: trigger at (500,0), first action at y>=300, 160px vertical spacing, 320px horizontal spacing for parallel nodes.", {
|
|
50
|
+
workflow_id: z.string().describe("Workflow ID"),
|
|
51
|
+
positions: z.array(z.object({
|
|
52
|
+
action_id: z.string().describe("Action ID to reposition"),
|
|
53
|
+
x: z.number().describe("X coordinate"),
|
|
54
|
+
y: z.number().describe("Y coordinate"),
|
|
55
|
+
})).describe("Array of node positions"),
|
|
56
|
+
}, async ({ workflow_id, positions }) => {
|
|
57
|
+
const operations = [{
|
|
58
|
+
type: "move_nodes",
|
|
59
|
+
payload: {
|
|
60
|
+
positions: positions.map((p) => ({
|
|
61
|
+
action_id: p.action_id,
|
|
62
|
+
x: p.x,
|
|
63
|
+
y: p.y,
|
|
64
|
+
})),
|
|
65
|
+
},
|
|
66
|
+
}];
|
|
67
|
+
const result = await client.patchGraph(workflow_id, operations);
|
|
68
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
69
|
+
});
|
|
70
|
+
server.tool("tracecat_update_trigger_position", "Reposition the trigger node in the workflow graph. Default recommended position is (500, 0).", {
|
|
71
|
+
workflow_id: z.string().describe("Workflow ID"),
|
|
72
|
+
x: z.number().describe("X coordinate"),
|
|
73
|
+
y: z.number().describe("Y coordinate"),
|
|
74
|
+
}, async ({ workflow_id, x, y }) => {
|
|
75
|
+
const operations = [{
|
|
76
|
+
type: "update_trigger_position",
|
|
77
|
+
payload: { x, y },
|
|
78
|
+
}];
|
|
79
|
+
const result = await client.patchGraph(workflow_id, operations);
|
|
80
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
81
|
+
});
|
|
82
|
+
}
|