splunk-soar-mcp
An MCP server for Splunk SOAR (formerly Phantom).
It gives an AI assistant a first-class view of a SOAR instance — apps, assets, playbooks, containers, artifacts, custom functions and custom lists — plus the two things that actually make SOAR work go faster: playbook run logs for debugging, and a visual editor block generator that emits payloads you paste straight onto the playbook canvas.
You: why did playbook run 88214 fail?
→ soar_get_playbook_run_log(88214)
→ soar_get_playbook_source("Triage", block="build_digest")
The format block reads cf_lookup:custom_function:result, but cf_lookup is a
custom function block — that form is the code-block shape. It needs
cf_lookup:custom_function_result.data.result.
Contents
- Why
- Safety model
- Install
- Configure
- Connect a client
- Tools
- Resources and prompts
- HTTP transport
- Development
- Security
Why
The SOAR REST API is capable but awkward: filter values need literal quotes inside the query string, half the useful data hangs off endpoints you have to know the name of, and the clipboard format the playbook editor uses is not documented anywhere. This server wraps all of that so an assistant can answer real questions — which asset does this action run against, what did that run actually return, why did this block fail — without a REST call being hand-written each time.
Three things it does that a generic REST wrapper does not:
- Run logs, sliced.
soar_get_playbook_run_logpulls the full debug log for one run and greps it, so a failing block is one call away rather than a scroll through the UI. - Playbook source by block.
soar_get_playbook_source(pb, block="format_1")returns one function instead of a 2000-line file — the generated python is the ground truth for block names and datapaths. - Paste-able editor blocks.
soar_build_*emits the base64 the playbook editor accepts, with the fields the editor silently rejects a node for omitting already filled in.soar_decode_vpe_blockgoes the other way, so you can learn a node's real shape by copying a working block out of the UI.
Safety model
Read this first. These modes are guardrails, not a security boundary. They stop an assistant from wandering; they stop nobody who holds the API token, because that person can bypass this server with one
curl. The only enforcement that actually holds is the SOAR role on the automation user whose token you configured. Scope that role to the job, and treat everything below as defence in depth on top of it.
SOAR_MCP_MODE decides which tools are registered, not merely which ones refuse when
called. A readonly server does not expose a single mutating tool, so nothing can be
talked into using one.
| Mode | Tools | What it can do |
|---|---|---|
readonly |
51 | Every query, administration included. Cannot change anything. |
standard (default) |
59 | ...plus notes, comments, artifacts, container status/severity/owner, and custom lists. |
full |
70 | ...plus running playbooks and app actions, creating and deleting containers, lists and roles, raw POST/DELETE. |
Two further guards:
SOAR_MCP_ALLOWED_LABELS— a container-label allowlist, applied to reads and writes alike. On a multi-tenant or MSSP instance this is what keeps an assistant inside one customer's data. See Tenant scoping.soar_delete_containerrequires the container's exact name as a second argument and refuses if it does not match, so a wrong id cannot delete the wrong case.
Credential redaction
SOAR's REST API returns asset configuration verbatim, credentials included —
/rest/asset hands back populated password, client_secret, api_key and
ph auth token fields in plaintext. Anything this server returns may be read by a
language model, written to a transcript and retained by whoever runs that model, so
every response is scrubbed before it leaves the process:
soar_get_asset("prod_edr")
name prod_edr
base_url https://api.example.com
client_secret «redacted by splunk-soar-mcp»
Redaction is keyed on the field name, not the value — guessing at values both misses
real secrets and destroys legitimate data like file hashes. It runs at the single point
every tool serialises through, covers soar_rest_get and error bodies as well as the
typed tools, and is not configurable off.
It is a safety net, not a licence: scope the automation user so it cannot read what it does not need.
Tenant scoping
SOAR_MCP_ALLOWED_LABELS gates reads as well as writes. Container listings are
filtered to the permitted labels server-side, and fetching a container, its artifacts,
notes or comments outside that set is refused — on a scoped deployment, reading another
tenant's case is the disclosure, not just changing it.
Refusals name neither the target's label nor the permitted set, and soar_system_info
reports only how many labels are permitted. On a multi-tenant instance the allowlist is
the customer list, and an error message is an answer: without this, repeated calls would
enumerate every tenant the deployment knows about.
Running it for more than one person
If several people or roles share an instance, do not try to express that with one server and application-level checks. Run one process per role, each with its own SOAR automation user:
| Process | SOAR_MCP_MODE |
SOAR automation user's role |
|---|---|---|
| analyst | full |
view/edit containers, run playbooks, scoped to their tenants |
| engineer | readonly |
read-only across the instance |
| dashboard | readonly |
read-only, one tenant |
The isolation that matters there is not this server's mode — it is that each process holds a differently privileged credential. If the layer above is compromised, prompt-injected or simply wrong, SOAR still refuses the call. A single process with in-code role checks gives you none of that, because it holds one token that can do everything any role can do.
Three rules for that deployment:
- The SOAR token never reaches a browser, and this server is never reachable from the internet.
- Authenticate your users at your own application, not here — MCP's OAuth support secures the service-to-service hop, not end-user login.
- Anything irreversible — containment, isolation, blocking — gets an explicit human confirmation, not an assistant's decision. Container data is attacker-controlled text, so prompt injection against a SOAR assistant is a realistic threat, not a theoretical one.
Install
# with uv (recommended — no virtualenv to manage)
uvx splunk-soar-mcp --help
# or with pip
pip install splunk-soar-mcp
# or from source
git clone https://github.com/huseynAgazade/splunk-soar-mcp
cd splunk-soar-mcp
pip install -e ".[dev]"
Requires Python 3.10+.
Configure
Set these in the environment, in the env block of your MCP client's server config, or
in an env file (copy .env.example to .env).
| Variable | Default | Meaning |
|---|---|---|
SPLUNK_SOAR_URL |
required | Base URL, e.g. https://soar.example.com. A trailing /rest is stripped. |
SPLUNK_SOAR_API |
required | Automation user token (ph-auth-token). |
SOAR_MCP_ENV_FILE |
.env |
Path to the env file to read. A stdio server inherits its working directory from its client, so an absolute path here is usually what you want. |
SOAR_MCP_MODE |
standard |
readonly, standard or full. |
SOAR_MCP_ALLOWED_LABELS |
(all) | Comma-separated container labels this server may read or write. |
SOAR_MCP_VERIFY_SSL |
true |
Set false only for self-signed certs on a trusted network. |
SOAR_MCP_CA_BUNDLE |
— | Path to a CA bundle. Preferred over disabling verification. |
SOAR_MCP_TIMEOUT |
60 |
Per-request timeout, seconds. |
SOAR_MCP_DEFAULT_PAGE_SIZE |
25 |
Default rows per listing. |
SOAR_MCP_MAX_PAGE_SIZE |
500 |
Ceiling on page_size. |
Get a token in the SOAR UI under Administration → User Management → Automation Users. Give that user the narrowest role that covers your chosen mode.
Check the configuration without starting a server:
splunk-soar-mcp --list-tools # what this configuration exposes
splunk-soar-mcp --list-tools --mode readonly
Connect a client
Claude Code
claude mcp add splunk-soar \
--env SPLUNK_SOAR_URL=https://soar.example.com \
--env SPLUNK_SOAR_API=your-token \
--env SOAR_MCP_MODE=standard \
-- uvx splunk-soar-mcp
Claude Desktop / any stdio client
claude_desktop_config.json:
{
"mcpServers": {
"splunk-soar": {
"command": "uvx",
"args": ["splunk-soar-mcp"],
"env": {
"SPLUNK_SOAR_URL": "https://soar.example.com",
"SPLUNK_SOAR_API": "your-token",
"SOAR_MCP_MODE": "standard"
}
}
}
}
Installed with pip instead of uv? Use "command": "splunk-soar-mcp" and drop args.
Tools
Discovery — what the instance has
| Tool | Purpose |
|---|---|
soar_system_info |
Instance version, base URL, this server's mode and label scope. Call it first. |
soar_list_apps |
Installed apps, filterable by name. |
soar_list_app_actions |
The actions an app exposes. |
soar_get_app_action |
One action in full — every parameter and output datapath. |
soar_list_assets |
Configured assets. |
soar_get_asset |
One asset's full configuration, credentials redacted. |
soar_list_custom_functions |
Custom functions available to playbooks. |
soar_get_custom_function |
One custom function, with its inputs, outputs and source. |
soar_list_repos |
Source-control repositories. |
Playbooks and runs
| Tool | Purpose |
|---|---|
soar_list_playbooks |
Playbooks, filterable by name, label, or active-only. |
soar_get_playbook |
Metadata and block inventory. |
soar_list_playbook_blocks |
Block names and signatures, cheaply. |
soar_get_playbook_source |
Generated python — whole file, or one block. |
soar_list_playbook_runs |
Run history, filterable by playbook, container or status. |
soar_get_playbook_run |
One run's full record. |
soar_get_playbook_run_log |
The debug log for one run, with a substring filter. |
soar_list_action_runs |
App action executions, failures included. |
soar_get_action_run |
One action run plus the per-asset executions beneath it. |
On logs. There are two different things here, and only one of them is a log.
Playbook runs have a real debug log — every phantom.debug line, block transition and
traceback — read with soar_get_playbook_run_log.
Actions have no debug log; action_run/<id>/log and app_run/<id>/log both return 400.
What they have instead is history, which is what the UI's Action Run page shows and
what soar_list_action_runs returns: newest first, with the action, its status and
message, the container and playbook run, and — via the _annotation_playbook_run_effective_user
annotation — the user each action ran as. That last column is what makes it an audit
trail rather than a list of anonymous events, and it is how you tell automation apart from
a specific service account.
For one action's detail, soar_get_action_run returns the action_run (status, message)
together with the app_runs beneath it (exception_occured, result_summary,
result_data). On a failure result_data is null, so the message is what to read; if
the action ran inside a playbook, the fuller reason is in that playbook's run log.
Containers and artifacts
| Tool | Mode | Purpose |
|---|---|---|
soar_list_containers |
read | Containers, filterable by name, label, status, severity, owner. |
soar_get_container |
read | One container, with its custom fields. |
soar_list_artifacts |
read | A container's artifacts. |
soar_get_artifact |
read | One artifact, with every CEF field. |
soar_list_notes |
read | Notes on a container. |
soar_list_comments |
read | Comments on a container. |
soar_add_comment |
standard | Add a comment. |
soar_add_note |
standard | Add a titled note. |
soar_update_container |
standard | Change status, severity, sensitivity, owner, name or description. |
soar_add_artifact |
standard | Add an artifact, with CEF fields. |
soar_create_container |
full | Create a container. |
soar_delete_artifact |
full | Delete an artifact. |
soar_delete_container |
full | Delete a container — requires its exact name to confirm. |
Custom lists
| Tool | Mode | Purpose |
|---|---|---|
soar_list_custom_lists |
read | Custom lists (decided_list). |
soar_get_custom_list |
read | One list's contents as a grid. |
soar_create_custom_list |
standard | Create a list, optionally with initial rows. |
soar_append_to_custom_list |
standard | Append one row, leaving existing rows alone. |
soar_update_custom_list_row |
standard | Replace one row by index — the surgical option. |
soar_replace_custom_list |
standard | Replace the whole list. |
soar_delete_custom_list |
full | Delete a list — requires its exact name to confirm. |
Administration
Read-only by design. The administration surface holds an instance's credentials and tenant arrangements; a misconfigured SMTP relay or authentication setting is not something an assistant should be able to change by accident.
| Tool | Purpose |
|---|---|
soar_get_system_settings |
Instance settings by section — company info, ROI, email, forwarders, credential management, playbook execution, authentication, account security, debug levels, audit-trail config, clustering, FIPS, multi-tenancy. Call it bare to list the 34 sections and which admin page each backs. |
soar_get_license |
Licence status, entitlements and current usage. |
soar_get_system_health |
Service states plus load, memory, swap, database and vault utilisation. |
soar_list_cluster_nodes |
Cluster members. Empty on a single node. |
soar_list_feature_flags |
Platform feature flags and their values. |
soar_list_ingestion_status |
Ingestion runs per asset — finds an on-poll integration that stopped. |
Event metadata
The vocabulary a playbook works in. Reading these is what stops a block being written against a status or field that does not exist on the instance.
| Tool | Purpose |
|---|---|
soar_list_container_statuses |
Configured statuses — the values soar_update_container accepts. |
soar_list_severities |
Configured severities, with display order and colour. |
soar_list_custom_fields |
Container custom fields and their types. |
soar_list_cef_fields |
CEF field definitions and what each contains. |
soar_list_workbooks |
Workbook templates. |
soar_get_workbook |
One workbook's phases and tasks, in order. |
Users and roles
| Tool | Mode | Purpose |
|---|---|---|
soar_list_users · soar_get_user |
read | Platform users and their roles. |
soar_list_roles |
read | Roles. immutable marks a platform built-in. |
soar_get_role |
read | One role's full permission matrix. |
soar_create_role |
full | Create a role. Unspecified verbs default to deny. |
soar_update_role |
full | Change name, description or permissions. |
soar_delete_role |
full | Delete a role — exact name required to confirm. |
Permission areas: apps, assets, automation_broker, case_management, containers,
custom_lists, onprem_automation, playbooks, system_settings, users_roles,
workbooks. Verbs: view, edit, delete, execute.
Two guards on role management: platform built-ins (immutable: true) are refused
outright, and deletion requires the role's exact name. Note that SOAR soft-deletes
roles — the role is disabled and leaves the listing, but the record stays fetchable by id
and a second delete returns 404.
Execution — full mode only
| Tool | Purpose |
|---|---|
soar_run_playbook |
Run a playbook against a container. Returns a playbook_run_id. |
soar_run_action |
Run one app action against an asset. Returns an action_run_id. |
Both are asynchronous: they queue the work and return an id. Poll it with
soar_get_playbook_run / soar_get_action_run, and read soar_get_playbook_run_log
for the detail. A response saying the run started is not a response saying it
succeeded.
These perform real automation — containment, blocking, notification. Confirm with an operator before calling them.
Visual editor blocks — local, never touches the instance
| Tool | Purpose |
|---|---|
soar_decode_vpe_block |
Decode a payload copied out of the playbook editor. |
soar_encode_vpe_block |
Encode a raw envelope. |
soar_build_code_block |
Custom Code block — the code is compiled first, so syntax errors surface here. |
soar_build_action_block |
App action block. |
soar_build_decision_block |
Decision block, with the else branch the editor requires. |
soar_build_format_block |
Format block. |
soar_build_custom_function_block |
Custom function (utility) block. |
soar_build_playbook_block |
Child playbook block. |
Raw REST
| Tool | Mode | Purpose |
|---|---|---|
soar_rest_get |
read | GET any /rest path. |
soar_rest_post |
full | POST any /rest path. Bypasses the label allowlist. |
soar_rest_delete |
full | DELETE any /rest path. |
Resources and prompts
Resources:
| URI | Contents |
|---|---|
soar://reference/phantom-api |
Every callable on the phantom object inside a running block, grouped by purpose. |
soar://reference/datapaths |
How each block type's output is read downstream, and which forms fan out. |
soar://reference/vpe-blocks |
The editor clipboard format and the fields it rejects a node for omitting. |
soar://instance/summary |
Live counts of apps, assets, playbooks, lists and containers. |
Prompts: debug_playbook_run, triage_container, design_block.
HTTP transport
For a shared deployment:
splunk-soar-mcp --transport streamable-http --host 0.0.0.0 --port 8000
The endpoint is /mcp. The server carries no authentication of its own — anyone who
can reach the port inherits the API token's permissions. Put it behind an authenticating
proxy, bind it to a private interface, and run it in readonly unless you have a reason
not to.
Development
pip install -e ".[dev]"
pytest # no live instance needed — the REST layer is mocked
ruff check .
python scripts/smoke_test.py # drives the server over stdio as a real MCP client
python scripts/smoke_test.py --live # ...and calls the configured instance
To exercise every registered tool against a real instance and get a coverage report of what was and was not called:
python scripts/test_all_tools.py # local builders + all reads
python scripts/test_all_tools.py --write # + writes, confined to --label
python scripts/test_all_tools.py --write --full # + create/delete, self-cleaning
python scripts/test_all_tools.py --write --full --execute \
--playbook "My Playbook" --action "geolocate ip" --asset maxmind
Writes are confined to --label (default test_label) and every object the
script creates, it deletes.
Layout:
src/splunk_soar_mcp/
config.py settings, credential resolution, the mode enum
client.py async REST client — auth, pagination, SOAR's filter syntax
app.py shared runtime state, the label allowlist guard
server.py builds the server, registers tools per mode
formatting.py compact table rendering
redaction.py strips credentials from every response
tools/ platform, playbooks, containers, lists, admin, metadata,
users, run, raw, vpe
vpe/blocks.py clipboard payload codec and node builders
reference/ the markdown served as resources
scripts/smoke_test.py an example MCP client
scripts/test_all_tools.py exhaustive tool exerciser with a coverage report
Adding a tool: write it in the right tools/ module with a @mcp.tool decorator, a
docstring whose Args: section documents each parameter, and a ToolAnnotations that
tells the truth about whether it mutates. Register it in server.py under the lowest mode
that should have it. Raise SoarError for anything the caller could act on — its message
reaches the model, where a plain exception would be masked.
Security
- Never commit credentials.
.envis gitignored. The token is equivalent to the automation user's full permissions. - Scope the automation user's role to the mode you run in.
SOAR_MCP_MODEshapes what the assistant is offered; the SOAR role is what actually enforces it. - Use
SOAR_MCP_ALLOWED_LABELSon multi-tenant instances. It is the difference between an assistant that can comment on one customer's cases and one that can comment on all of them. - On self-signed certificates. On-prem SOAR usually ships one. If it is signed by a CA
you control, point
SOAR_MCP_CA_BUNDLEat that CA and keep verification on. If it is self-signed by the appliance itself, pinning it often cannot work: SOAR's default certificate carries no Authority Key Identifier, and Python 3.13+ enablesssl.VERIFY_X509_STRICTby default, which rejects such a certificate as its own CA even though OpenSSL accepts the chain. In that caseSOAR_MCP_VERIFY_SSL=falseon a trusted network is the honest option — the alternative is a CA bundle that silently does nothing. - Found a vulnerability? See SECURITY.md.
License
MIT — see LICENSE.
Not affiliated with or endorsed by Splunk Inc. "Splunk" and "Splunk SOAR" are trademarks of Splunk Inc.
Release files for splunk-soar-mcp 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| splunk_soar_mcp-0.1.1.tar.gz | 68.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| splunk_soar_mcp-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 132.9 kB
Release files / splunk_soar_mcp-0.1.1.tar.gz
| Download URL | splunk_soar_mcp-0.1.1.tar.gz |
|---|---|
| Size | 68.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
814dc71f0d4d6650f927ea5e9fcae9f413999940f2837cbaa3902d51646f5ec4
|
|
BLAKE2b-256 checksum How to use checksums |
b60f3ad795ac5f122bfff54d190504aad86b4eebd8bde50d8d191b5970278961
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / splunk_soar_mcp-0.1.1-py3-none-any.whl
| Download URL | splunk_soar_mcp-0.1.1-py3-none-any.whl |
|---|---|
| Size | 64.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f4775d5b1de08969e89ad99b6a9a55696ab7677dc9c8123b09095d88915e1c64
|
|
BLAKE2b-256 checksum How to use checksums |
d05a7f9352841fafd9155f79f8260e2248b4f15e0bab859a7023236735577d37
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|