Content
# misp-galaxy-mcp
<img width="2172" height="724" alt="image" src="https://github.com/user-attachments/assets/e57f4c8d-b79a-4139-a2ee-4cee834140d4" />
A small [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for
searching the [MISP galaxy](https://github.com/MISP/MISP-galaxy) knowledge base.
Unlike [`misp-mcp`](https://github.com/MISP/misp-mcp) (which talks to a live MISP
instance), this server operates entirely against the static MISP-galaxy data
vendored as a git submodule. It needs **no MISP instance, no API key, and no
network access** at runtime.
It exposes two tools that return the canonical MISP galaxy tag for each match,
e.g. `misp-galaxy:threat-actor="APT28"`.
## Tools
### `search_galaxy_clusters(query, galaxy_type=None, limit=50)`
Keyword search over each cluster's name (`value`) and its `synonyms`, using an
exact-first waterfall:
1. Return case-insensitive **exact** matches if any exist, else
2. return case-insensitive **substring** matches.
This keeps a precise query like `apt1` from dragging in `apt11`/`apt28`, while a
loose query like `apt` still falls back to substring matching. Descriptions are
never searched.
### `search_galaxy_clusters_by_meta(key, value, galaxy_type=None, limit=50)`
Search clusters by an arbitrary metadata key/value pair under `values[].meta`,
using **case-insensitive exact matching only** (no substring fallback). For list
or nested metadata, a match on any element counts.
```
search_galaxy_clusters_by_meta("synonyms", "Sofacy") -> APT28
search_galaxy_clusters_by_meta("country", "RU") -> all RU actors
search_galaxy_clusters_by_meta("cfr-suspected-victims", "Germany")
```
### `list_galaxies(galaxy_type=None)`
Discovery helper. With no argument, returns every galaxy (`galaxy_type`,
`galaxy_name`, `cluster_count`) so you know what to pass as `galaxy_type`. With
a `galaxy_type`, it additionally returns the `meta_keys` present on that
galaxy's clusters — i.e. the keys you can pass to
`search_galaxy_clusters_by_meta`.
### Result shape
Both search tools return:
```json
{
"query": "apt28",
"galaxy_type": null,
"match_mode": "exact",
"count": 1,
"truncated": false,
"results": [
{
"tag": "misp-galaxy:threat-actor=\"APT28\"",
"galaxy_type": "threat-actor",
"value": "APT28",
"uuid": "5b4ee3ea-eee3-4c8e-8323-85ae32658754"
}
]
}
```
`match_mode` is `"exact"`, `"substring"`, or `"none"`. `truncated` is `true`
when more matches existed than `limit`.
## Install
```bash
git clone --recurse-submodules git@github.com:MISP/misp-galaxy-mcp.git
cd misp-galaxy-mcp
# if you already cloned without --recurse-submodules:
git submodule update --init --recursive
pip install -e . # or: uv pip install -e .
```
The MISP-galaxy data lives in the `MISP-galaxy/` submodule. To pull newer galaxy
data, update the submodule and restart the server:
```bash
git submodule update --remote MISP-galaxy
```
By default the server reads `MISP-galaxy/clusters/`. Override with the
`MISP_GALAXY_PATH` environment variable if your checkout lives elsewhere.
## Run
```bash
misp-galaxy-mcp # stdio transport
```
### MCP client configuration (Claude Desktop / Claude Code)
```json
{
"mcpServers": {
"misp-galaxy": {
"command": "uv",
"args": ["run", "--directory", "/path/to/misp-galaxy-mcp", "misp-galaxy-mcp"]
}
}
}
```
(or `"command": "misp-galaxy-mcp"` once it is installed on your `PATH`).
## Tests
```bash
pip install -e ".[dev]"
pytest
```
Tests run against small synthetic fixtures in `tests/fixtures/`, so they do not
depend on the submodule.
## License
MIT — see [LICENSE](LICENSE).