@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.
@@ -0,0 +1,110 @@
1
+ # Caddyfile for s1-secops-mcp behind TLS on a private network.
2
+ #
3
+ # Two adjustments before using:
4
+ # 1. Replace mcp.s1.internal with your DNS name (or Tailscale hostname).
5
+ # 2. Decide on a TLS strategy:
6
+ # tls internal Caddy's built-in CA (good for private networks;
7
+ # distribute the root cert to clients).
8
+ # tls <email> Let's Encrypt over HTTP-01 (needs public DNS + port 80).
9
+ # tls /path/to/cert.pem /path/to/key.pem
10
+ # Bring your own cert from an internal PKI.
11
+ #
12
+ # AWS users: Let's Encrypt refuses to issue for *.amazonaws.com hostnames by
13
+ # policy. When you use the `tls <email>` shorthand, Caddy automatically falls
14
+ # back to ZeroSSL (also free, also publicly trusted, no policy block). The
15
+ # end-state cert should have `issuer=ZeroSSL ECC DV SSL CA 2`. Port 80 must
16
+ # be open in the SG for HTTP-01 to complete.
17
+ #
18
+ # DO NOT mix `tls <email>` with an `issuer acme { ... }` block: Caddy errors
19
+ # with "cannot mix issuer subdirective with other issuer-specific subdirectives".
20
+ # Pick one form: the email shorthand on its own line, OR a full `tls { ... }`
21
+ # block with the email moved inside the `issuer acme` stanza. Not both.
22
+ #
23
+ # Why Caddy and not nginx? Caddy auto-renews certs, has zero config for SSE
24
+ # streaming (flush_interval handled), and is one binary with no system Python
25
+ # or Lua dependency. Equally valid alternative: nginx with the snippet at the
26
+ # bottom of this file.
27
+
28
+ mcp.s1.internal {
29
+ tls internal
30
+
31
+ # Strict bearer-token enforcement. The MCP server also enforces tokens
32
+ # when MCP_BEARER_TOKENS_FILE is set, but checking here too means we never
33
+ # forward unauthenticated traffic to the backend.
34
+ #
35
+ # To bypass Caddy auth (and rely solely on the MCP server's enforcement),
36
+ # delete the @authorized matcher and the handle blocks below, leaving only
37
+ # the reverse_proxy.
38
+ @anyAuth header_regexp Authorization "^Bearer\s+\S+$"
39
+
40
+ handle @anyAuth {
41
+ reverse_proxy 127.0.0.1:8765 {
42
+ # MCP responses are streamed; flush immediately so partial replies
43
+ # reach the client without sitting in a buffer.
44
+ flush_interval -1
45
+
46
+ # Reasonable timeouts for long-running PowerQueries (LRQ can take
47
+ # ~30s on heavy queries). Increase if you see 504s on big queries.
48
+ transport http {
49
+ read_timeout 90s
50
+ write_timeout 90s
51
+ response_header_timeout 30s
52
+ }
53
+ }
54
+ }
55
+
56
+ handle {
57
+ respond "unauthorized" 401 {
58
+ close
59
+ }
60
+ }
61
+
62
+ # Standard security headers.
63
+ header {
64
+ Strict-Transport-Security "max-age=31536000; includeSubDomains"
65
+ X-Content-Type-Options "nosniff"
66
+ Referrer-Policy "no-referrer"
67
+ -Server
68
+ }
69
+
70
+ # Healthcheck pass-through (no auth needed on /healthz).
71
+ @health path /healthz /health
72
+ handle @health {
73
+ reverse_proxy 127.0.0.1:8765
74
+ }
75
+
76
+ # Access log to journald via systemd-journald, structured JSON.
77
+ log {
78
+ output stdout
79
+ format json
80
+ level INFO
81
+ }
82
+ }
83
+
84
+ # ─── nginx equivalent (commented; copy if you prefer nginx) ──────────────────
85
+ # upstream mcp_backend {
86
+ # server 127.0.0.1:8765;
87
+ # keepalive 16;
88
+ # }
89
+ # server {
90
+ # listen 443 ssl http2;
91
+ # server_name mcp.s1.internal;
92
+ # ssl_certificate /etc/ssl/certs/mcp.s1.internal.crt;
93
+ # ssl_certificate_key /etc/ssl/private/mcp.s1.internal.key;
94
+ #
95
+ # location = /healthz {
96
+ # proxy_pass http://mcp_backend;
97
+ # }
98
+ #
99
+ # location / {
100
+ # if ($http_authorization !~ "^Bearer\s+\S+$") {
101
+ # return 401;
102
+ # }
103
+ # proxy_pass http://mcp_backend;
104
+ # proxy_http_version 1.1;
105
+ # proxy_set_header Connection "";
106
+ # proxy_buffering off;
107
+ # proxy_read_timeout 90s;
108
+ # proxy_send_timeout 90s;
109
+ # }
110
+ # }
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # s1-secops-mcp installer for macOS and Linux.
4
+ #
5
+ # Modes:
6
+ # --user (default) Install for the current user only.
7
+ # Writes credentials to ~/.config/sentinelone/credentials.json,
8
+ # installs the npm package globally via the current Node toolchain.
9
+ #
10
+ # --server Linux VM deployment. Creates a system `mcp` user, installs the
11
+ # npm package globally, writes credentials and bearer tokens to
12
+ # /etc/s1-secops-mcp/, drops the systemd unit, enables and
13
+ # starts the service.
14
+ #
15
+ # Idempotent: rerunning is safe; it skips steps already completed.
16
+ #
17
+ # Exit codes: 0 ok, 1 generic failure, 2 unsupported platform, 3 missing prereq.
18
+
19
+ set -euo pipefail
20
+
21
+ # Token and credential files must never be world-readable, even for the
22
+ # instant between creation and the explicit chmod 600 below.
23
+ umask 077
24
+
25
+ # ─── helpers ─────────────────────────────────────────────────────────────────
26
+
27
+ c_red() { printf '\033[31m%s\033[0m\n' "$*"; }
28
+ c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
29
+ c_yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
30
+ c_bold() { printf '\033[1m%s\033[0m\n' "$*"; }
31
+
32
+ step() { c_bold ">> $*"; }
33
+ ok() { c_green " ok: $*"; }
34
+ warn() { c_yellow " warn: $*"; }
35
+ die() {
36
+ c_red " error: $*"
37
+ exit 1
38
+ }
39
+
40
+ PKG="@pmoses-s1/s1-secops-mcp"
41
+ MODE="user"
42
+
43
+ while [[ $# -gt 0 ]]; do
44
+ case "$1" in
45
+ --user)
46
+ MODE="user"
47
+ shift
48
+ ;;
49
+ --server)
50
+ MODE="server"
51
+ shift
52
+ ;;
53
+ -h | --help)
54
+ cat <<EOF
55
+ Usage: $0 [--user|--server]
56
+
57
+ --user Install for current user (default).
58
+ Default install path on macOS: ~/.config/sentinelone/
59
+ Default install path on Linux: ~/.config/sentinelone/
60
+
61
+ --server Install on a Linux VM as a shared service.
62
+ System path: /etc/s1-secops-mcp/
63
+ systemd unit: s1-secops-mcp.service
64
+ Requires sudo.
65
+
66
+ EOF
67
+ exit 0
68
+ ;;
69
+ *) die "Unknown flag: $1 (try --help)" ;;
70
+ esac
71
+ done
72
+
73
+ OS="$(uname -s)"
74
+ case "$OS" in
75
+ Darwin) PLATFORM="mac" ;;
76
+ Linux) PLATFORM="linux" ;;
77
+ *)
78
+ c_red "Unsupported platform: $OS"
79
+ exit 2
80
+ ;;
81
+ esac
82
+
83
+ if [[ "$MODE" == "server" && "$PLATFORM" != "linux" ]]; then
84
+ die "--server mode is Linux only (got $PLATFORM)"
85
+ fi
86
+
87
+ # ─── prereqs ─────────────────────────────────────────────────────────────────
88
+
89
+ step "Checking prerequisites"
90
+
91
+ if ! command -v node >/dev/null 2>&1; then
92
+ c_red "Node.js is required but not found on PATH."
93
+ c_red "Install Node 18+:"
94
+ c_red " macOS: brew install node@20"
95
+ c_red " Linux: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs"
96
+ exit 3
97
+ fi
98
+ NODE_MAJOR="$(node --version | sed 's/^v\([0-9]*\).*/\1/')"
99
+ if [[ "$NODE_MAJOR" -lt 18 ]]; then
100
+ die "Node $(node --version) is too old. Need Node 18+."
101
+ fi
102
+ ok "node $(node --version)"
103
+
104
+ if ! command -v npm >/dev/null 2>&1; then
105
+ die "npm not found alongside node; please install Node 18+ from nodejs.org or your package manager."
106
+ fi
107
+ ok "npm $(npm --version)"
108
+
109
+ if [[ "$MODE" == "server" ]]; then
110
+ if [[ "$EUID" -ne 0 ]]; then
111
+ die "--server mode must be run with sudo (need to create /etc/s1-secops-mcp/, system user, and systemd unit)."
112
+ fi
113
+ command -v systemctl >/dev/null 2>&1 || die "systemctl not found; this script targets systemd-based Linux."
114
+ ok "running as root, systemd present"
115
+ fi
116
+
117
+ # ─── install package ─────────────────────────────────────────────────────────
118
+
119
+ step "Installing $PKG globally"
120
+ if [[ "$MODE" == "server" ]]; then
121
+ npm install -g "$PKG" >/dev/null
122
+ else
123
+ # Avoid sudo on Mac/personal Linux: use a per-user npm prefix if not already.
124
+ if ! npm config get prefix --location=user 2>/dev/null | grep -qE '^/'; then
125
+ npm config set prefix "$HOME/.npm-global"
126
+ case ":$PATH:" in
127
+ *":$HOME/.npm-global/bin:"*) ;;
128
+ *) warn "Add $HOME/.npm-global/bin to your PATH (currently missing)." ;;
129
+ esac
130
+ fi
131
+ npm install -g "$PKG" >/dev/null
132
+ fi
133
+ ok "$(npm ls -g --depth=0 "$PKG" 2>/dev/null | grep "$PKG" | head -1 | sed 's/.*-> //' || echo installed)"
134
+
135
+ # ─── credentials skeleton ────────────────────────────────────────────────────
136
+
137
+ if [[ "$MODE" == "server" ]]; then
138
+ CONF_DIR="/etc/s1-secops-mcp"
139
+ CRED_PATH="$CONF_DIR/credentials.json"
140
+ TOKEN_PATH="$CONF_DIR/bearer-tokens.json"
141
+ ENV_PATH="$CONF_DIR/server.env"
142
+ OWNER="mcp"
143
+ else
144
+ CONF_DIR="$HOME/.config/sentinelone"
145
+ CRED_PATH="$CONF_DIR/credentials.json"
146
+ TOKEN_PATH=""
147
+ ENV_PATH=""
148
+ OWNER="$USER"
149
+ fi
150
+
151
+ step "Setting up $CONF_DIR"
152
+ mkdir -p "$CONF_DIR"
153
+ if [[ ! -f "$CRED_PATH" ]]; then
154
+ cat >"$CRED_PATH" <<'EOF'
155
+ {
156
+ "S1_CONSOLE_URL": "https://usea1-acme.sentinelone.net",
157
+ "S1_CONSOLE_API_TOKEN": "REPLACE_WITH_API_TOKEN",
158
+ "S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net"
159
+ }
160
+ EOF
161
+ chmod 600 "$CRED_PATH"
162
+ ok "wrote $CRED_PATH (placeholder, edit before starting)"
163
+ else
164
+ ok "$CRED_PATH already exists, leaving untouched"
165
+ fi
166
+
167
+ if [[ "$MODE" == "server" ]]; then
168
+ if ! id "$OWNER" >/dev/null 2>&1; then
169
+ step "Creating system user '$OWNER'"
170
+ useradd --system --no-create-home --shell /usr/sbin/nologin "$OWNER"
171
+ ok "created"
172
+ else
173
+ ok "user '$OWNER' already exists"
174
+ fi
175
+ chown -R "$OWNER":"$OWNER" "$CONF_DIR"
176
+ chmod 600 "$CRED_PATH"
177
+
178
+ if [[ ! -f "$TOKEN_PATH" ]]; then
179
+ step "Generating initial bearer token"
180
+ if command -v openssl >/dev/null 2>&1; then
181
+ TOKEN_ADMIN="$(openssl rand -hex 32)"
182
+ else
183
+ TOKEN_ADMIN="$(node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))')"
184
+ fi
185
+ cat >"$TOKEN_PATH" <<EOF
186
+ {
187
+ "admin": "$TOKEN_ADMIN"
188
+ }
189
+ EOF
190
+ chmod 600 "$TOKEN_PATH"
191
+ chown "$OWNER":"$OWNER" "$TOKEN_PATH"
192
+ ok "wrote $TOKEN_PATH (one initial admin token)"
193
+ c_yellow " INITIAL ADMIN BEARER TOKEN:"
194
+ c_yellow " $TOKEN_ADMIN"
195
+ c_yellow " Save this value now; it is also stored in $TOKEN_PATH."
196
+ else
197
+ ok "$TOKEN_PATH already exists"
198
+ fi
199
+
200
+ if [[ ! -f "$ENV_PATH" ]]; then
201
+ cat >"$ENV_PATH" <<EOF
202
+ # Environment file for s1-secops-mcp.service.
203
+ # Adjust LOG_LEVEL or override anything here; apply with: systemctl restart s1-secops-mcp
204
+ # (systemd only re-reads EnvironmentFile on restart; reload/SIGHUP re-reads bearer tokens only.)
205
+ EOF
206
+ chmod 600 "$ENV_PATH"
207
+ chown "$OWNER":"$OWNER" "$ENV_PATH"
208
+ ok "wrote $ENV_PATH"
209
+ else
210
+ ok "$ENV_PATH already exists"
211
+ fi
212
+
213
+ step "Installing systemd unit"
214
+ SVC_PATH="/etc/systemd/system/s1-secops-mcp.service"
215
+ GLOBAL_NODE_MODULES="$(npm root -g)"
216
+ SCRIPT_DIR="$GLOBAL_NODE_MODULES/$PKG"
217
+ # Rewrite the ExecStart path to point at the resolved global install,
218
+ # since the bundled unit uses %h which assumes per-user install.
219
+ sed "s|%h/.npm-global/lib/node_modules/@pmoses-s1/s1-secops-mcp|$SCRIPT_DIR|g" \
220
+ "$SCRIPT_DIR/deploy/systemd/s1-secops-mcp.service" >"$SVC_PATH"
221
+ systemctl daemon-reload
222
+ systemctl enable s1-secops-mcp >/dev/null 2>&1
223
+ ok "wrote $SVC_PATH and enabled the service"
224
+
225
+ step "Starting the service"
226
+ if systemctl is-active s1-secops-mcp >/dev/null 2>&1; then
227
+ systemctl restart s1-secops-mcp
228
+ ok "restarted"
229
+ else
230
+ systemctl start s1-secops-mcp
231
+ ok "started"
232
+ fi
233
+ sleep 1
234
+ if systemctl is-active --quiet s1-secops-mcp; then
235
+ ok "service is active"
236
+ else
237
+ c_red "service failed to start. Recent log lines:"
238
+ journalctl -u s1-secops-mcp -n 30 --no-pager | sed 's/^/ /'
239
+ exit 1
240
+ fi
241
+ fi
242
+
243
+ # ─── final notes ─────────────────────────────────────────────────────────────
244
+
245
+ step "Next steps"
246
+ if [[ "$MODE" == "user" ]]; then
247
+ cat <<EOF
248
+
249
+ 1. Edit $CRED_PATH with your real SentinelOne values.
250
+ 2. Try the server:
251
+ s1-secops-mcp --version
252
+ s1-secops-mcp --help
253
+ 3. Wire it into Claude Cowork / Claude Desktop / Claude Code via stdio
254
+ (no HTTP needed for single-user local). See deploy/README.md for the
255
+ exact config block.
256
+
257
+ EOF
258
+ elif [[ "$MODE" == "server" ]]; then
259
+ cat <<EOF
260
+
261
+ 1. Edit $CRED_PATH with your real SentinelOne values, then restart:
262
+ sudo systemctl restart s1-secops-mcp
263
+ (credentials.json is read once at startup; reload/SIGHUP only re-reads
264
+ bearer tokens, so credential changes need a full restart.)
265
+ 2. Verify the server is up:
266
+ curl -s http://127.0.0.1:8765/healthz
267
+ 3. Put TLS in front (Caddy template at $SCRIPT_DIR/deploy/caddy/Caddyfile.example).
268
+ 4. Add team members by editing $TOKEN_PATH and reloading:
269
+ echo '{"admin":"...", "alice":"...", "bob":"..."}' > $TOKEN_PATH
270
+ sudo systemctl reload s1-secops-mcp
271
+ (Reload sends SIGHUP; no connection drops.)
272
+ 5. Tail the audit log:
273
+ sudo journalctl -u s1-secops-mcp -f | grep '\[audit\]'
274
+
275
+ See deploy/README.md for the full Linux VM walkthrough.
276
+
277
+ EOF
278
+ fi
279
+
280
+ c_green "Done."
@@ -0,0 +1,59 @@
1
+ [Unit]
2
+ Description=SentinelOne MCP server (Streamable HTTP, team-shared)
3
+ Documentation=https://github.com/pmoses-s1/claude-skills/tree/main/s1-secops-mcp
4
+ After=network-online.target
5
+ Wants=network-online.target
6
+
7
+ [Service]
8
+ Type=simple
9
+ User=mcp
10
+ Group=mcp
11
+
12
+ # Server credentials (the S1 service-user token + SDL keys) and bearer tokens.
13
+ # Both are written by deploy/install.sh. Bearer tokens rotate via
14
+ # `systemctl reload` (SIGHUP, no dropped connections). credentials.json is
15
+ # read once at startup, so changes to it require `systemctl restart`.
16
+ EnvironmentFile=/etc/s1-secops-mcp/server.env
17
+ Environment=MCP_BEARER_TOKENS_FILE=/etc/s1-secops-mcp/bearer-tokens.json
18
+ Environment=S1_CREDS_FILE=/etc/s1-secops-mcp/credentials.json
19
+
20
+ ExecStart=/usr/bin/env node %h/.npm-global/lib/node_modules/@pmoses-s1/s1-secops-mcp/index.js \
21
+ --transport http \
22
+ --host 127.0.0.1 \
23
+ --port 8765 \
24
+ --path /mcp
25
+
26
+ # SIGHUP reloads the bearer token file without restart.
27
+ ExecReload=/bin/kill -HUP $MAINPID
28
+
29
+ Restart=on-failure
30
+ RestartSec=5
31
+
32
+ # Hardening
33
+ # NOTE: MemoryDenyWriteExecute=true and LockPersonality=true are incompatible
34
+ # with Node.js V8 JIT (W+X mappings). Including them causes a silent SIGTRAP
35
+ # at startup with no useful log output. Leave them off for Node services.
36
+ NoNewPrivileges=true
37
+ PrivateTmp=true
38
+ ProtectSystem=strict
39
+ ProtectHome=true
40
+ ProtectKernelTunables=true
41
+ ProtectKernelModules=true
42
+ ProtectControlGroups=true
43
+ RestrictNamespaces=true
44
+ RestrictRealtime=true
45
+ RestrictSUIDSGID=true
46
+ SystemCallArchitectures=native
47
+ ReadWritePaths=
48
+
49
+ # Resource limits
50
+ LimitNOFILE=4096
51
+ TasksMax=64
52
+
53
+ # Logging
54
+ StandardOutput=journal
55
+ StandardError=journal
56
+ SyslogIdentifier=s1-secops-mcp
57
+
58
+ [Install]
59
+ WantedBy=multi-user.target
package/index.js ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SentinelOne MCP Server
4
+ *
5
+ * Implements the Model Context Protocol over either stdio (default) or
6
+ * Streamable HTTP, using raw JSON-RPC 2.0 throughout. No external runtime
7
+ * dependencies. Pure Node.js 18+.
8
+ *
9
+ * Exposes 26 tools across PowerQuery, Mgmt Console REST, UAM, SDL API,
10
+ * Hyperautomation, and UAM Ingest; plus 2 resources and 2 prompts.
11
+ *
12
+ * Quick start:
13
+ * stdio (default, used by Claude Desktop / Claude Code / Cowork):
14
+ * node index.js
15
+ *
16
+ * Streamable HTTP, bound to localhost, no auth (single-user local):
17
+ * node index.js --transport http
18
+ *
19
+ * Streamable HTTP, bound to 0.0.0.0 with team bearer tokens:
20
+ * MCP_BEARER_TOKENS_FILE=/etc/s1-secops-mcp/bearer-tokens.json \
21
+ * node index.js --transport http --host 0.0.0.0 --port 8765
22
+ *
23
+ * Full configuration reference: README.md and deploy/README.md.
24
+ */
25
+
26
+ import { dispatch, SERVER_INFO, ALL_TOOLS } from './lib/server-core.js';
27
+ import { getCreds, hasS1Creds, hasSdlCreds } from './lib/credentials.js';
28
+ import { hasHecCreds } from './lib/uam-ingest.js';
29
+ import { loadTokens, installSighupReload } from './lib/auth.js';
30
+
31
+ function log(...args) {
32
+ process.stderr.write('[s1-secops-mcp] ' + args.join(' ') + '\n');
33
+ }
34
+
35
+ // ─── CLI flag parser ──────────────────────────────────────────────────────────
36
+
37
+ function parseArgs(argv) {
38
+ const out = {
39
+ transport: process.env.MCP_TRANSPORT || 'stdio',
40
+ host: process.env.MCP_HTTP_HOST || '127.0.0.1',
41
+ port: Number(process.env.MCP_HTTP_PORT) || 8765,
42
+ path: process.env.MCP_HTTP_PATH || '/mcp',
43
+ help: false,
44
+ version: false,
45
+ };
46
+
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const a = argv[i];
49
+ const next = () => argv[++i];
50
+ switch (a) {
51
+ case '-h': case '--help': out.help = true; break;
52
+ case '-v': case '--version': out.version = true; break;
53
+ case '--transport': out.transport = next(); break;
54
+ case '--host': out.host = next(); break;
55
+ case '--port': out.port = Number(next()); break;
56
+ case '--path': out.path = next(); break;
57
+ default:
58
+ if (a.startsWith('--')) {
59
+ process.stderr.write(`Unknown flag: ${a}\nRun with --help for usage.\n`);
60
+ process.exit(2);
61
+ }
62
+ }
63
+ }
64
+
65
+ if (!['stdio', 'http'].includes(out.transport)) {
66
+ process.stderr.write(`Invalid --transport: ${out.transport} (expected: stdio, http)\n`);
67
+ process.exit(2);
68
+ }
69
+ if (out.transport === 'http' && (!out.port || out.port <= 0 || out.port > 65535)) {
70
+ process.stderr.write(`Invalid --port: ${out.port}\n`);
71
+ process.exit(2);
72
+ }
73
+ if (!out.path.startsWith('/')) {
74
+ process.stderr.write(`Invalid --path: ${out.path} (must start with /)\n`);
75
+ process.exit(2);
76
+ }
77
+
78
+ return out;
79
+ }
80
+
81
+ function printHelp() {
82
+ process.stdout.write(`\
83
+ s1-secops-mcp ${SERVER_INFO.version}
84
+
85
+ USAGE
86
+ s1-secops-mcp [options]
87
+
88
+ OPTIONS
89
+ --transport <stdio|http> Transport to use. Default: stdio.
90
+ --host <host> HTTP bind address. Default: 127.0.0.1.
91
+ --port <port> HTTP port. Default: 8765.
92
+ --path <path> HTTP MCP endpoint path. Default: /mcp.
93
+ -h, --help Show this help.
94
+ -v, --version Show server version.
95
+
96
+ ENVIRONMENT
97
+ MCP_TRANSPORT Same as --transport.
98
+ MCP_HTTP_HOST Same as --host.
99
+ MCP_HTTP_PORT Same as --port.
100
+ MCP_HTTP_PATH Same as --path.
101
+
102
+ MCP_BEARER_TOKENS_FILE Path to a JSON file mapping {name: token} for
103
+ per-user authenticated HTTP access. Recommended
104
+ for teams. SIGHUP reloads without restart.
105
+ MCP_BEARER_TOKENS Comma-separated raw tokens (no per-user names).
106
+ Fallback when MCP_BEARER_TOKENS_FILE is not set.
107
+
108
+ S1_CONSOLE_URL Console URL, e.g. https://usea1-acme.sentinelone.net
109
+ S1_CONSOLE_API_TOKEN Mgmt Console API token. Required for most tools.
110
+ S1_HEC_INGEST_URL HEC ingest host. Required for uam_ingest_alert,
111
+ uam_post_indicators, uam_post_alert.
112
+ S1_CREDS_FILE Explicit path to a credentials.json file.
113
+ Highest priority for credential resolution.
114
+ S1_CLAUDE_MD_PATH Absolute path to CLAUDE.md for the soc_analyst
115
+ prompt and sentinelone://soc-context resource.
116
+
117
+ EXAMPLES
118
+ Run as a local MCP server for Claude Desktop / Cowork:
119
+ s1-secops-mcp
120
+
121
+ Run as an HTTP service for personal use:
122
+ s1-secops-mcp --transport http
123
+ # then: curl -s http://127.0.0.1:8765/healthz
124
+
125
+ Run as a shared team service with token auth:
126
+ MCP_BEARER_TOKENS_FILE=/etc/s1-secops-mcp/bearer-tokens.json \\
127
+ s1-secops-mcp --transport http --host 0.0.0.0 --port 8765
128
+ # See deploy/README.md for the full Linux VM walkthrough.
129
+ `);
130
+ }
131
+
132
+ // ─── Main ────────────────────────────────────────────────────────────────────
133
+
134
+ async function main() {
135
+ const opts = parseArgs(process.argv.slice(2));
136
+
137
+ if (opts.help) { printHelp(); process.exit(0); }
138
+ if (opts.version) { process.stdout.write(`${SERVER_INFO.version}\n`); process.exit(0); }
139
+
140
+ log(`Starting ${SERVER_INFO.name} v${SERVER_INFO.version} (node ${process.version})`);
141
+
142
+ const creds = getCreds();
143
+ log(`S1 Mgmt API: ${hasS1Creds() ? 'configured (' + creds.S1_CONSOLE_URL + ')' : 'NOT configured'}`);
144
+ log(`SDL API: ${hasSdlCreds() ? 'configured (' + creds.S1_CONSOLE_URL + '/sdl)' : 'NOT configured'}`);
145
+ log(`UAM Ingest: ${hasHecCreds() ? 'configured (' + creds.S1_HEC_INGEST_URL + ')' : 'NOT configured (add S1_HEC_INGEST_URL)'}`);
146
+ log(`Tools: ${ALL_TOOLS.length} registered`);
147
+
148
+ if (opts.transport === 'http') {
149
+ try {
150
+ loadTokens();
151
+ } catch (e) {
152
+ process.stderr.write(`[auth] FATAL: ${e.message}\n`);
153
+ process.exit(1);
154
+ }
155
+ installSighupReload();
156
+
157
+ const { startHttp } = await import('./lib/http-transport.js');
158
+ await startHttp(dispatch, { port: opts.port, host: opts.host, path: opts.path });
159
+ log('HTTP transport ready. Press Ctrl+C to stop.');
160
+ return;
161
+ }
162
+
163
+ // Default: stdio
164
+ const { startStdio } = await import('./lib/stdio-transport.js');
165
+ await startStdio(dispatch);
166
+ }
167
+
168
+ main().catch(e => {
169
+ process.stderr.write(`Fatal: ${e.message}\n${e.stack}\n`);
170
+ process.exit(1);
171
+ });