@pmoses-s1/s1-secops-mcp 1.3.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 ADDED
@@ -0,0 +1,119 @@
1
+ # Changelog
2
+
3
+ ## 1.2.4 - 2026-07-31
4
+
5
+ Hardening release from the 2026-07-31 code review. Tool count unchanged at 26.
6
+
7
+ ### Fixed
8
+ - **Missing `Retry-After` header no longer sleeps 0ms before retrying.** `Number(null)` is 0, so `lib/s1.js`, `lib/hec.js`, and `lib/uam-ingest.js` treated an absent header as "wait 0ms" and hammered the backend. All three now use the validated pattern from `lib/sdl.js`: honor the header only when present and parseable as a finite number of seconds (capped at 30s), otherwise fall back to the exponential delay. Numeric headers behave exactly as before.
9
+ - **`uam_set_status` no longer reports silent success.** The `alertTriggerActions` mutation selected only `__typename`, so a skipped or failed action still returned as if it worked (observed live: status unchanged after a "successful" call). The selection now mirrors the full `actions { success failure skip }` shape and the client throws when the backend reports a failure entry, skips the action without a success, or returns an empty actions list (nothing applied, e.g. the filter matched no alert). `uam_add_note` was audited for the same pattern; it already verifies via the returned note list.
10
+ - **LRQ polling tolerates transient poll errors.** A single 429/5xx poll response used to throw and cancel the running query. Transient statuses now keep polling (interval doubles up to 5s) until the existing 5-minute deadline; other 4xx responses remain fatal.
11
+ - **`hasSdlCreds()` recognizes read-only key deployments.** It required `SDL_CONFIG_WRITE_KEY` or the console token; tenants configured with only `SDL_LOG_READ_KEY` / `SDL_CONFIG_READ_KEY` were reported as unconfigured. All chain keys now count.
12
+ - **HTTP transport: oversized bodies get their 413.** The request was destroyed before the response was written, so clients saw a connection reset instead of the 413 JSON error. The server now stops reading, sends the 413, then drops the connection after the response is flushed.
13
+ - **HTTP transport exits nonzero on any listen-time server error** (EACCES, EADDRNOTAVAIL, ...), not only EADDRINUSE, so systemd/Docker restart policies see the failure.
14
+ - **`powerquery_schema_discover` escapes backslashes before quotes** in the data-source name; quote-only escaping let a trailing backslash re-open the V1 filter string.
15
+ - **`s1_api_get` honors an inline `?isLegacy=` already present in the path** instead of appending a conflicting `isLegacy=false` query param, matching `_maybe_inject_islegacy` in the Python twin.
16
+ - **SDL `config_read` key chain reordered to least-privilege first** (config_read, config_write, console JWT), matching the Python `SDLClient` the header claims to mirror.
17
+
18
+ ### Changed
19
+ - **Removed dead exports `purpleAiQuery` and `purpleAiInvestigate`** from `lib/s1.js`; their MCP tools were removed 2026-05-03 (browser-session teamToken requirement) and nothing referenced the library functions. Corrected stale doc text that pointed at a nonexistent `uam_set_analyst_verdict` tool: the analyst verdict is set via a raw `alertTriggerActions` mutation with the `analystVerdictUpdate` action through `s1_api_post`.
20
+ - **Deploy docs: credentials.json changes require `systemctl restart`.** SIGHUP reload only re-reads bearer tokens; the installer output and systemd unit comment said reload was enough. `deploy/install.sh` also sets `umask 077` so token/credential files are never world-readable at creation (the explicit `chmod 600` lines remain).
21
+ - **Claude Desktop bridge hardening:** 120s fetch timeout (`AbortSignal.timeout`), notification responses are drained so keep-alive sockets are released, and the URL constant no longer shadows the global `URL` constructor.
22
+ - **Docker entrypoint passes extra args through** to the selected server binary (e.g. `s1-secops-mcp --transport http`); no-args behavior is unchanged.
23
+ - **`const status = response.error ? 200 : 200`** simplified; JSON-RPC errors still return HTTP 200 with an error envelope.
24
+
25
+ ### Tests
26
+ - New `tests/regressions-2026-07-31.test.mjs` (mocked fetch, no network): missing `Retry-After` uses the exponential delay, `uamSetStatus` throws on failure/skip results, inline `?isLegacy=` is honored. Both regression suites are now part of `npm test`.
27
+ - Transport and smoke tests read the expected version from `package.json` instead of a hardcoded string.
28
+
29
+ ## 1.2.3 - 2026-07-29
30
+
31
+ Correctness release from the 2026-07-29 defect review. Fixes two bugs that produced plausible-but-wrong query results, hardens the SDL auth chain and HTTP retry paths, corrects the HEC `/event` content type, and adds a regression suite. Tool count unchanged at 26. (The Docker bundle image moved to 1.2.4 pinning `S1_MCP_VERSION=1.2.3`; the image tag stays ahead of the npm tag as it has since the split.)
32
+
33
+ ### Fixed
34
+ - **`powerquery_run` no longer collapses a caller-supplied time window.** The old code overwrote BOTH `startTime` and `endTime` with the last-`hours` default whenever either was missing, so a startTime-only call silently ran over the last 24 hours. Each bound now defaults independently. Live A/B: a startTime-only 7.4-day query returned 73,755 events on the fixed server vs 12,911 (the 24h control) on the old one.
35
+ - **`powerquery_run` now reports `matchCount`.** It was read from the top level of the LRQ response and came back `null` on every call; it lives inside the `data` block on current engines.
36
+ - **SDL auth chain falls through on 401/403.** `lib/sdl.js` treated an auth failure on the first configured key as fatal even when a later key (e.g. the console JWT) would work. It now advances through the chain and raises only when exhausted.
37
+ - **HEC `/event` ingestion uses `application/json`,** so per-event `time` backdating is honored instead of the envelope being indexed as opaque text at receive time.
38
+
39
+ ### Changed
40
+ - **Write requests no longer auto-retry on 5xx.** Retry is restricted to idempotent methods (GET/HEAD); read-only POSTs (GraphQL queries, Purple AI launches) opt back in via `allowRetry`. HEC raw ingest no longer retries 5xx (no idempotency key); UAM ingest still retries because `metadata.uid` dedupes.
41
+ - **`Retry-After` parsing hardened:** an HTTP-date value no longer collapses to `sleep(NaN)`; waits are validated and capped at 30s.
42
+ - **`uam_add_note` returns the correct note** (matches by text, tiebreaks on newest `createdAt`) instead of assuming newest-last ordering.
43
+ - **`uam_post_alert` schema help corrected** to require `class_uid 99602001` (the S1 Security Alert extension class); generic OCSF `2002` is silently dropped by the stitcher.
44
+ - **`ha_export_workflow` no longer implies it returns the archive** (metadata only).
45
+ - **`powerquery_schema_discover` escapes single quotes** in the data-source name before building the V1 filter.
46
+
47
+ ### Tests
48
+ - New `tests/regressions-2026-07-29.test.mjs` (mocked fetch, no network): independent time-bound defaults, `matchCount` extraction, SDL 401/403 fall-through, HEC per-endpoint content type, write-vs-read retry semantics.
49
+
50
+ ## 1.2.2 - 2026-06-13
51
+
52
+ ### Changed
53
+ - **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.
54
+ - **`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 `powerquery/references/datasource-command.md`.
55
+
56
+ ### Notes
57
+ - Tool count unchanged at 26 (the Hyperautomation tool was renamed, not added or removed).
58
+ - `SERVER_INFO.version` bumped in lockstep with `package.json` (the drift that forced the 1.2.0 -> 1.2.1 re-release).
59
+
60
+ ## 1.2.1 - 2026-06-11
61
+
62
+ 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.
63
+
64
+ ### Added
65
+ - **`hec_ingest` tool**: raw-log/event ingestion into the Singularity Data Lake via the HEC (HTTP Event Collector) endpoint (`/services/collector/raw` and `/services/collector/event`). Supports `parser` (-> `?sourcetype=`), custom `fields` (query params), **required** `scope` (S1-Scope header), gzip compression, and `isParsed` (-> `?isParsed=true`, indexes already-structured JSON with no SDL parser). Replaces the removed `sdl_upload_logs`. Validated live across the full HEC matrix (both endpoints, gzip on/off, parser field extraction, multi-line, batched, reserved-field handling, scope enforcement, isParsed). Grounded in the S-26.1 HEC docs (p.4723-4726).
66
+
67
+ ### Removed
68
+ - **`sdl_upload_logs` tool** plus the underlying SDL `uploadLogs`/`addEvents` library functions and `SDL_LOG_WRITE_KEY` plumbing. SDL raw-log ingestion moves to the HEC path (`hec_ingest`). The `sdl-api` skill is now query + configuration only; the `sdl-log-parser` validation loop uses HEC ingest.
69
+
70
+ ### Changed
71
+ - Tool count unchanged at 26 (removed `sdl_upload_logs`, added `hec_ingest`).
72
+ - Skill docs corrected: scheduled detection rules bind the Target Asset via `entityMappings` ("Entity column mapping"); the full scheduled-rule option set (UI <-> API) is catalogued in `powerquery/references/detection-rules.md`.
73
+
74
+
75
+ ## 1.1.0 - 2026-05-28 (rebuilt 2026-05-31)
76
+
77
+ ### Fixed (rebuild)
78
+ - **`s1_api_get` now auto-injects `isLegacy=false` for `/cloud-detection/rules` listings.** Without `isLegacy=false` the S1 API silently omits `queryType="scheduled"` PowerQuery rules from the response; no error, no warning, the response just lies by omission. The handler now guards against this when the caller forgets, and the tool description loudly flags the requirement. This eliminates the "I see zero scheduled detections" failure mode that was producing wrong verdicts when listing Custom Detection rules. Same `1.1.0` version per the rebuild request.
79
+
80
+ ### Added
81
+ - **Streamable HTTP transport.** New `--transport http` mode (default stays `stdio`). Single-endpoint POST `/mcp` per the MCP 2024-11-05 spec, plus `/healthz` for load balancer probes. Implementation is pure `node:http`, no new dependencies.
82
+ - **Per-user bearer token auth.** New `MCP_BEARER_TOKENS_FILE` env var pointing at a `{ "<name>": "<token>" }` JSON file gives each team member a stable name in audit logs and supports rotation. SIGHUP reloads tokens without dropping connections. `MCP_BEARER_TOKENS` env var (comma-separated raw tokens) is a fallback for small or quick-test setups.
83
+ - **Audit logging.** Every authenticated HTTP request emits `[audit] <ts> | <name> | <method> | <param-summary> | <status>` to stderr; systemd captures it via journald.
84
+ - **`S1_CREDS_FILE` credential resolver.** Highest-priority explicit path for credentials, useful for VM deployments and secret-store integrations (Vault, Doppler, 1Password Connect, sealed-secrets).
85
+ - **Deploy artifacts** under `deploy/`:
86
+ - `install.sh`: one-shot installer for Mac and Linux. `--user` mode for individuals, `--server` mode for Linux VMs (creates `mcp` system user, generates an initial bearer token, installs systemd unit, starts the service).
87
+ - `systemd/s1-secops-mcp.service`: hardened unit with `NoNewPrivileges`, `ProtectSystem=strict`, `MemoryDenyWriteExecute`, SIGHUP-as-reload.
88
+ - `caddy/Caddyfile.example`: TLS reverse proxy template with bearer header gate and streaming-friendly flush.
89
+ - `README.md`: full topology guide (single-user local, single-user HTTP, team VM-hosted) with day-2 operations.
90
+ - **Test suite.** Three new files under `tests/`, runnable via `npm test`:
91
+ - `smoke.test.mjs`: source-of-truth tool inventory (26 tools by name).
92
+ - `stdio-transport.test.mjs`: JSON-RPC round trip via spawned stdio process.
93
+ - `http-transport.test.mjs`: HTTP transport end-to-end, bearer auth happy/sad paths.
94
+ - **README auto-regenerator** at `scripts/regen-readme-tools-table.mjs`. `npm run regen:readme` keeps the README table in sync with `ALL_TOOLS`. `npm run regen:readme -- --check` fails when stale (suitable for CI).
95
+
96
+ ### Fixed
97
+ - **README tool table.** Previous count was 19; actual is 26. Auto-generated now.
98
+ - **Header comment in `index.js`.** Previously said 21; updated to 26.
99
+ - **`purple_ai_query`** removed from the documentation. The tool itself was removed 2026-05-03 because the underlying API requires a browser-session `teamToken` that service-account API tokens never obtain. The README, `index.js`, and `docs/mcp-tools.md` no longer reference it.
100
+ - **`uam_set_status` documentation.** Doc previously said valid status values include `CLOSED`. The source enum is `NEW`, `IN_PROGRESS`, `RESOLVED`; doc now matches.
101
+
102
+ ### Changed
103
+ - **Refactored** dispatch out of `index.js` into `lib/server-core.js` so both transports use one code path. `lib/stdio-transport.js` is the extracted stdio loop; `lib/http-transport.js` is new.
104
+ - **package.json**:
105
+ - `version` 1.0.0 → 1.1.0
106
+ - new scripts: `start:http`, `test`, `regen:readme`
107
+ - new files included in the npm tarball: `deploy/`, `scripts/`, `CHANGELOG.md`
108
+
109
+ ### Compatibility
110
+ - Default invocation is unchanged: `npx -y @pmoses-s1/s1-secops-mcp` still produces a stdio MCP server with identical behaviour to 1.0.0.
111
+ - Existing `claude_desktop_config.json` and `.mcp.json` configs work without modification.
112
+ - The 26 tools, 2 resources, and 2 prompts are unchanged from the late-1.0.0 line; only the documentation now matches reality.
113
+
114
+ ## 1.0.0 - 2026-05-07
115
+
116
+ Initial public release.
117
+ - 19 tools across PowerQuery, S1 Mgmt REST, UAM, SDL API, Hyperautomation.
118
+ - stdio transport only.
119
+ - Credentials via env vars or auto-discovered `credentials.json`.
package/README.md ADDED
@@ -0,0 +1,519 @@
1
+ # SentinelOne MCP Server
2
+
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.
9
+
10
+ ## What this exposes
11
+
12
+ <!-- BEGIN AUTO-GENERATED TOOLS TABLE -->
13
+ **26 tools** across PowerQuery, Mgmt Console, SDL API, Hyperautomation, and UAM Ingest:
14
+
15
+ | Group | Tool | Skill |
16
+ |-------|------|-------|
17
+ | PowerQuery | `powerquery_enumerate_sources` | powerquery |
18
+ | PowerQuery | `powerquery_run` | powerquery |
19
+ | PowerQuery | `powerquery_schema_discover` | powerquery |
20
+ | Mgmt Console | `purple_ai_alert_summary` | mgmt-console-api |
21
+ | Mgmt Console | `s1_api_delete` | mgmt-console-api |
22
+ | Mgmt Console | `s1_api_get` | mgmt-console-api |
23
+ | Mgmt Console | `s1_api_patch` | mgmt-console-api |
24
+ | Mgmt Console | `s1_api_post` | mgmt-console-api |
25
+ | Mgmt Console | `s1_api_put` | mgmt-console-api |
26
+ | Mgmt Console | `uam_add_note` | mgmt-console-api |
27
+ | Mgmt Console | `uam_get_alert` | mgmt-console-api |
28
+ | Mgmt Console | `uam_list_alerts` | mgmt-console-api |
29
+ | Mgmt Console | `uam_set_status` | mgmt-console-api |
30
+ | SDL API | `hec_ingest` | sdl-api / sdl-log-parser |
31
+ | SDL API | `sdl_delete_file` | sdl-api |
32
+ | SDL API | `sdl_get_file` | sdl-api / sdl-dashboard / sdl-log-parser |
33
+ | SDL API | `sdl_list_files` | sdl-api / sdl-dashboard / sdl-log-parser |
34
+ | SDL API | `sdl_put_file` | sdl-api / sdl-dashboard / sdl-log-parser |
35
+ | Hyperautomation | `ha_delete_workflow` | hyperautomation |
36
+ | Hyperautomation | `ha_export_workflow` | hyperautomation |
37
+ | Hyperautomation | `ha_get_workflow` | hyperautomation |
38
+ | Hyperautomation | `ha_import_workflow` | hyperautomation |
39
+ | Hyperautomation | `ha_list_workflows` | hyperautomation |
40
+ | UAM Ingest | `uam_ingest_alert` | mgmt-console-api (UAM Alert Interface) |
41
+ | UAM Ingest | `uam_post_alert` | mgmt-console-api (UAM Alert Interface) |
42
+ | UAM Ingest | `uam_post_indicators` | mgmt-console-api (UAM Alert Interface) |
43
+ <!-- END AUTO-GENERATED TOOLS TABLE -->
44
+
45
+ **2 resources:**
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.
48
+
49
+ **2 prompts:**
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.
52
+
53
+ ## Quick install
54
+
55
+ For the end-user install paths (Docker quick start, npx/uvx, and team VM), see the canonical **[README Installation section](../README.md#installation)**; credential keys and where to get them are in **[docs/credentials.md](../docs/credentials.md)**. This section is the MCP-server-specific reference: the exact npm package and pin, the reproducible install script, and the Claude Desktop stdio bridge for a shared team VM. Three paths, pick the one that matches your setup:
56
+
57
+ ### A. Local single-user via `npx` (Claude Desktop / Claude Code / Cowork)
58
+
59
+ MCP runs as a subprocess on your machine, talking SentinelOne APIs directly. Credentials live in the Claude config `env` block.
60
+
61
+ Add this to `claude_desktop_config.json` (or `.mcp.json` for Claude Code):
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "s1-secops-mcp": {
67
+ "command": "npx",
68
+ "args": ["-y", "@pmoses-s1/s1-secops-mcp@1.3.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
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ Restart Claude Desktop. `npx -y` caches the package on first launch.
80
+
81
+ ### B. Reproducible: install script
82
+
83
+ ```bash
84
+ curl -fsSL https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/s1-secops-mcp/deploy/install.sh | bash
85
+ ```
86
+
87
+ 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.
88
+
89
+ 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).
90
+
91
+ ### C. Claude Desktop connecting to a team VM (stdio bridge)
92
+
93
+ 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.)
94
+
95
+ The bridge is a 40-line zero-dependency Node script shipped at [`deploy/bridge/s1-secops-mcp-bridge.mjs`](./deploy/bridge/s1-secops-mcp-bridge.mjs).
96
+
97
+ Each team member installs the script once:
98
+
99
+ ```bash
100
+ mkdir -p ~/.local/bin
101
+ curl -fsSL https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/s1-secops-mcp/deploy/bridge/s1-secops-mcp-bridge.mjs \
102
+ -o ~/.local/bin/s1-secops-mcp-bridge.mjs
103
+ chmod +x ~/.local/bin/s1-secops-mcp-bridge.mjs
104
+ ```
105
+
106
+ Then adds this block to `claude_desktop_config.json`:
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "s1-secops-mcp": {
112
+ "command": "node",
113
+ "args": ["/Users/<you>/.local/bin/s1-secops-mcp-bridge.mjs"],
114
+ "env": {
115
+ "MCP_URL": "https://mcp.example.internal:8764/mcp",
116
+ "MCP_BEARER": "<your personal bearer token>"
117
+ }
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ Cmd+Q and reopen Claude Desktop. SentinelOne credentials live on the VM in `/etc/s1-secops-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).
124
+
125
+ ## Credentials
126
+
127
+ Credential keys, where to get each one, and the two token types are documented canonically in **[docs/credentials.md](../docs/credentials.md)**. This section adds the MCP-server-specific detail: which tools each key gates, and the server's full credential-resolution order.
128
+
129
+ `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).
130
+
131
+ `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.
132
+
133
+ `SDL_*` keys gate the SDL tools as follows:
134
+
135
+ | Variable | Description | Required for |
136
+ |----------|-------------|--------------|
137
+ | `S1_CONSOLE_URL` | Console URL, e.g. `https://usea1-acme.sentinelone.net` | All Mgmt + PowerQuery tools |
138
+ | `S1_CONSOLE_API_TOKEN` | Mgmt Console API token (Settings → Users → Service Users) | All Mgmt + PowerQuery + UAM tools |
139
+ | `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` |
140
+
141
+ ### Credential resolution order (highest priority wins)
142
+
143
+ 1. Environment variables (set in `claude_desktop_config.json` `env`, systemd `EnvironmentFile`, or your shell).
144
+ 2. `S1_CREDS_FILE`: explicit path to a JSON file (recommended for VM deployments and secret-store integrations).
145
+ 3. `COWORK_WORKSPACE/credentials.json`.
146
+ 4. Walk-up from the current working directory looking for `credentials.json`.
147
+ 5. `~/mnt/<folder>/credentials.json` (Cowork workspace mounts).
148
+ 6. `$CLAUDE_CONFIG_DIR/sentinelone/credentials.json`.
149
+ 7. `~/.config/sentinelone/credentials.json`.
150
+
151
+ The server logs the resolved credential source at startup so you can diagnose surprise overrides.
152
+
153
+ ## Transport modes
154
+
155
+ ### stdio (default)
156
+
157
+ The transport used by Claude Desktop, Claude Code, Claude Cowork, and any other client launched via `npx` / `node index.js`.
158
+
159
+ ```bash
160
+ s1-secops-mcp # auto-discovers credentials
161
+ node index.js # same as above, from a local clone
162
+ ```
163
+
164
+ ### Streamable HTTP
165
+
166
+ ```bash
167
+ s1-secops-mcp --transport http # 127.0.0.1:8765/mcp, no auth
168
+ s1-secops-mcp --transport http --host 0.0.0.0 # all interfaces, no auth (loud warning)
169
+ MCP_BEARER_TOKENS_FILE=/etc/s1-secops-mcp/bearer-tokens.json \
170
+ s1-secops-mcp --transport http --host 0.0.0.0 # team mode with per-user tokens
171
+ ```
172
+
173
+ Configuration via flags or environment variables:
174
+
175
+ | Flag | Env var | Default | Purpose |
176
+ |------|---------|---------|---------|
177
+ | `--transport` | `MCP_TRANSPORT` | `stdio` | `stdio` or `http`. |
178
+ | `--host` | `MCP_HTTP_HOST` | `127.0.0.1` | HTTP bind address. Use `0.0.0.0` for cross-host access. |
179
+ | `--port` | `MCP_HTTP_PORT` | `8765` | HTTP port. |
180
+ | `--path` | `MCP_HTTP_PATH` | `/mcp` | MCP endpoint path. |
181
+
182
+ In HTTP mode the server exposes:
183
+
184
+ - `POST /mcp`: accepts JSON-RPC, returns JSON-RPC. The MCP entry point.
185
+ - `GET /healthz`: returns `200 ok`. For load balancer probes; no auth.
186
+
187
+ ### Team auth: bearer tokens
188
+
189
+ To enable team auth, set one of:
190
+
191
+ - `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.
192
+ - `MCP_BEARER_TOKENS="token1,token2,..."` (fallback, no per-user names).
193
+
194
+ Token rotation:
195
+
196
+ ```bash
197
+ sudo vim /etc/s1-secops-mcp/bearer-tokens.json # add/remove entries
198
+ sudo systemctl reload s1-secops-mcp # SIGHUP, no connection drops
199
+ ```
200
+
201
+ 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.
202
+
203
+ ### Audit log
204
+
205
+ Every authenticated HTTP request emits a structured stderr line that systemd captures via journald:
206
+
207
+ ```
208
+ [audit] 2026-05-28T15:01:22.413Z | alice | tools/call | name=powerquery_run | 200 ok
209
+ [audit] 2026-05-28T15:01:34.221Z | bob | tools/list | - | 200 ok
210
+ [audit] 2026-05-28T17:03:11.221Z | - | - | - | 401 unauthorized
211
+ ```
212
+
213
+ ## Calling the HTTP endpoint directly
214
+
215
+ 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.
216
+
217
+ > **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 s1-secops-mcp/scripts/smoke-test-http.sh`. Good for new-team-member onboarding and post-rotation validation.
218
+
219
+ ### Endpoint contract
220
+
221
+ | Item | Value |
222
+ |---|---|
223
+ | Method | `POST` |
224
+ | URL | `https://<host>:<port>/mcp` (path is `/mcp` by default; configurable with `--path`) |
225
+ | `Content-Type` | `application/json` |
226
+ | `Authorization` | `Bearer <token>` (one of the tokens in `MCP_BEARER_TOKENS_FILE`) |
227
+ | Body | JSON-RPC 2.0 envelope |
228
+ | Response | JSON-RPC 2.0 envelope (`result` on success, `error` on failure) |
229
+
230
+ Health probe (no auth, no JSON): `GET /healthz` returns `200 ok`.
231
+
232
+ ### Initialize, then list tools, then call one (curl)
233
+
234
+ ```bash
235
+ HOST=mcp.s1.internal
236
+ TOKEN='your-bearer-token-here'
237
+
238
+ # 1. initialize (required first call per spec; advertises protocol version and capabilities)
239
+ curl -s -X POST "https://$HOST/mcp" \
240
+ -H "Authorization: Bearer $TOKEN" \
241
+ -H "Content-Type: application/json" \
242
+ -d '{
243
+ "jsonrpc": "2.0",
244
+ "id": 1,
245
+ "method": "initialize",
246
+ "params": {
247
+ "protocolVersion": "2024-11-05",
248
+ "capabilities": {},
249
+ "clientInfo": { "name": "my-script", "version": "1.0" }
250
+ }
251
+ }'
252
+ # -> {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{...},"serverInfo":{...}}}
253
+
254
+ # 2. list every tool the server exposes
255
+ curl -s -X POST "https://$HOST/mcp" \
256
+ -H "Authorization: Bearer $TOKEN" \
257
+ -H "Content-Type: application/json" \
258
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
259
+ | jq '.result.tools | length'
260
+ # -> 26
261
+
262
+ # 3. call a tool (here: list custom detection rules with the mandatory isLegacy=false)
263
+ curl -s -X POST "https://$HOST/mcp" \
264
+ -H "Authorization: Bearer $TOKEN" \
265
+ -H "Content-Type: application/json" \
266
+ -d '{
267
+ "jsonrpc": "2.0",
268
+ "id": 3,
269
+ "method": "tools/call",
270
+ "params": {
271
+ "name": "s1_api_get",
272
+ "arguments": {
273
+ "path": "/web/api/v2.1/cloud-detection/rules",
274
+ "params": { "isLegacy": false, "limit": 50 }
275
+ }
276
+ }
277
+ }' \
278
+ | jq '.result.content[0].text | fromjson | .pagination.totalItems'
279
+ ```
280
+
281
+ ### Python (requests)
282
+
283
+ ```python
284
+ import json
285
+ import requests
286
+
287
+ URL = "https://mcp.s1.internal/mcp"
288
+ TOKEN = "your-bearer-token-here"
289
+ HEADERS = {
290
+ "Authorization": f"Bearer {TOKEN}",
291
+ "Content-Type": "application/json",
292
+ }
293
+
294
+ def rpc(method, params=None, id=1):
295
+ body = {"jsonrpc": "2.0", "id": id, "method": method}
296
+ if params is not None:
297
+ body["params"] = params
298
+ r = requests.post(URL, headers=HEADERS, json=body, timeout=30)
299
+ r.raise_for_status()
300
+ return r.json()
301
+
302
+ # initialize once per session
303
+ rpc("initialize", {
304
+ "protocolVersion": "2024-11-05",
305
+ "capabilities": {},
306
+ "clientInfo": {"name": "python-client", "version": "1.0"},
307
+ }, id=0)
308
+
309
+ # list tools
310
+ tools = rpc("tools/list", id=1)["result"]["tools"]
311
+ print(f"{len(tools)} tools available")
312
+
313
+ # call a tool
314
+ resp = rpc("tools/call", {
315
+ "name": "powerquery_run",
316
+ "arguments": {
317
+ "query": "dataSource.name=* | group count=count() by dataSource.name | sort -count | limit 10",
318
+ "hours": 24,
319
+ },
320
+ }, id=2)
321
+
322
+ # Tool results live in result.content[0].text as a JSON string.
323
+ payload = json.loads(resp["result"]["content"][0]["text"])
324
+ print(json.dumps(payload, indent=2))
325
+ ```
326
+
327
+ ### Node (built-in fetch, Node 18+)
328
+
329
+ ```javascript
330
+ const URL = 'https://mcp.s1.internal/mcp';
331
+ const TOKEN = process.env.MCP_BEARER;
332
+
333
+ async function rpc(method, params, id = 1) {
334
+ const body = { jsonrpc: '2.0', id, method };
335
+ if (params !== undefined) body.params = params;
336
+ const res = await fetch(URL, {
337
+ method: 'POST',
338
+ headers: {
339
+ 'Authorization': `Bearer ${TOKEN}`,
340
+ 'Content-Type': 'application/json',
341
+ },
342
+ body: JSON.stringify(body),
343
+ });
344
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
345
+ return res.json();
346
+ }
347
+
348
+ await rpc('initialize', {
349
+ protocolVersion: '2024-11-05',
350
+ capabilities: {},
351
+ clientInfo: { name: 'node-client', version: '1.0' },
352
+ }, 0);
353
+
354
+ const { result } = await rpc('tools/list', null, 1);
355
+ console.log(`${result.tools.length} tools available`);
356
+
357
+ const call = await rpc('tools/call', {
358
+ name: 'uam_list_alerts',
359
+ arguments: { first: 20, status: 'NEW' },
360
+ }, 2);
361
+ console.log(JSON.parse(call.result.content[0].text));
362
+ ```
363
+
364
+ ### JSON-RPC envelope shapes
365
+
366
+ **Success response:**
367
+
368
+ ```json
369
+ {
370
+ "jsonrpc": "2.0",
371
+ "id": 2,
372
+ "result": { ... method-specific payload ... }
373
+ }
374
+ ```
375
+
376
+ **Error response:**
377
+
378
+ ```json
379
+ {
380
+ "jsonrpc": "2.0",
381
+ "id": 2,
382
+ "error": {
383
+ "code": -32602,
384
+ "message": "Tool not found: bad_tool_name"
385
+ }
386
+ }
387
+ ```
388
+
389
+ **Notifications** (one-way messages with no `id`, e.g. `notifications/initialized`):
390
+
391
+ ```bash
392
+ curl -i -s -X POST "https://$HOST/mcp" \
393
+ -H "Authorization: Bearer $TOKEN" \
394
+ -H "Content-Type: application/json" \
395
+ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
396
+ # -> HTTP/2 202 (no body, per JSON-RPC spec)
397
+ ```
398
+
399
+ ### Error codes you'll actually see
400
+
401
+ | HTTP | JSON-RPC code | Meaning |
402
+ |---|---|---|
403
+ | 200 | (none, has `result`) | Success |
404
+ | 200 | `-32601` | Method not found (e.g. typo in method name) |
405
+ | 200 | `-32602` | Invalid params (tool not found, missing required arg) |
406
+ | 200 | `-32603` | Tool handler threw: upstream S1 API error usually |
407
+ | 400 | `-32700` | Parse error (malformed JSON body) |
408
+ | 400 | `-32600` | Invalid request (e.g. JSON-RPC batch, not supported) |
409
+ | 401 | `-32001` | Missing or invalid bearer token |
410
+ | 405 | (none) | Wrong HTTP method on `/mcp` (only POST is accepted) |
411
+ | 413 | `-32600` | Body exceeds 4 MB |
412
+
413
+ ### Tool inputs and outputs
414
+
415
+ 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:
416
+
417
+ ```json
418
+ {
419
+ "content": [
420
+ { "type": "text", "text": "<JSON-encoded result>" }
421
+ ],
422
+ "isError": false
423
+ }
424
+ ```
425
+
426
+ 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.
427
+
428
+ ### Row / page limits (measured, not enforced)
429
+
430
+ The `maxRows` (`powerquery_run`) and `first` (`uam_list_alerts`) parameters are soft client-side hints, not hard backend caps. Live-verified 2026-07-29:
431
+
432
+ - `powerquery_run` `maxRows`: default 1000, but not a ceiling. The LRQ engine returns as many rows as the query's own `| limit N` asks for. A `| limit 20000` query with `maxRows: 20000` returned 20,000 rows in a single response. Set `maxRows` to match a large `| limit`; the practical ceiling is LRQ response size, not a fixed 5000. Prefer aggregating in the query (`| group ... | limit N`) over pulling tens of thousands of raw rows.
433
+ - `uam_list_alerts` `first`: default 20, not enforced client-side. The UAM GraphQL backend accepts larger pages: `first: 500` returned 500 alerts with `pageInfo.hasNextPage: true`. Paginate with the returned `pageInfo.endCursor` via `after` rather than requesting one unbounded page.
434
+
435
+ ## CLI reference
436
+
437
+ ```
438
+ s1-secops-mcp [options]
439
+
440
+ OPTIONS
441
+ --transport <stdio|http> Transport. Default: stdio.
442
+ --host <host> HTTP bind address. Default: 127.0.0.1.
443
+ --port <port> HTTP port. Default: 8765.
444
+ --path <path> HTTP MCP endpoint path. Default: /mcp.
445
+ -h, --help Show help.
446
+ -v, --version Show server version.
447
+ ```
448
+
449
+ ## Architecture
450
+
451
+ ```
452
+ s1-secops-mcp/
453
+ index.js Entry: flag parsing + transport selection
454
+ lib/
455
+ server-core.js Tool registry, JSON-RPC dispatch (transport-agnostic)
456
+ stdio-transport.js stdin/stdout JSON-RPC loop
457
+ http-transport.js Streamable HTTP (node:http, zero deps)
458
+ auth.js Bearer token allowlist with SIGHUP reload
459
+ credentials.js S1 + SDL credential resolution
460
+ s1.js Mgmt REST + LRQ PowerQuery + Purple AI + UAM GraphQL
461
+ sdl.js SDL config files + V1 query
462
+ uam-ingest.js HEC alert/indicator ingestion
463
+ tools/
464
+ powerquery.js PowerQuery enumerate/run/schema-discover
465
+ mgmt-console.js S1 REST verbs + Purple AI summary + UAM
466
+ sdl-api.js SDL config file + log ingestion tools
467
+ hyperautomation.js Hyperautomation list/get/import/export/delete
468
+ uam-ingest.js UAM Alert Interface ingestion tools
469
+ deploy/
470
+ install.sh One-shot installer (Mac and Linux)
471
+ systemd/ Service unit for Linux VM deployments
472
+ caddy/ TLS reverse proxy template
473
+ README.md Deployment walkthrough
474
+ scripts/
475
+ regen-readme-tools-table.mjs Tools-table regenerator (no drift)
476
+ tests/ Smoke + stdio + HTTP test suites (node --test)
477
+ ```
478
+
479
+ ## Auth patterns (implemented)
480
+
481
+ | API surface | Auth header | Key |
482
+ |-------------|-------------|-----|
483
+ | S1 Mgmt REST API | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
484
+ | LRQ PowerQuery | `Authorization: Bearer <jwt>` | Same token, different prefix |
485
+ | Purple AI GraphQL | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
486
+ | UAM GraphQL | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
487
+ | UAM HEC ingest | `Authorization: Bearer <jwt>` | `S1_CONSOLE_API_TOKEN` |
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.
502
+
503
+ ## Updating CLAUDE.md
504
+
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.
516
+
517
+ ## Version history
518
+
519
+ See [CHANGELOG.md](./CHANGELOG.md).