@pmoses-s1/sentinelone-mcp 1.0.0 → 1.2.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/CHANGELOG.md +60 -0
- package/README.md +422 -102
- package/deploy/README.md +366 -0
- package/deploy/bridge/README.md +93 -0
- package/deploy/bridge/sentinelone-mcp-bridge.mjs +104 -0
- package/deploy/caddy/Caddyfile.example +110 -0
- package/deploy/install.sh +264 -0
- package/deploy/systemd/sentinelone-mcp.service +58 -0
- package/index.js +135 -312
- package/lib/auth.js +161 -0
- package/lib/credentials.js +18 -7
- package/lib/hec.js +128 -0
- package/lib/http-transport.js +223 -0
- package/lib/s1.js +1 -1
- package/lib/sdl.js +0 -32
- package/lib/server-core.js +264 -0
- package/lib/stdio-transport.js +77 -0
- package/lib/uam-ingest.js +1 -1
- package/package.json +10 -4
- package/scripts/regen-readme-tools-table.mjs +142 -0
- package/scripts/smoke-test-http.sh +122 -0
- package/scripts/test-mac.sh +179 -0
- package/tools/hyperautomation.js +1 -1
- package/tools/mgmt-console.js +30 -3
- package/tools/sdl-api.js +16 -24
package/README.md
CHANGED
|
@@ -1,180 +1,478 @@
|
|
|
1
1
|
# SentinelOne MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Model Context Protocol server orchestrating the SentinelOne Management Console, Singularity Data Lake, UAM Alert Interface, and Hyperautomation APIs. Pure Node.js 18+, zero external dependencies. Supports both stdio (for Claude Desktop / Cowork / Claude Code) and Streamable HTTP (for team-shared VM deployments) transports.
|
|
4
|
+
|
|
5
|
+
- **Single-user, local:** install with `npx`, plug into Claude Desktop in 30 seconds.
|
|
6
|
+
- **Team, VM-hosted:** install on one Linux box, per-user bearer tokens, audit logs, SIGHUP-reloadable rotation.
|
|
7
|
+
|
|
8
|
+
See **[deploy/README.md](./deploy/README.md)** for the full deployment walkthrough across all three topologies.
|
|
4
9
|
|
|
5
10
|
## What this exposes
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
<!-- BEGIN AUTO-GENERATED TOOLS TABLE -->
|
|
13
|
+
**26 tools** across PowerQuery, Mgmt Console, SDL API, Hyperautomation, and UAM Ingest:
|
|
8
14
|
|
|
9
15
|
| Group | Tool | Skill |
|
|
10
16
|
|-------|------|-------|
|
|
11
17
|
| PowerQuery | `powerquery_enumerate_sources` | sentinelone-powerquery |
|
|
12
18
|
| PowerQuery | `powerquery_run` | sentinelone-powerquery |
|
|
13
19
|
| PowerQuery | `powerquery_schema_discover` | sentinelone-powerquery |
|
|
20
|
+
| Mgmt Console | `purple_ai_alert_summary` | sentinelone-mgmt-console-api |
|
|
21
|
+
| Mgmt Console | `s1_api_delete` | sentinelone-mgmt-console-api |
|
|
14
22
|
| Mgmt Console | `s1_api_get` | sentinelone-mgmt-console-api |
|
|
23
|
+
| Mgmt Console | `s1_api_patch` | sentinelone-mgmt-console-api |
|
|
15
24
|
| Mgmt Console | `s1_api_post` | sentinelone-mgmt-console-api |
|
|
16
|
-
| Mgmt Console | `
|
|
17
|
-
| Mgmt Console | `uam_list_alerts` | sentinelone-mgmt-console-api |
|
|
18
|
-
| Mgmt Console | `uam_get_alert` | sentinelone-mgmt-console-api |
|
|
25
|
+
| Mgmt Console | `s1_api_put` | sentinelone-mgmt-console-api |
|
|
19
26
|
| Mgmt Console | `uam_add_note` | sentinelone-mgmt-console-api |
|
|
27
|
+
| Mgmt Console | `uam_get_alert` | sentinelone-mgmt-console-api |
|
|
28
|
+
| Mgmt Console | `uam_list_alerts` | sentinelone-mgmt-console-api |
|
|
20
29
|
| Mgmt Console | `uam_set_status` | sentinelone-mgmt-console-api |
|
|
21
|
-
| SDL API | `
|
|
30
|
+
| SDL API | `sdl_delete_file` | sentinelone-sdl-api |
|
|
22
31
|
| SDL API | `sdl_get_file` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
|
|
32
|
+
| SDL API | `sdl_list_files` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
|
|
23
33
|
| SDL API | `sdl_put_file` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
|
|
24
|
-
| SDL API | `
|
|
25
|
-
|
|
|
26
|
-
| Hyperautomation | `
|
|
34
|
+
| SDL API | `hec_ingest` | sentinelone-sdl-api / sdl-log-parser |
|
|
35
|
+
| Hyperautomation | `ha_archive_workflow` | sentinelone-hyperautomation |
|
|
36
|
+
| Hyperautomation | `ha_export_workflow` | sentinelone-hyperautomation |
|
|
27
37
|
| Hyperautomation | `ha_get_workflow` | sentinelone-hyperautomation |
|
|
28
38
|
| Hyperautomation | `ha_import_workflow` | sentinelone-hyperautomation |
|
|
29
|
-
| Hyperautomation | `
|
|
39
|
+
| Hyperautomation | `ha_list_workflows` | sentinelone-hyperautomation |
|
|
40
|
+
| UAM Ingest | `uam_ingest_alert` | sentinelone-mgmt-console-api (UAM Alert Interface) |
|
|
41
|
+
| UAM Ingest | `uam_post_alert` | sentinelone-mgmt-console-api (UAM Alert Interface) |
|
|
42
|
+
| UAM Ingest | `uam_post_indicators` | sentinelone-mgmt-console-api (UAM Alert Interface) |
|
|
43
|
+
<!-- END AUTO-GENERATED TOOLS TABLE -->
|
|
30
44
|
|
|
31
45
|
**2 resources:**
|
|
32
|
-
- `sentinelone://soc-context
|
|
33
|
-
- `sentinelone://credentials-status
|
|
46
|
+
- `sentinelone://soc-context` — `CLAUDE.md`, the Principal SOC Analyst operating instructions.
|
|
47
|
+
- `sentinelone://credentials-status` — which credentials are configured and which API surfaces are available.
|
|
34
48
|
|
|
35
49
|
**2 prompts:**
|
|
36
|
-
- `soc_analyst
|
|
37
|
-
- `session_init
|
|
50
|
+
- `soc_analyst` — embeds `CLAUDE.md` as a system prompt; call at session start.
|
|
51
|
+
- `session_init` — structured init: enumerate sources + triage alerts in parallel.
|
|
38
52
|
|
|
39
|
-
##
|
|
53
|
+
## Quick install
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
- No `npm install` needed: zero external dependencies
|
|
55
|
+
Three paths, pick the one that matches your setup:
|
|
43
56
|
|
|
44
|
-
|
|
57
|
+
### A. Local single-user via `npx` (Claude Desktop / Claude Code / Cowork)
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
MCP runs as a subprocess on your machine, talking SentinelOne APIs directly. Credentials live in the Claude config `env` block.
|
|
47
60
|
|
|
48
|
-
|
|
61
|
+
Add this to `claude_desktop_config.json` (or `.mcp.json` for Claude Code):
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"sentinelone-mcp": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "@pmoses-s1/sentinelone-mcp@1.1.0"],
|
|
69
|
+
"env": {
|
|
70
|
+
"S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
|
|
71
|
+
"S1_CONSOLE_API_TOKEN": "eyJ...",
|
|
72
|
+
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net",
|
|
73
|
+
"SDL_XDR_URL": "https://xdr.us1.sentinelone.net",
|
|
74
|
+
"SDL_LOG_READ_KEY": "...",
|
|
75
|
+
"SDL_CONFIG_READ_KEY": "...",
|
|
76
|
+
"SDL_CONFIG_WRITE_KEY": "..."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
60
82
|
|
|
61
|
-
|
|
83
|
+
Restart Claude Desktop. `npx -y` caches the package on first launch.
|
|
62
84
|
|
|
63
|
-
|
|
64
|
-
# From the published npm package (no clone, no install)
|
|
65
|
-
npx -y @pmoses-s1/sentinelone-mcp
|
|
85
|
+
### B. Reproducible: install script
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
```bash
|
|
88
|
+
curl -fsSL https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/sentinelone-mcp/deploy/install.sh | bash
|
|
69
89
|
```
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
Sets up a per-user npm prefix if needed, installs the package, drops a credentials skeleton at `~/.config/sentinelone/credentials.json` (mode 0600), and prints the wiring instructions for Claude Desktop.
|
|
72
92
|
|
|
73
|
-
|
|
93
|
+
For VM deployments, the same script in `--server` mode does everything (system user, systemd unit, initial bearer token, service start). See [deploy/README.md](./deploy/README.md).
|
|
74
94
|
|
|
75
|
-
|
|
95
|
+
### C. Claude Desktop connecting to a team VM (stdio bridge)
|
|
76
96
|
|
|
77
|
-
|
|
97
|
+
When the MCP is running as a shared service on a Linux VM (deploy topology C in [deploy/README.md](./deploy/README.md)) and you're connecting from Claude Desktop, you need a small stdio↔HTTPS shim because Claude Desktop's stable build doesn't accept `type: "http"` configs. (Claude Cowork and Claude Code do; see "[Calling the HTTP endpoint directly](#calling-the-http-endpoint-directly)" for the native `type: "http"` form.)
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
The bridge is a 40-line zero-dependency Node script shipped at [`deploy/bridge/sentinelone-mcp-bridge.mjs`](./deploy/bridge/sentinelone-mcp-bridge.mjs).
|
|
80
100
|
|
|
81
|
-
|
|
101
|
+
Each team member installs the script once:
|
|
82
102
|
|
|
83
|
-
|
|
103
|
+
```bash
|
|
104
|
+
mkdir -p ~/.local/bin
|
|
105
|
+
curl -fsSL https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/sentinelone-mcp/deploy/bridge/sentinelone-mcp-bridge.mjs \
|
|
106
|
+
-o ~/.local/bin/sentinelone-mcp-bridge.mjs
|
|
107
|
+
chmod +x ~/.local/bin/sentinelone-mcp-bridge.mjs
|
|
108
|
+
```
|
|
84
109
|
|
|
85
|
-
|
|
110
|
+
Then adds this block to `claude_desktop_config.json`:
|
|
86
111
|
|
|
87
112
|
```json
|
|
88
113
|
{
|
|
89
114
|
"mcpServers": {
|
|
90
115
|
"sentinelone-mcp": {
|
|
91
|
-
"command": "
|
|
92
|
-
"args": ["
|
|
116
|
+
"command": "node",
|
|
117
|
+
"args": ["/Users/<you>/.local/bin/sentinelone-mcp-bridge.mjs"],
|
|
93
118
|
"env": {
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net",
|
|
97
|
-
"SDL_XDR_URL": "https://xdr.us1.sentinelone.net",
|
|
98
|
-
"SDL_LOG_WRITE_KEY": "0Z1Fy0...",
|
|
99
|
-
"SDL_LOG_READ_KEY": "0tzj...",
|
|
100
|
-
"SDL_CONFIG_WRITE_KEY": "0mXas6PD...",
|
|
101
|
-
"SDL_CONFIG_READ_KEY": "0MQTx..."
|
|
119
|
+
"MCP_URL": "https://mcp.example.internal:8764/mcp",
|
|
120
|
+
"MCP_BEARER": "<your personal bearer token>"
|
|
102
121
|
}
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
```
|
|
107
126
|
|
|
108
|
-
|
|
127
|
+
Cmd+Q and reopen Claude Desktop. SentinelOne credentials live on the VM in `/etc/sentinelone-mcp/credentials.json` — only the bearer token sits in each user's local Claude config. Full setup + smoke-test instructions at [`deploy/bridge/README.md`](./deploy/bridge/README.md).
|
|
128
|
+
|
|
129
|
+
## Credentials
|
|
130
|
+
|
|
131
|
+
`S1_CONSOLE_URL` and `S1_CONSOLE_API_TOKEN` are sufficient for the PowerQuery, Mgmt Console REST, Purple AI summary, and UAM tools (16 of the 26).
|
|
132
|
+
|
|
133
|
+
`S1_HEC_INGEST_URL` is **required** for the three UAM Ingest tools (`uam_ingest_alert`, `uam_post_indicators`, `uam_post_alert`) and for `hec_ingest`. Without it those tools error at call time; the rest still work.
|
|
134
|
+
|
|
135
|
+
`SDL_*` keys gate the SDL tools as follows:
|
|
136
|
+
|
|
137
|
+
| Variable | Description | Required for |
|
|
138
|
+
|----------|-------------|--------------|
|
|
139
|
+
| `S1_CONSOLE_URL` | Console URL, e.g. `https://usea1-acme.sentinelone.net` | All Mgmt + PowerQuery tools |
|
|
140
|
+
| `S1_CONSOLE_API_TOKEN` | Mgmt Console API token (Settings → Users → Service Users) | All Mgmt + PowerQuery + UAM tools |
|
|
141
|
+
| `S1_HEC_INGEST_URL` | HEC ingest host, e.g. `https://ingest.us1.sentinelone.net` | `uam_ingest_alert`, `uam_post_indicators`, `uam_post_alert`, `hec_ingest` |
|
|
142
|
+
| `SDL_XDR_URL` | SDL tenant URL, e.g. `https://xdr.us1.sentinelone.net` | All `sdl_*` tools and `powerquery_schema_discover` |
|
|
143
|
+
| `SDL_LOG_READ_KEY` | SDL Log Read key | SDL query operations |
|
|
144
|
+
| `SDL_CONFIG_READ_KEY` | SDL Config Read key | `sdl_list_files`, `sdl_get_file` |
|
|
145
|
+
| `SDL_CONFIG_WRITE_KEY` | SDL Config Write key | `sdl_put_file`, `sdl_delete_file` |
|
|
146
|
+
|
|
147
|
+
### Credential resolution order (highest priority wins)
|
|
148
|
+
|
|
149
|
+
1. Environment variables (set in `claude_desktop_config.json` `env`, systemd `EnvironmentFile`, or your shell).
|
|
150
|
+
2. `S1_CREDS_FILE` — explicit path to a JSON file (recommended for VM deployments and secret-store integrations).
|
|
151
|
+
3. `COWORK_WORKSPACE/credentials.json`.
|
|
152
|
+
4. Walk-up from the current working directory looking for `credentials.json`.
|
|
153
|
+
5. `~/mnt/<folder>/credentials.json` (Cowork workspace mounts).
|
|
154
|
+
6. `$CLAUDE_CONFIG_DIR/sentinelone/credentials.json`.
|
|
155
|
+
7. `~/.config/sentinelone/credentials.json`.
|
|
156
|
+
|
|
157
|
+
The server logs the resolved credential source at startup so you can diagnose surprise overrides.
|
|
158
|
+
|
|
159
|
+
## Transport modes
|
|
160
|
+
|
|
161
|
+
### stdio (default)
|
|
162
|
+
|
|
163
|
+
The transport used by Claude Desktop, Claude Code, Claude Cowork, and any other client launched via `npx` / `node index.js`.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
sentinelone-mcp # auto-discovers credentials
|
|
167
|
+
node index.js # same as above, from a local clone
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Streamable HTTP
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
sentinelone-mcp --transport http # 127.0.0.1:8765/mcp, no auth
|
|
174
|
+
sentinelone-mcp --transport http --host 0.0.0.0 # all interfaces, no auth (loud warning)
|
|
175
|
+
MCP_BEARER_TOKENS_FILE=/etc/sentinelone-mcp/bearer-tokens.json \
|
|
176
|
+
sentinelone-mcp --transport http --host 0.0.0.0 # team mode with per-user tokens
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Configuration via flags or environment variables:
|
|
180
|
+
|
|
181
|
+
| Flag | Env var | Default | Purpose |
|
|
182
|
+
|------|---------|---------|---------|
|
|
183
|
+
| `--transport` | `MCP_TRANSPORT` | `stdio` | `stdio` or `http`. |
|
|
184
|
+
| `--host` | `MCP_HTTP_HOST` | `127.0.0.1` | HTTP bind address. Use `0.0.0.0` for cross-host access. |
|
|
185
|
+
| `--port` | `MCP_HTTP_PORT` | `8765` | HTTP port. |
|
|
186
|
+
| `--path` | `MCP_HTTP_PATH` | `/mcp` | MCP endpoint path. |
|
|
187
|
+
|
|
188
|
+
In HTTP mode the server exposes:
|
|
109
189
|
|
|
110
|
-
|
|
190
|
+
- `POST /mcp` — accepts JSON-RPC, returns JSON-RPC. The MCP entry point.
|
|
191
|
+
- `GET /healthz` — returns `200 ok`. For load balancer probes; no auth.
|
|
192
|
+
|
|
193
|
+
### Team auth: bearer tokens
|
|
194
|
+
|
|
195
|
+
To enable team auth, set one of:
|
|
196
|
+
|
|
197
|
+
- `MCP_BEARER_TOKENS_FILE=/path/to/file.json` (recommended). The file is `{ "<name>": "<token>", ... }`. Names appear in audit logs; revoking a user is a one-line edit. SIGHUP reloads without restart.
|
|
198
|
+
- `MCP_BEARER_TOKENS="token1,token2,..."` (fallback, no per-user names).
|
|
199
|
+
|
|
200
|
+
Token rotation:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
sudo vim /etc/sentinelone-mcp/bearer-tokens.json # add/remove entries
|
|
204
|
+
sudo systemctl reload sentinelone-mcp # SIGHUP, no connection drops
|
|
205
|
+
```
|
|
111
206
|
|
|
112
|
-
|
|
207
|
+
If neither env var is set, HTTP transport runs **without** authentication and the server logs a warning at startup. That's acceptable for `--host 127.0.0.1` single-user use; never use it on `0.0.0.0` in production.
|
|
113
208
|
|
|
114
|
-
|
|
209
|
+
### Audit log
|
|
210
|
+
|
|
211
|
+
Every authenticated HTTP request emits a structured stderr line that systemd captures via journald:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
[audit] 2026-05-28T15:01:22.413Z | alice | tools/call | name=powerquery_run | 200 ok
|
|
215
|
+
[audit] 2026-05-28T15:01:34.221Z | bob | tools/list | - | 200 ok
|
|
216
|
+
[audit] 2026-05-28T17:03:11.221Z | - | - | - | 401 unauthorized
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Calling the HTTP endpoint directly
|
|
220
|
+
|
|
221
|
+
You don't need an MCP client library. The HTTP transport is plain JSON-RPC 2.0 over `POST`, with bearer auth in the `Authorization` header. Any HTTP client works — `curl`, Python `requests`, Node `fetch`, Go `net/http`, etc. This is how you'd integrate from a custom script, a CI job, or a non-MCP tool that just needs to call SentinelOne via the same wrapped surface.
|
|
222
|
+
|
|
223
|
+
> **Ready-made check:** [`scripts/smoke-test-http.sh`](./scripts/smoke-test-http.sh) runs the six contract checks documented below (healthz, initialize, tools/list, tools/call, bad-bearer 401, unknown-method JSON-RPC error) and prints PASS/FAIL. Run as `MCP_HOST=<host:port> MCP_BEARER=<token> bash sentinelone-mcp/scripts/smoke-test-http.sh`. Good for new-team-member onboarding and post-rotation validation.
|
|
224
|
+
|
|
225
|
+
### Endpoint contract
|
|
226
|
+
|
|
227
|
+
| Item | Value |
|
|
228
|
+
|---|---|
|
|
229
|
+
| Method | `POST` |
|
|
230
|
+
| URL | `https://<host>:<port>/mcp` (path is `/mcp` by default; configurable with `--path`) |
|
|
231
|
+
| `Content-Type` | `application/json` |
|
|
232
|
+
| `Authorization` | `Bearer <token>` (one of the tokens in `MCP_BEARER_TOKENS_FILE`) |
|
|
233
|
+
| Body | JSON-RPC 2.0 envelope |
|
|
234
|
+
| Response | JSON-RPC 2.0 envelope (`result` on success, `error` on failure) |
|
|
235
|
+
|
|
236
|
+
Health probe (no auth, no JSON): `GET /healthz` returns `200 ok`.
|
|
237
|
+
|
|
238
|
+
### Initialize, then list tools, then call one (curl)
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
HOST=mcp.s1.internal
|
|
242
|
+
TOKEN='your-bearer-token-here'
|
|
243
|
+
|
|
244
|
+
# 1. initialize (required first call per spec; advertises protocol version and capabilities)
|
|
245
|
+
curl -s -X POST "https://$HOST/mcp" \
|
|
246
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
247
|
+
-H "Content-Type: application/json" \
|
|
248
|
+
-d '{
|
|
249
|
+
"jsonrpc": "2.0",
|
|
250
|
+
"id": 1,
|
|
251
|
+
"method": "initialize",
|
|
252
|
+
"params": {
|
|
253
|
+
"protocolVersion": "2024-11-05",
|
|
254
|
+
"capabilities": {},
|
|
255
|
+
"clientInfo": { "name": "my-script", "version": "1.0" }
|
|
256
|
+
}
|
|
257
|
+
}'
|
|
258
|
+
# -> {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{...},"serverInfo":{...}}}
|
|
259
|
+
|
|
260
|
+
# 2. list every tool the server exposes
|
|
261
|
+
curl -s -X POST "https://$HOST/mcp" \
|
|
262
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
263
|
+
-H "Content-Type: application/json" \
|
|
264
|
+
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
|
|
265
|
+
| jq '.result.tools | length'
|
|
266
|
+
# -> 26
|
|
267
|
+
|
|
268
|
+
# 3. call a tool (here: list custom detection rules with the mandatory isLegacy=false)
|
|
269
|
+
curl -s -X POST "https://$HOST/mcp" \
|
|
270
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
271
|
+
-H "Content-Type: application/json" \
|
|
272
|
+
-d '{
|
|
273
|
+
"jsonrpc": "2.0",
|
|
274
|
+
"id": 3,
|
|
275
|
+
"method": "tools/call",
|
|
276
|
+
"params": {
|
|
277
|
+
"name": "s1_api_get",
|
|
278
|
+
"arguments": {
|
|
279
|
+
"path": "/web/api/v2.1/cloud-detection/rules",
|
|
280
|
+
"params": { "isLegacy": false, "limit": 50 }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}' \
|
|
284
|
+
| jq '.result.content[0].text | fromjson | .pagination.totalItems'
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Python (requests)
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
import json
|
|
291
|
+
import requests
|
|
292
|
+
|
|
293
|
+
URL = "https://mcp.s1.internal/mcp"
|
|
294
|
+
TOKEN = "your-bearer-token-here"
|
|
295
|
+
HEADERS = {
|
|
296
|
+
"Authorization": f"Bearer {TOKEN}",
|
|
297
|
+
"Content-Type": "application/json",
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
def rpc(method, params=None, id=1):
|
|
301
|
+
body = {"jsonrpc": "2.0", "id": id, "method": method}
|
|
302
|
+
if params is not None:
|
|
303
|
+
body["params"] = params
|
|
304
|
+
r = requests.post(URL, headers=HEADERS, json=body, timeout=30)
|
|
305
|
+
r.raise_for_status()
|
|
306
|
+
return r.json()
|
|
307
|
+
|
|
308
|
+
# initialize once per session
|
|
309
|
+
rpc("initialize", {
|
|
310
|
+
"protocolVersion": "2024-11-05",
|
|
311
|
+
"capabilities": {},
|
|
312
|
+
"clientInfo": {"name": "python-client", "version": "1.0"},
|
|
313
|
+
}, id=0)
|
|
314
|
+
|
|
315
|
+
# list tools
|
|
316
|
+
tools = rpc("tools/list", id=1)["result"]["tools"]
|
|
317
|
+
print(f"{len(tools)} tools available")
|
|
318
|
+
|
|
319
|
+
# call a tool
|
|
320
|
+
resp = rpc("tools/call", {
|
|
321
|
+
"name": "powerquery_run",
|
|
322
|
+
"arguments": {
|
|
323
|
+
"query": "dataSource.name=* | group count=count() by dataSource.name | sort -count | limit 10",
|
|
324
|
+
"hours": 24,
|
|
325
|
+
},
|
|
326
|
+
}, id=2)
|
|
327
|
+
|
|
328
|
+
# Tool results live in result.content[0].text as a JSON string.
|
|
329
|
+
payload = json.loads(resp["result"]["content"][0]["text"])
|
|
330
|
+
print(json.dumps(payload, indent=2))
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Node (built-in fetch, Node 18+)
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
const URL = 'https://mcp.s1.internal/mcp';
|
|
337
|
+
const TOKEN = process.env.MCP_BEARER;
|
|
338
|
+
|
|
339
|
+
async function rpc(method, params, id = 1) {
|
|
340
|
+
const body = { jsonrpc: '2.0', id, method };
|
|
341
|
+
if (params !== undefined) body.params = params;
|
|
342
|
+
const res = await fetch(URL, {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
headers: {
|
|
345
|
+
'Authorization': `Bearer ${TOKEN}`,
|
|
346
|
+
'Content-Type': 'application/json',
|
|
347
|
+
},
|
|
348
|
+
body: JSON.stringify(body),
|
|
349
|
+
});
|
|
350
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
|
351
|
+
return res.json();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await rpc('initialize', {
|
|
355
|
+
protocolVersion: '2024-11-05',
|
|
356
|
+
capabilities: {},
|
|
357
|
+
clientInfo: { name: 'node-client', version: '1.0' },
|
|
358
|
+
}, 0);
|
|
359
|
+
|
|
360
|
+
const { result } = await rpc('tools/list', null, 1);
|
|
361
|
+
console.log(`${result.tools.length} tools available`);
|
|
362
|
+
|
|
363
|
+
const call = await rpc('tools/call', {
|
|
364
|
+
name: 'uam_list_alerts',
|
|
365
|
+
arguments: { first: 20, status: 'NEW' },
|
|
366
|
+
}, 2);
|
|
367
|
+
console.log(JSON.parse(call.result.content[0].text));
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### JSON-RPC envelope shapes
|
|
371
|
+
|
|
372
|
+
**Success response:**
|
|
115
373
|
|
|
116
374
|
```json
|
|
117
375
|
{
|
|
118
|
-
"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"args": ["-y", "@pmoses-s1/sentinelone-mcp"],
|
|
122
|
-
"env": {
|
|
123
|
-
"S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
|
|
124
|
-
"S1_CONSOLE_API_TOKEN": "eyJ...your-api-token..."
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
376
|
+
"jsonrpc": "2.0",
|
|
377
|
+
"id": 2,
|
|
378
|
+
"result": { ... method-specific payload ... }
|
|
128
379
|
}
|
|
129
380
|
```
|
|
130
381
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
If you are developing the MCP server itself, replace the `npx` invocation with a path to your clone:
|
|
382
|
+
**Error response:**
|
|
134
383
|
|
|
135
384
|
```json
|
|
136
|
-
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
385
|
+
{
|
|
386
|
+
"jsonrpc": "2.0",
|
|
387
|
+
"id": 2,
|
|
388
|
+
"error": {
|
|
389
|
+
"code": -32602,
|
|
390
|
+
"message": "Tool not found: bad_tool_name"
|
|
391
|
+
}
|
|
140
392
|
}
|
|
141
393
|
```
|
|
142
394
|
|
|
143
|
-
|
|
395
|
+
**Notifications** (one-way messages with no `id`, e.g. `notifications/initialized`):
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
curl -i -s -X POST "https://$HOST/mcp" \
|
|
399
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
400
|
+
-H "Content-Type: application/json" \
|
|
401
|
+
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
|
|
402
|
+
# -> HTTP/2 202 (no body, per JSON-RPC spec)
|
|
403
|
+
```
|
|
144
404
|
|
|
145
|
-
|
|
405
|
+
### Error codes you'll actually see
|
|
146
406
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
407
|
+
| HTTP | JSON-RPC code | Meaning |
|
|
408
|
+
|---|---|---|
|
|
409
|
+
| 200 | (none, has `result`) | Success |
|
|
410
|
+
| 200 | `-32601` | Method not found (e.g. typo in method name) |
|
|
411
|
+
| 200 | `-32602` | Invalid params (tool not found, missing required arg) |
|
|
412
|
+
| 200 | `-32603` | Tool handler threw — upstream S1 API error usually |
|
|
413
|
+
| 400 | `-32700` | Parse error (malformed JSON body) |
|
|
414
|
+
| 400 | `-32600` | Invalid request (e.g. JSON-RPC batch — not supported) |
|
|
415
|
+
| 401 | `-32001` | Missing or invalid bearer token |
|
|
416
|
+
| 405 | (none) | Wrong HTTP method on `/mcp` (only POST is accepted) |
|
|
417
|
+
| 413 | `-32600` | Body exceeds 4 MB |
|
|
150
418
|
|
|
151
|
-
|
|
419
|
+
### Tool inputs and outputs
|
|
152
420
|
|
|
153
|
-
|
|
421
|
+
Every tool's input schema is documented in the `tools/list` response (look at the `inputSchema` JSON Schema on each tool). The response shape is always:
|
|
422
|
+
|
|
423
|
+
```json
|
|
424
|
+
{
|
|
425
|
+
"content": [
|
|
426
|
+
{ "type": "text", "text": "<JSON-encoded result>" }
|
|
427
|
+
],
|
|
428
|
+
"isError": false
|
|
429
|
+
}
|
|
430
|
+
```
|
|
154
431
|
|
|
155
|
-
|
|
432
|
+
Parse `content[0].text` as JSON to get the actual data the tool returned. Tool-level errors set `isError: true` and put the error text in the same field.
|
|
156
433
|
|
|
157
|
-
|
|
158
|
-
2. Call `powerquery_enumerate_sources` to discover active SDL data sources (mandatory: never assume sources from a prior session).
|
|
159
|
-
3. In parallel, call `uam_list_alerts` with `filter="status=OPEN"` to pull active alerts.
|
|
434
|
+
## CLI reference
|
|
160
435
|
|
|
161
|
-
|
|
436
|
+
```
|
|
437
|
+
sentinelone-mcp [options]
|
|
438
|
+
|
|
439
|
+
OPTIONS
|
|
440
|
+
--transport <stdio|http> Transport. Default: stdio.
|
|
441
|
+
--host <host> HTTP bind address. Default: 127.0.0.1.
|
|
442
|
+
--port <port> HTTP port. Default: 8765.
|
|
443
|
+
--path <path> HTTP MCP endpoint path. Default: /mcp.
|
|
444
|
+
-h, --help Show help.
|
|
445
|
+
-v, --version Show server version.
|
|
446
|
+
```
|
|
162
447
|
|
|
163
448
|
## Architecture
|
|
164
449
|
|
|
165
450
|
```
|
|
166
451
|
sentinelone-mcp/
|
|
167
|
-
index.js
|
|
452
|
+
index.js Entry: flag parsing + transport selection
|
|
168
453
|
lib/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
454
|
+
server-core.js Tool registry, JSON-RPC dispatch (transport-agnostic)
|
|
455
|
+
stdio-transport.js stdin/stdout JSON-RPC loop
|
|
456
|
+
http-transport.js Streamable HTTP (node:http, zero deps)
|
|
457
|
+
auth.js Bearer token allowlist with SIGHUP reload
|
|
458
|
+
credentials.js S1 + SDL credential resolution
|
|
459
|
+
s1.js Mgmt REST + LRQ PowerQuery + Purple AI + UAM GraphQL
|
|
460
|
+
sdl.js SDL config files + V1 query
|
|
461
|
+
uam-ingest.js HEC alert/indicator ingestion
|
|
172
462
|
tools/
|
|
173
|
-
powerquery.js
|
|
174
|
-
mgmt-console.js
|
|
175
|
-
sdl-api.js
|
|
176
|
-
hyperautomation.js
|
|
177
|
-
|
|
463
|
+
powerquery.js PowerQuery enumerate/run/schema-discover
|
|
464
|
+
mgmt-console.js S1 REST verbs + Purple AI summary + UAM
|
|
465
|
+
sdl-api.js SDL config file + log ingestion tools
|
|
466
|
+
hyperautomation.js Hyperautomation list/get/import/export/archive
|
|
467
|
+
uam-ingest.js UAM Alert Interface ingestion tools
|
|
468
|
+
deploy/
|
|
469
|
+
install.sh One-shot installer (Mac and Linux)
|
|
470
|
+
systemd/ Service unit for Linux VM deployments
|
|
471
|
+
caddy/ TLS reverse proxy template
|
|
472
|
+
README.md Deployment walkthrough
|
|
473
|
+
scripts/
|
|
474
|
+
regen-readme-tools-table.mjs Tools-table regenerator (no drift)
|
|
475
|
+
tests/ Smoke + stdio + HTTP test suites (node --test)
|
|
178
476
|
```
|
|
179
477
|
|
|
180
478
|
## Auth patterns (implemented)
|
|
@@ -185,15 +483,37 @@ sentinelone-mcp/
|
|
|
185
483
|
| LRQ PowerQuery | `Authorization: Bearer <jwt>` | Same token, different prefix |
|
|
186
484
|
| Purple AI GraphQL | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
|
|
187
485
|
| UAM GraphQL | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
|
|
486
|
+
| UAM HEC ingest | `Authorization: Bearer <jwt>` | `S1_CONSOLE_API_TOKEN` |
|
|
188
487
|
| SDL config ops | `Authorization: Bearer <key>` | `SDL_CONFIG_WRITE_KEY` or console JWT |
|
|
189
|
-
|
|
488
|
+
|
|
489
|
+
## Testing
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
npm test
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Three test suites under `tests/`:
|
|
496
|
+
|
|
497
|
+
- `smoke.test.mjs` — introspects `ALL_TOOLS` directly, no spawning. Asserts 26 tools by name; catches any drift between code and the README regenerator.
|
|
498
|
+
- `stdio-transport.test.mjs` — spawns the server in stdio mode, exercises `initialize`, `tools/list`, `resources/list`, `prompts/list`, and error handling.
|
|
499
|
+
- `http-transport.test.mjs` — spawns in HTTP mode on a random ephemeral port, exercises `/healthz`, `POST /mcp`, both auth-required and auth-optional flows, and the env-var token fallback.
|
|
500
|
+
|
|
501
|
+
The smoke suite is the source of truth for the tool count and is what `scripts/regen-readme-tools-table.mjs` derives the README table from. If the table goes stale, `npm run regen:readme -- --check` fails CI; `npm run regen:readme` fixes it.
|
|
190
502
|
|
|
191
503
|
## Updating CLAUDE.md
|
|
192
504
|
|
|
193
|
-
The `sentinelone://soc-context` resource and `soc_analyst` prompt load CLAUDE.md at server startup. Resolution order
|
|
505
|
+
The `sentinelone://soc-context` resource and `soc_analyst` prompt load `CLAUDE.md` at server startup. Resolution order:
|
|
506
|
+
|
|
507
|
+
1. `S1_CLAUDE_MD_PATH` env var (explicit absolute path).
|
|
508
|
+
2. `<cwd>/CLAUDE.md` — your Cowork project folder, when launched from there.
|
|
509
|
+
3. Same-dir / parent / grandparent of the server's `index.js` — when running from a git clone.
|
|
510
|
+
|
|
511
|
+
For npx installs without a CLAUDE.md nearby, set `S1_CLAUDE_MD_PATH` in the `env` block of `claude_desktop_config.json` to point at the one in your Cowork project folder. Restart Claude Desktop to pick up edits.
|
|
512
|
+
|
|
513
|
+
## Removed tools
|
|
514
|
+
|
|
515
|
+
`purple_ai_query` and `purple_ai_investigate` were removed on 2026-05-03. Both required a browser-session `teamToken` from `/sdl/v2/graphql` that service-account API tokens never obtain (returns `AsimovError` / `SERVICE_ERROR`). Use `mcp__purple-mcp__purple_ai` instead, which holds the right credentials.
|
|
194
516
|
|
|
195
|
-
|
|
196
|
-
2. `<cwd>/CLAUDE.md` (your Cowork project folder when launched from a project)
|
|
197
|
-
3. Same-dir / parent / grandparent of the server's `index.js` (when running from a git clone)
|
|
517
|
+
## Version history
|
|
198
518
|
|
|
199
|
-
|
|
519
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|