Skip to main content
Glama
delonius22

ThreatConnect v3 MCP Server

by delonius22
README.md
# ThreatConnect v3 MCP Server

A [Model Context Protocol](https://modelcontextprotocol.io) server that gives an
MCP-capable LLM client a small set of reliable, validated tools to drive
ThreatConnect Case Management and Threat Intelligence through the v3 REST API.

It hides v3's HMAC authentication, retries, pagination, and error quirks behind
typed tools so the model never hand-rolls HTTP or signing.

## Tools

| Tool | v3 call | What it does |
| --- | --- | --- |
| `create_case` | `POST /v3/cases` | Create a case (name/status/severity + optional nested artifacts, tags, attributes). |
| `update_case` | `PUT /v3/cases/{id}` | Partial update; nested associations honor `mode` (append/replace/delete). |
| `create_indicator` | `POST /v3/indicators` | Create any indicator type; type→summary-field resolved automatically. |
| `add_artifact` | `POST /v3/artifacts` | Attach one artifact to an existing case by `case_id`/`case_xid`. |
| `add_artifacts_bulk` | `PUT /v3/cases/{id}` or fan-out `POST` | Attach many artifacts: one nested-append request, or concurrent POSTs with a per-item ledger. |
| `enrich_indicator` | `GET /v3/indicators/{id\|summary}` | Return TC's context: rating/confidence, tags, attributes, associations, observations, web link. |
| `tc_get` (read-only) | `GET /v3/<allowlisted>` | Escape hatch for arbitrary **reads** with TQL/fields — never writes. |

There is deliberately **no generic write tool**: unconstrained `PUT`/`DELETE`
against shared threat intel is too dangerous to hand an LLM.

## Install

Requires Python 3.12+ and [`uv`](https://docs.astral.sh/uv/).

```bash
uv sync
cp .env.example .env   # then fill in your credentials
```

## Configuration

Set these in `.env` (or the environment). HMAC is the primary auth path.

| Variable | Required | Default | Notes |
| --- | --- | --- | --- |
| `TC_BASE_URL` | yes | — | e.g. `https://myinstance.threatconnect.com` (normalized to `/api`). |
| `TC_API_ACCESS_ID` | HMAC | — | API user access id. |
| `TC_API_SECRET_KEY` | HMAC | — | API user secret key (never logged). |
| `TC_API_TOKEN` | token | — | Alternative to HMAC; used only if the HMAC pair is absent. |
| `TC_DEFAULT_OWNER` | no | — | Default owner for owner-relative reads/writes. |
| `TC_TIMEOUT` | no | `30` | Per-request timeout (seconds). |
| `TC_MAX_RETRIES` | no | `3` | Retries on 429/5xx with backoff + jitter. |
| `TC_VERIFY_SSL` | no | `true` | TLS verification. |
| `TC_LOG_LEVEL` | no | `WARNING` | Logs go to stderr, secret-redacted. |

> **Clock skew:** the HMAC `Timestamp` must be within five minutes of server
> time. Keep the host on NTP.

## Run

```bash
uv run tc-mcp          # stdio transport
```

### Claude Desktop / `mcp.json`

```json
{
  "mcpServers": {
    "threatconnect": {
      "command": "uv",
      "args": ["--directory", "/abs/path/to/threat_connect_mcp", "run", "tc-mcp"]
    }
  }
}
```

### Inspect the tool surface

```bash
npx @modelcontextprotocol/inspector uv run tc-mcp
```

## Development

```bash
uv run ruff check .        # lint
uv run mypy src            # types
uv run pytest -q           # mocked unit/integration tests
```

### Live smoke test (gated)

The live tests are skipped unless credentials are present and `-m live` is
passed. A green `test_signature` proves the HMAC string-to-sign is correct
against your instance:

```bash
TC_BASE_URL=... TC_API_ACCESS_ID=... TC_API_SECRET_KEY=... \
    uv run pytest -m live tests/test_live.py
```

## Design notes

- **Thin httpx client, not TcEx.** TcEx assumes it runs *inside* the TC platform;
  a small signed `httpx` client is easier to test (golden HMAC vector) and has no
  hidden runtime assumptions.
- **"Dynamic" via schema introspection.** Tools validate caller fields/types
  against the live `OPTIONS /v3/<endpoint>` and `/v3/artifactTypes` descriptors,
  so they track the API instead of a frozen copy. Validation degrades gracefully
  if a descriptor is unavailable — the API stays the final authority.
- **TQL injection defense.** Any caller value interpolated into a TQL clause
  (enrich-by-summary) is escaped and control characters are rejected.