@pmoses-s1/sentinelone-mcp 1.2.1 → 1.2.2
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/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/deploy/README.md +2 -2
- package/lib/server-core.js +3 -3
- package/package.json +1 -1
- package/scripts/regen-readme-tools-table.mjs +1 -1
- package/scripts/test-mac.sh +5 -5
- package/tools/hyperautomation.js +55 -21
- package/tools/powerquery.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.2 - 2026-06-13
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Renamed `ha_archive_workflow` to `ha_delete_workflow`.** The old tool hit `POST /hyper-automate/api/v1/workflows/archive`, which returns HTTP 500 on this tenant. The replacement uses the validated `DELETE /hyper-automate/api/v1/workflows/{id}` endpoint (a soft, recoverable delete equivalent to clicking Delete in the Hyperautomation UI). Scope the call with `accountIds` or `siteIds`; a 404 "Object not found" means the id is not under that scope or is already deleted. Updated `README.md`, the tools-table regenerator, and the smoke test in lockstep.
|
|
7
|
+
- **`powerquery_run` description now documents the `datasource` and `savelookup` capabilities** (querying SentinelOne-managed inventory such as assets/alerts/vulnerabilities/misconfigurations, and persisting a result as a reusable lookup table), pointing at the new `sentinelone-powerquery/references/datasource-command.md`.
|
|
8
|
+
|
|
9
|
+
### Notes
|
|
10
|
+
- Tool count unchanged at 26 (the Hyperautomation tool was renamed, not added or removed).
|
|
11
|
+
- `SERVER_INFO.version` bumped in lockstep with `package.json` (the drift that forced the 1.2.0 -> 1.2.1 re-release).
|
|
12
|
+
|
|
3
13
|
## 1.2.1 - 2026-06-11
|
|
4
14
|
|
|
5
15
|
Supersedes 1.2.0, which was deprecated on npm. The 1.2.0 build shipped with a stale internal `SERVER_INFO.version` of `1.1.0` despite a `1.2.0` package version, so the server announced the wrong version on `initialize`. 1.2.1 is identical in features and corrects the reported runtime version. The content below is unchanged from the 1.2.0 work.
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ See **[deploy/README.md](./deploy/README.md)** for the full deployment walkthrou
|
|
|
32
32
|
| SDL API | `sdl_list_files` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
|
|
33
33
|
| SDL API | `sdl_put_file` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
|
|
34
34
|
| SDL API | `hec_ingest` | sentinelone-sdl-api / sdl-log-parser |
|
|
35
|
-
| Hyperautomation | `
|
|
35
|
+
| Hyperautomation | `ha_delete_workflow` | sentinelone-hyperautomation |
|
|
36
36
|
| Hyperautomation | `ha_export_workflow` | sentinelone-hyperautomation |
|
|
37
37
|
| Hyperautomation | `ha_get_workflow` | sentinelone-hyperautomation |
|
|
38
38
|
| Hyperautomation | `ha_import_workflow` | sentinelone-hyperautomation |
|
|
@@ -65,7 +65,7 @@ Add this to `claude_desktop_config.json` (or `.mcp.json` for Claude Code):
|
|
|
65
65
|
"mcpServers": {
|
|
66
66
|
"sentinelone-mcp": {
|
|
67
67
|
"command": "npx",
|
|
68
|
-
"args": ["-y", "@pmoses-s1/sentinelone-mcp@1.
|
|
68
|
+
"args": ["-y", "@pmoses-s1/sentinelone-mcp@1.2.2"],
|
|
69
69
|
"env": {
|
|
70
70
|
"S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
|
|
71
71
|
"S1_CONSOLE_API_TOKEN": "eyJ...",
|
|
@@ -463,7 +463,7 @@ sentinelone-mcp/
|
|
|
463
463
|
powerquery.js PowerQuery enumerate/run/schema-discover
|
|
464
464
|
mgmt-console.js S1 REST verbs + Purple AI summary + UAM
|
|
465
465
|
sdl-api.js SDL config file + log ingestion tools
|
|
466
|
-
hyperautomation.js Hyperautomation list/get/import/export/
|
|
466
|
+
hyperautomation.js Hyperautomation list/get/import/export/delete
|
|
467
467
|
uam-ingest.js UAM Alert Interface ingestion tools
|
|
468
468
|
deploy/
|
|
469
469
|
install.sh One-shot installer (Mac and Linux)
|
package/deploy/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Or, equivalently, by package name without the install:
|
|
|
52
52
|
"mcpServers": {
|
|
53
53
|
"sentinelone-mcp": {
|
|
54
54
|
"command": "npx",
|
|
55
|
-
"args": ["-y", "@pmoses-s1/sentinelone-mcp@1.
|
|
55
|
+
"args": ["-y", "@pmoses-s1/sentinelone-mcp@1.2.2"]
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -359,7 +359,7 @@ Both block the W+X memory mappings V8 needs to JIT JavaScript. Adding them cause
|
|
|
359
359
|
|
|
360
360
|
These are supported but not first-class:
|
|
361
361
|
|
|
362
|
-
- **Docker / docker-compose.** Not shipped in this version. The single-file Node binary doesn't need it. If you want a container, the install is `FROM node:20-alpine` + `RUN npm install -g @pmoses-s1/sentinelone-mcp@1.
|
|
362
|
+
- **Docker / docker-compose.** Not shipped in this version. The single-file Node binary doesn't need it. If you want a container, the install is `FROM node:20-alpine` + `RUN npm install -g @pmoses-s1/sentinelone-mcp@1.2.2` + `CMD ["sentinelone-mcp", "--transport", "http", "--host", "0.0.0.0"]`. Mount creds at `/etc/sentinelone-mcp/credentials.json` and tokens at `/etc/sentinelone-mcp/bearer-tokens.json`.
|
|
363
363
|
|
|
364
364
|
- **External bridge (`supergateway`, `mcp-proxy`).** Pre-1.1.0 deployments used these to wrap the stdio-only server. They still work; this server's native HTTP mode is functionally equivalent and removes the extra process. Prefer native unless you have a specific reason.
|
|
365
365
|
|
package/lib/server-core.js
CHANGED
|
@@ -94,7 +94,7 @@ const PROMPTS = [
|
|
|
94
94
|
|
|
95
95
|
export const SERVER_INFO = {
|
|
96
96
|
name: 'sentinelone-mcp-server',
|
|
97
|
-
version: '1.2.
|
|
97
|
+
version: '1.2.2',
|
|
98
98
|
};
|
|
99
99
|
|
|
100
100
|
export const PROTOCOL_VERSION = '2024-11-05';
|
|
@@ -203,9 +203,9 @@ export async function dispatch(method, params, id) {
|
|
|
203
203
|
text: `Begin a new SOC analyst session. Follow this initialization sequence:
|
|
204
204
|
|
|
205
205
|
1. Call \`powerquery_enumerate_sources\` to discover active SDL data sources (MANDATORY, never assume sources from prior sessions).
|
|
206
|
-
2. In parallel, call \`uam_list_alerts\` with
|
|
206
|
+
2. In parallel, call \`uam_list_alerts\` with status="NEW" to pull untriaged active alerts. Valid status values are "NEW", "IN_PROGRESS", or "RESOLVED" (there is no "OPEN" status; it silently returns 0 results). Omit the status argument to pull the most recent alerts across all states.
|
|
207
207
|
3. For each discovered data source not already in the schema registry, plan schema discovery via \`powerquery_schema_discover\`.
|
|
208
|
-
4. Report: (a) active data sources list, (b)
|
|
208
|
+
4. Report: (a) active data sources list, (b) untriaged (NEW) alert count and top 5 by severity, (c) which sources need schema discovery.
|
|
209
209
|
|
|
210
210
|
Apply the SOC analyst context from the soc_analyst prompt throughout.`,
|
|
211
211
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmoses-s1/sentinelone-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "MCP server orchestrating SentinelOne skills, APIs, and SOC analyst context. Stdio or Streamable HTTP transport with per-user bearer auth for team deployments.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -48,7 +48,7 @@ const TOOL_SKILL = {
|
|
|
48
48
|
// Hyperautomation
|
|
49
49
|
ha_list_workflows: 'sentinelone-hyperautomation',
|
|
50
50
|
ha_get_workflow: 'sentinelone-hyperautomation',
|
|
51
|
-
|
|
51
|
+
ha_delete_workflow: 'sentinelone-hyperautomation',
|
|
52
52
|
ha_import_workflow: 'sentinelone-hyperautomation',
|
|
53
53
|
ha_export_workflow: 'sentinelone-hyperautomation',
|
|
54
54
|
// UAM Ingest
|
package/scripts/test-mac.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# Mac validation script for sentinelone-mcp v1.
|
|
3
|
+
# Mac validation script for sentinelone-mcp v1.2.2.
|
|
4
4
|
#
|
|
5
5
|
# Runs the same test matrix as Linux:
|
|
6
6
|
# - syntax-check every .js/.mjs file
|
|
@@ -100,12 +100,12 @@ fi
|
|
|
100
100
|
# ─── 5. CLI flag sanity ───────────────────────────────────────────────────────
|
|
101
101
|
|
|
102
102
|
step "5. CLI flag sanity"
|
|
103
|
-
if [[ "$(node index.js --version 2>/dev/null)" == "1.
|
|
104
|
-
pass "--version returns 1.
|
|
103
|
+
if [[ "$(node index.js --version 2>/dev/null)" == "1.2.2" ]]; then
|
|
104
|
+
pass "--version returns 1.2.2"
|
|
105
105
|
else
|
|
106
|
-
fail "--version did not return 1.
|
|
106
|
+
fail "--version did not return 1.2.2" "Got: $(node index.js --version 2>&1)"
|
|
107
107
|
fi
|
|
108
|
-
if node index.js --help 2>&1 | grep -q "sentinelone-mcp 1.
|
|
108
|
+
if node index.js --help 2>&1 | grep -q "sentinelone-mcp 1.2.2"; then
|
|
109
109
|
pass "--help renders"
|
|
110
110
|
else
|
|
111
111
|
fail "--help broken" "$(node index.js --help 2>&1 | head -5)"
|
package/tools/hyperautomation.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Tools:
|
|
5
5
|
* ha_list_workflows List Hyperautomation workflows (with scope/state/sort filters)
|
|
6
6
|
* ha_get_workflow Get a single workflow by ID (+ optional revisionId)
|
|
7
|
-
*
|
|
8
|
-
* ha_import_workflow Import (create) a workflow from JSON
|
|
7
|
+
* ha_delete_workflow Delete (soft, recoverable) a workflow via REST DELETE
|
|
8
|
+
* ha_import_workflow Import (create) a workflow from JSON (account- or site-scoped)
|
|
9
9
|
* ha_export_workflow Export all workflows as a ZIP archive
|
|
10
10
|
*
|
|
11
11
|
* API root (confirmed via live network capture 2026-05-03):
|
|
@@ -15,14 +15,15 @@
|
|
|
15
15
|
* GET /workflows/single/{workflowId}/{revisionId}
|
|
16
16
|
* The revisionId is workflow.version_id in the list response.
|
|
17
17
|
*
|
|
18
|
-
* Deletion is a soft
|
|
19
|
-
*
|
|
18
|
+
* Deletion is a soft, recoverable HTTP DELETE (validated 2026-06-13):
|
|
19
|
+
* DELETE /workflows/{id}?accountIds=<acct> (or ?siteIds=<site>)
|
|
20
|
+
* The older POST /workflows/archive returns 500 on this tenant — do not use it.
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* Import scope: the public import endpoint accepts ?accountIds=<acct> or
|
|
23
|
+
* ?siteIds=<site>; with no scope it returns a misleading 403 on a scoped tenant.
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
|
-
import { apiGet, apiPost } from '../lib/s1.js';
|
|
26
|
+
import { apiGet, apiPost, apiDelete } from '../lib/s1.js';
|
|
26
27
|
|
|
27
28
|
// Confirmed base path from live network monitor (2026-05-03).
|
|
28
29
|
const HA_BASE = '/web/api/v2.1/hyper-automate/api/v1';
|
|
@@ -156,36 +157,57 @@ export const tools = [
|
|
|
156
157
|
},
|
|
157
158
|
},
|
|
158
159
|
|
|
159
|
-
// ───
|
|
160
|
+
// ─── ha_delete_workflow ───────────────────────────────────────────────────
|
|
160
161
|
{
|
|
161
|
-
name: '
|
|
162
|
-
description: `
|
|
162
|
+
name: 'ha_delete_workflow',
|
|
163
|
+
description: `Delete one or more Hyperautomation workflows. Uses the REST DELETE /hyper-automate/api/v1/workflows/{id} endpoint (validated 2026-06-13) — a soft, recoverable delete (the console offers a "Restore workflow" action), equivalent to clicking Delete in the Hyperautomation UI. Scope the call to where the workflow lives with accountIds (account-scoped workflow) or siteIds (site-scoped); a 404 "Object not found" means the id is not under that scope or is already deleted. Requires Hyper Automate.write permission. NOTE: do NOT use the older POST /workflows/archive path — it returns 500 on this tenant.`,
|
|
163
164
|
inputSchema: {
|
|
164
165
|
type: 'object',
|
|
165
166
|
properties: {
|
|
166
167
|
workflowIds: {
|
|
167
168
|
type: 'array',
|
|
168
169
|
items: { type: 'string' },
|
|
169
|
-
description: 'One or more workflow UUIDs to
|
|
170
|
+
description: 'One or more workflow UUIDs to delete (from ha_list_workflows).',
|
|
171
|
+
},
|
|
172
|
+
accountIds: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description: 'Account scope for an account-level workflow (e.g. "2046190533732727925"). Provide this OR siteIds.',
|
|
175
|
+
},
|
|
176
|
+
siteIds: {
|
|
177
|
+
type: 'string',
|
|
178
|
+
description: 'Site scope for a site-level workflow. Provide this OR accountIds.',
|
|
170
179
|
},
|
|
171
180
|
},
|
|
172
181
|
required: ['workflowIds'],
|
|
173
182
|
},
|
|
174
|
-
async handler({ workflowIds }) {
|
|
183
|
+
async handler({ workflowIds, accountIds, siteIds } = {}) {
|
|
175
184
|
if (!Array.isArray(workflowIds) || workflowIds.length === 0) {
|
|
176
185
|
return JSON.stringify({ error: 'workflowIds must be a non-empty array of UUIDs.' });
|
|
177
186
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
if (!accountIds && !siteIds) {
|
|
188
|
+
return JSON.stringify({ error: 'Provide accountIds or siteIds to scope the delete to where the workflow lives (a workflow created at a site must be deleted with siteIds; an account-scoped one with accountIds).' });
|
|
189
|
+
}
|
|
190
|
+
const scope = accountIds
|
|
191
|
+
? `accountIds=${encodeURIComponent(accountIds)}`
|
|
192
|
+
: `siteIds=${encodeURIComponent(siteIds)}`;
|
|
193
|
+
const results = [];
|
|
194
|
+
for (const id of workflowIds) {
|
|
195
|
+
try {
|
|
196
|
+
// DELETE returns 204 No Content on success.
|
|
197
|
+
await apiDelete(`${HA_BASE}/workflows/${encodeURIComponent(id)}?${scope}`);
|
|
198
|
+
results.push({ id, status: 'deleted' });
|
|
199
|
+
} catch (e) {
|
|
200
|
+
results.push({ id, status: 'error', error: e.message });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return JSON.stringify({ deleted: results }, null, 2);
|
|
182
204
|
},
|
|
183
205
|
},
|
|
184
206
|
|
|
185
207
|
// ─── ha_import_workflow ───────────────────────────────────────────────────
|
|
186
208
|
{
|
|
187
209
|
name: 'ha_import_workflow',
|
|
188
|
-
description: `Import a Hyperautomation workflow JSON into the SentinelOne console. The workflow JSON must follow the Hyperautomation schema (use the sentinelone-hyperautomation skill to generate valid JSON). Integration-backed actions (type=http_request with an integration_id) require pre-configured connections in Hyperautomation > Integrations before the workflow will run
|
|
210
|
+
description: `Import a Hyperautomation workflow JSON into the SentinelOne console. Scope the import with accountIds (account-level) or siteIds (site-level); on a scoped tenant a bare import with no scope returns a misleading 403 "Insufficient permissions". The workflow JSON must follow the Hyperautomation schema (use the sentinelone-hyperautomation skill to generate valid JSON). Integration-backed actions (type=http_request with an integration_id) require pre-configured connections in Hyperautomation > Integrations before the workflow will run, and an imported flow lands as a private draft until published (publish endpoint) or activated. Returns the created workflow ID on success. Requires Hyper Automate.write permission.`,
|
|
189
211
|
inputSchema: {
|
|
190
212
|
type: 'object',
|
|
191
213
|
properties: {
|
|
@@ -193,19 +215,31 @@ export const tools = [
|
|
|
193
215
|
type: 'string',
|
|
194
216
|
description: 'Full Hyperautomation workflow JSON as a string. Must be valid Hyperautomation schema. Generate this using the sentinelone-hyperautomation skill.',
|
|
195
217
|
},
|
|
218
|
+
accountIds: {
|
|
219
|
+
type: 'string',
|
|
220
|
+
description: 'Account scope for an account-level import (e.g. "2046190533732727925"). Provide this OR siteIds.',
|
|
221
|
+
},
|
|
222
|
+
siteIds: {
|
|
223
|
+
type: 'string',
|
|
224
|
+
description: 'Site scope for a site-level import. Provide this OR accountIds.',
|
|
225
|
+
},
|
|
196
226
|
},
|
|
197
227
|
required: ['workflowJson'],
|
|
198
228
|
},
|
|
199
|
-
async handler({ workflowJson }) {
|
|
229
|
+
async handler({ workflowJson, accountIds, siteIds }) {
|
|
200
230
|
let parsed;
|
|
201
231
|
try {
|
|
202
232
|
parsed = JSON.parse(workflowJson);
|
|
203
233
|
} catch (e) {
|
|
204
234
|
return JSON.stringify({ error: `Invalid JSON: ${e.message}` });
|
|
205
235
|
}
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
236
|
+
// Public import endpoint. Append the scope query param: account-level imports use
|
|
237
|
+
// ?accountIds=, site-level use ?siteIds=. A bare import (no scope) returns a misleading
|
|
238
|
+
// 403 on a scoped tenant (validated 2026-06-13).
|
|
239
|
+
let qs = '';
|
|
240
|
+
if (accountIds) qs = `?accountIds=${encodeURIComponent(accountIds)}`;
|
|
241
|
+
else if (siteIds) qs = `?siteIds=${encodeURIComponent(siteIds)}`;
|
|
242
|
+
const result = await apiPost(`${HA_PUBLIC}/workflow-import-export/import${qs}`, { data: parsed });
|
|
209
243
|
return JSON.stringify(result, null, 2);
|
|
210
244
|
},
|
|
211
245
|
},
|
package/tools/powerquery.js
CHANGED
|
@@ -45,7 +45,7 @@ export const tools = [
|
|
|
45
45
|
properties: {
|
|
46
46
|
query: {
|
|
47
47
|
type: 'string',
|
|
48
|
-
description: 'The PowerQuery string. Use pipe-separated commands: | filter | group | sort | limit | columns. Three distinct wildcard idioms — use the right one: (1) FIELD PRESENCE / ATTRIBUTE WILDCARD: field=* means "field is present/non-null", e.g. dataSource.name=* | group count=count() by dataSource.name — use this as a query-opener or whenever you need "all events that have this field". (2) ALL-COLUMN TEXT SEARCH: * contains \'value\' or * matches \'regex\' in the initial filter (before the first |) searches ALL indexed fields — use when the user asks to find text anywhere in the event, e.g. dataSource.name=\'MySource\' * contains \'evil.com\'. Dramatically faster than message contains. (3) EMPTY FILTER (all events): start with | and no initial predicate, e.g. | group ct=count() by event.type. Do NOT use bare * alone as the initial filter — that causes HTTP 500 ("Don\'t understand [*]").',
|
|
48
|
+
description: 'The PowerQuery string. Use pipe-separated commands: | filter | group | sort | limit | columns. Three distinct wildcard idioms — use the right one: (1) FIELD PRESENCE / ATTRIBUTE WILDCARD: field=* means "field is present/non-null", e.g. dataSource.name=* | group count=count() by dataSource.name — use this as a query-opener or whenever you need "all events that have this field". (2) ALL-COLUMN TEXT SEARCH: * contains \'value\' or * matches \'regex\' in the initial filter (before the first |) searches ALL indexed fields — use when the user asks to find text anywhere in the event, e.g. dataSource.name=\'MySource\' * contains \'evil.com\'. Dramatically faster than message contains. (3) EMPTY FILTER (all events): start with | and no initial predicate, e.g. | group ct=count() by event.type. Do NOT use bare * alone as the initial filter — that causes HTTP 500 ("Don\'t understand [*]"). Beyond filtering, | datasource <name> [from <dataset>] reads SentinelOne-managed inventory (assets, alerts, vulnerabilities, misconfigurations, metering; e.g. | datasource assets from \'surface/identity\') and | savelookup \'<name>\' persists the result as a reusable lookup table (see references/datasource-command.md in sentinelone-powerquery).',
|
|
49
49
|
},
|
|
50
50
|
startTime: {
|
|
51
51
|
type: 'string',
|