@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/README.md CHANGED
@@ -1,180 +1,478 @@
1
1
  # SentinelOne MCP Server
2
2
 
3
- A Model Context Protocol (MCP) server that orchestrates all six SentinelOne skills and their APIs. Built in pure Node.js 18+ with zero external dependencies.
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
- **19 tools** covering every skill in the plugin:
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 | `purple_ai_query` | sentinelone-mgmt-console-api |
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 | `sdl_list_files` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
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 | `sdl_delete_file` | sentinelone-sdl-api |
25
- | SDL API | `sdl_upload_logs` | sentinelone-sdl-api / sdl-log-parser |
26
- | Hyperautomation | `ha_list_workflows` | sentinelone-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 | `ha_export_workflow` | sentinelone-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`: CLAUDE.md (full SOC analyst operating instructions)
33
- - `sentinelone://credentials-status`: Which credentials are configured
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`: Embeds CLAUDE.md as a system prompt; call at session start
37
- - `session_init`: Structured initialization: enumerate sources + triage alerts in parallel
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
- ## Prerequisites
53
+ ## Quick install
40
54
 
41
- - Node.js 18 or later
42
- - No `npm install` needed: zero external dependencies
55
+ Three paths, pick the one that matches your setup:
43
56
 
44
- ## Credentials
57
+ ### A. Local single-user via `npx` (Claude Desktop / Claude Code / Cowork)
45
58
 
46
- Credentials are passed as environment variables in `claude_desktop_config.json` (see Installation below). The server also auto-discovers a `credentials.json` file by searching from the working directory upward as a backwards-compatible fallback for direct-skill users.
59
+ MCP runs as a subprocess on your machine, talking SentinelOne APIs directly. Credentials live in the Claude config `env` block.
47
60
 
48
- `S1_CONSOLE_URL` and `S1_CONSOLE_API_TOKEN` are sufficient for most tools. Add the SDL keys only if you need `sdl_upload_logs` (requires `SDL_LOG_WRITE_KEY`) or `sdl_put_file` (requires `SDL_CONFIG_WRITE_KEY`).
61
+ Add this to `claude_desktop_config.json` (or `.mcp.json` for Claude Code):
49
62
 
50
- | Variable | Description |
51
- |----------|-------------|
52
- | `S1_CONSOLE_URL` | Your console URL, e.g. `https://usea1-acme.sentinelone.net` |
53
- | `S1_CONSOLE_API_TOKEN` | Management Console API token (Settings → Users → Service Users) |
54
- | `S1_HEC_INGEST_URL` | HEC ingest host, e.g. `https://ingest.us1.sentinelone.net` |
55
- | `SDL_XDR_URL` | SDL tenant URL, e.g. `https://xdr.us1.sentinelone.net` |
56
- | `SDL_LOG_WRITE_KEY` | SDL Log Write key (required for `sdl_upload_logs` only) |
57
- | `SDL_LOG_READ_KEY` | SDL Log Read key (required for SDL query operations) |
58
- | `SDL_CONFIG_WRITE_KEY` | SDL Config Write key (required for `sdl_put_file`) |
59
- | `SDL_CONFIG_READ_KEY` | SDL Config Read key (required for `sdl_list_files`, `sdl_get_file`) |
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
- ## Run the server
83
+ Restart Claude Desktop. `npx -y` caches the package on first launch.
62
84
 
63
- ```bash
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
- # Or from a local clone (development)
68
- node /path/to/claude-skills/sentinelone-mcp/index.js
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
- ## Installation
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
- ### Why you need this (or the allowlist alternative)
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
- The Claude sandbox proxy blocks outbound HTTPS to `*.sentinelone.net` by default. There are two ways to fix this:
95
+ ### C. Claude Desktop connecting to a team VM (stdio bridge)
76
96
 
77
- **Option A: Install sentinelone-mcp (recommended).** The MCP server runs as a local process on your machine outside the sandbox. All API calls go directly from your machine to SentinelOne, bypassing the sandbox proxy entirely. No allowlist changes needed.
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
- **Option B: Add `*.sentinelone.net` to the Claude sandbox allowlist.** In Claude Desktop go to Settings → Claude Code → Network Access and add `*.sentinelone.net` to the allowed domains. This lets the skills' Python scripts reach the API directly from inside the sandbox. Use this if you prefer to keep everything running in the sandbox rather than install a local server.
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
- Most users should use Option A: it requires no admin changes and keeps credentials out of the sandbox environment.
101
+ Each team member installs the script once:
82
102
 
83
- ### Option A: Add to Claude Desktop
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
- In `~/Library/Application Support/Claude/claude_desktop_config.json`, add the `sentinelone-mcp` entry to your `mcpServers` block. The server runs via `npx` directly from npm, so there is no clone, no install, and no absolute path to manage. Credentials go in the `env` section:
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": "npx",
92
- "args": ["-y", "@pmoses-s1/sentinelone-mcp"],
116
+ "command": "node",
117
+ "args": ["/Users/<you>/.local/bin/sentinelone-mcp-bridge.mjs"],
93
118
  "env": {
94
- "S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
95
- "S1_CONSOLE_API_TOKEN": "eyJ...your-api-token...",
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
- `npx -y` answers "yes" to the install prompt on first launch, fetches the package, and caches it. Subsequent launches start instantly from the cache. Restart Claude Desktop after saving.
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
- `S1_CONSOLE_URL` and `S1_CONSOLE_API_TOKEN` are the minimum required for most tools. Include the SDL keys only if you need log ingest or parser/dashboard deploy. Set `S1_CLAUDE_MD_PATH` if you keep CLAUDE.md outside your Cowork project folder.
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
- ### Option A: Add to Claude Code
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
- In `.mcp.json` at your project root, or `~/.mcp.json` globally. Same npx invocation, same env block:
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
- "mcpServers": {
119
- "sentinelone-mcp": {
120
- "command": "npx",
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
- ### Option A: Run from a local clone (development only)
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
- "sentinelone-mcp": {
137
- "command": "node",
138
- "args": ["/absolute/path/to/claude-skills/sentinelone-mcp/index.js"],
139
- "env": { "...": "..." }
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
- ### Option B: Sandbox allowlist (no MCP server)
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
- If you prefer to run API calls from inside the Claude sandbox rather than install a local server, add `*.sentinelone.net` to the network allowlist:
405
+ ### Error codes you'll actually see
146
406
 
147
- 1. Open Claude Desktop Settings Claude Code → Network Access
148
- 2. Add `*.sentinelone.net` to the allowed domains
149
- 3. Restart Claude Desktop
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
- The skills' Python scripts (`s1_client.py`, `sdl_client.py`, etc.) will then reach the API directly. No MCP server required for the skills to work.
419
+ ### Tool inputs and outputs
152
420
 
153
- ## Workflow: session startup
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
- When connecting to this MCP server, start every session with:
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
- 1. Read the `soc_analyst` prompt (or the `sentinelone://soc-context` resource) to load operating instructions from CLAUDE.md.
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
- The `session_init` prompt automates steps 2-3 as a structured prompt.
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 Raw MCP JSON-RPC over stdio (no SDK dependency)
452
+ index.js Entry: flag parsing + transport selection
168
453
  lib/
169
- credentials.js Auto-discovers credentials.json (env vars > file > walk-up > ~/mnt/*)
170
- s1.js S1 Mgmt REST API + LRQ PowerQuery + Purple AI + UAM GraphQL
171
- sdl.js SDL config files (get/put/list) + V1 query + uploadLogs
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 PowerQuery enumerate/run/schema-discover tools
174
- mgmt-console.js S1 REST + Purple AI + UAM tools
175
- sdl-api.js SDL config file + log ingestion tools
176
- hyperautomation.js Hyperautomation list/get/import/export tools
177
- README.md
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
- | SDL uploadLogs | `Authorization: Bearer <key>` | `SDL_LOG_WRITE_KEY` only (console JWT rejected) |
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 (highest priority wins):
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
- 1. `S1_CLAUDE_MD_PATH` env var (explicit absolute path)
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
- For npx installs, drop a copy of CLAUDE.md into your Cowork project folder, or set `S1_CLAUDE_MD_PATH` in the `env` block of `claude_desktop_config.json`. Restart Claude Desktop to pick up changes.
519
+ See [CHANGELOG.md](./CHANGELOG.md).