Skip to main content
Glama
blauwers

OpenCTI MCP Server

by blauwers
README.md
# OpenCTI MCP Server

The OpenCTI MCP server exposes OpenCTI threat-intelligence operations through
the Model Context Protocol (MCP). It uses the official `pycti` client to call
the OpenCTI GraphQL API.

This project is licensed under the Apache License 2.0. See [LICENSE](LICENSE)
for details.

## Features

| Category | Tools |
| --- | --- |
| Indicators | `lookup_indicator`, `list_indicators`, `list_indicators_page`, `get_indicator`, `summarize_indicator_intelligence`, `add_indicator`, `update_indicator`, `promote_observable_to_indicator`, `get_indicator_relationships` |
| Observables | `lookup_observable`, `list_observables`, `list_observables_page`, `get_observable`, `summarize_observable_intelligence`, `add_observable`, `enrich_observable`, `get_observable_indicators`, `get_observable_relationships` |
| Intelligence entities | `list_intelligence_entity_types`, `list_intelligence_entities`, `list_intelligence_entities_page`, `get_intelligence_entity`, `export_intelligence_entity_stix` |
| Reports | `lookup_report`, `list_reports`, `list_reports_page`, `create_report`, `add_object_to_report`, `get_report_objects`, `summarize_report_intelligence`, `export_report_stix` |
| Cases | `create_incident_case`, `create_rfi`, `lookup_case`, `list_cases`, `list_cases_page`, `add_object_to_case`, `update_case_status`, `summarize_case_intelligence` |
| Tasks | `create_task`, `complete_task` |
| Investigations | `create_investigation`, `get_investigation`, `list_investigations`, `list_investigations_page`, `add_to_investigation`, `export_investigation_as_report`, `start_investigation_from_container`, `start_investigation_from_entity`, `run_basic_investigation` |
| Enrichment | `list_enrichment_connectors`, `enrich_entity`, `enrich_entity_and_wait`, `run_available_enrichments`, `get_enrichment_status`, `get_entity_connectors` |
| Relationships | `create_relationship`, `lookup_relationships`, `find_relationship_paths`, `create_sighting` |
| Intelligence context | `global_search`, `resolve_entity`, `summarize_entity`, `expand_entity_context`, `find_by_stix_id`, `find_by_external_reference` |
| System | `get_current_identity`, `get_server_capabilities`, `get_runtime_metrics`, `get_recent_audit_events`, `list_audit_events` |

## Resources

| URI template | Description |
| --- | --- |
| `opencti://indicator/{indicator_id}` | Indicator details |
| `opencti://observable/{observable_id}` | Observable details |
| `opencti://report/{report_id}` | Report details with contained objects |
| `opencti://case/{case_id}` | Incident, RFI, or RFT case details |
| `opencti://investigation/{investigation_id}` | Investigation exported as a STIX 2.1 bundle |
| `opencti://server/capabilities` | Server capabilities, limits, backend version, and disabled features |

## Requirements

- Python 3.10 or later
- A running OpenCTI instance
- An OpenCTI API token with the permissions required by the tools you enable

## Installation

From the repository root:

```bash
pip install -e .
```

For development:

```bash
pip install -r requirements.txt
pip install -r test-requirements.txt
pip install -e .
```

`requirements.txt` and `test-requirements.txt` are the cross-platform source
requirements used for local development. `requirements.docker.lock` and
`build-requirements.docker.lock` are deterministic Linux/CPython 3.12 inputs
for the production container build; they are intentionally not the default
install path for Windows or macOS hosts.

To refresh the container locks from any host with Docker installed:

```bash
python scripts/refresh-docker-locks.py
```

That command resolves dependencies inside the same pinned Python image used by
the Dockerfile, so platform-specific dependencies are locked for the deployment
target rather than whichever workstation happened to run the command.

## Configuration

The server reads configuration from environment variables. For local development
you may load one explicitly trusted dotenv file by setting `MCP_DOTENV_PATH` to
an absolute path. Implicit working-directory `.env` discovery is intentionally
disabled.

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `OPENCTI_URL` | Yes |  | Base URL of the OpenCTI instance, for example `http://localhost:4000` |
| `OPENCTI_TOKEN` | Yes |  | OpenCTI API bearer token |
| `OPENCTI_SSL_VERIFY` | No | `true` | `true`, `false`, or a CA bundle path |
| `LOG_LEVEL` | No | `info` | Python log level |
| `MCP_TRANSPORT` | No | `stdio` | `stdio` or `sse` |
| `MCP_SSE_HOST` | No | `127.0.0.1` | Bind host for SSE transport |
| `MCP_SSE_PORT` | No | `8000` | Bind port for SSE transport |
| `MCP_API_KEY` | Required for SSE unless explicitly disabled |  | Bearer token required for SSE HTTP requests |
| `MCP_IDENTITY_MODE` | No | `server_token` | `server_token` uses the configured `OPENCTI_TOKEN`; `opencti_bearer` requires SSE callers to authenticate with their own OpenCTI bearer token and executes tools under that OpenCTI principal. |
| `MCP_DEFAULT_TENANT_ID` | No | `default` | Tenant id assigned to the primary `OPENCTI_URL` / `OPENCTI_TOKEN` connection. |
| `MCP_TENANTS_JSON` | No | unset | JSON object of additional routable tenants keyed by tenant id. Each entry must define `opencti_url`, `opencti_token`, and may override `ssl_verify`. Multi-tenant routing is SSE-only. |
| `MCP_COORDINATION_BACKEND_FACTORY` | No | unset | Optional `module:callable` import path that returns a `CoordinationBackends` bundle for externally managed audit, quota, and idempotency storage. |
| `MCP_AUDIT_BUFFER_SIZE` | No | `1000` | Maximum number of sanitized audit events retained in memory for local inspection. |
| `MCP_AUDIT_ACTOR_MAX_STREAMS` | No | `4096` | Maximum number of actor-local audit history and principal-metric streams retained at once. |
| `MCP_PRINCIPAL_QUOTA_PER_MINUTE` | No | `0` | Per-principal, per-operation local quota. `0` disables quota enforcement. |
| `MCP_PRINCIPAL_QUOTA_MAX_ACTORS` | No | `4096` | Maximum number of actor quota windows retained at once. |
| `MCP_EXPORT_MAX_OBJECTS` | No | `50` | Maximum direct STIX members included in one bounded report or investigation export. |
| `MCP_IDEMPOTENCY_TTL_SECONDS` | No | `300` | Retention window in seconds for completed mutation idempotency keys. |
| `MCP_IDEMPOTENCY_MAX_ENTRIES` | No | `4096` | Maximum number of retained idempotency records in the local backend. |
| `MCP_REQUIRE_MUTATION_CONFIRMATION` | No | `false` | Require `confirm=true` on mutating tools before execution. |
| `MCP_IDEMPOTENCY_BACKEND` | No | `memory` | `memory` for process-local replay protection or `sqlite` for restart-safe durable replay protection. |
| `MCP_IDEMPOTENCY_SQLITE_PATH` | Required when `MCP_IDEMPOTENCY_BACKEND=sqlite` |  | Absolute path to the SQLite idempotency database file. |
| `MCP_OBSERVABILITY_BACKEND` | No | `memory` | `memory` for process-local audit and quota state or `sqlite` for restart-safe, cross-process observability state. |
| `MCP_OBSERVABILITY_SQLITE_PATH` | Required when `MCP_OBSERVABILITY_BACKEND=sqlite` |  | Absolute path to the SQLite observability database file. |
| `MCP_ALLOW_UNAUTHENTICATED_SSE` | No | `false` | Development-only opt-in for unauthenticated SSE |
| `MCP_MAX_BODY_BYTES` | No | `1048576` | Maximum SSE HTTP request body size |
| `MCP_BODY_READ_TIMEOUT` | No | `10` | Positive, finite seconds allowed to receive an SSE request body; fractional values are supported |
| `MCP_MAX_CONCURRENT` | No | `20` | Maximum concurrent SSE streams and ordinary HTTP requests, enforced independently per pool |
| `MCP_RATE_LIMIT_PER_MINUTE` | No | `60` | Maximum SSE HTTP requests per client per minute |
| `MCP_MAX_RATE_LIMIT_CLIENTS` | No | `4096` | Maximum tracked SSE rate-limit client identifiers. Higher values support more unique client IPs and use more memory; lower values evict older clients sooner. |
| `MCP_TRUST_PROXY_HEADERS` | No | `false` | Use `X-Forwarded-For`, `Forwarded`, or `X-Real-IP` as the SSE rate-limit client identifier. Enable only when requests come through a trusted reverse proxy that overwrites these headers. |
| `MCP_TRUSTED_PROXY_SOURCES` | No | `127.0.0.1,::1` | Comma-separated proxy source IPs or CIDR ranges allowed to supply forwarded client headers when `MCP_TRUST_PROXY_HEADERS=true`. At least one source is required when enabled. |
| `OPENCTI_REQUEST_TIMEOUT` | No | `60` | Timeout in seconds for OpenCTI API requests |
| `MCP_MAX_ARGUMENT_BYTES` | No | `1048576` | Maximum serialized argument size for one tool call |
| `MCP_MAX_RESPONSE_BYTES` | No | `4194304` | Maximum serialized tool response size |

## Usage

### stdio transport

Use `stdio` when an MCP client launches the server process directly:

```bash
OPENCTI_URL=http://localhost:4000 \
OPENCTI_TOKEN=<opencti-token> \
opencti-mcp
```

Example MCP client configuration:

```json
{
  "mcpServers": {
    "opencti": {
      "command": "opencti-mcp",
      "env": {
        "OPENCTI_URL": "http://localhost:4000",
        "OPENCTI_TOKEN": "<opencti-token>"
      }
    }
  }
}
```

### SSE transport

Use `sse` when exposing the server as an HTTP endpoint. SSE requires
`MCP_API_KEY` by default.

```bash
MCP_TRANSPORT=sse \
MCP_SSE_HOST=127.0.0.1 \
MCP_SSE_PORT=8000 \
MCP_API_KEY=<mcp-bearer-token> \
OPENCTI_URL=http://localhost:4000 \
OPENCTI_TOKEN=<opencti-token> \
opencti-mcp
```

Configure MCP clients to use:

```text
http://localhost:8000/sse
```

Clients must send:

```text
Authorization: Bearer <mcp-bearer-token>
```

For OpenCTI-backed caller identity, switch SSE to delegated bearer mode:

```bash
MCP_TRANSPORT=sse \
MCP_IDENTITY_MODE=opencti_bearer \
OPENCTI_URL=http://localhost:4000 \
OPENCTI_TOKEN=<bootstrap-opencti-token> \
opencti-mcp
```

In this mode `MCP_API_KEY` must be unset and each client sends its own OpenCTI
bearer token in `Authorization`. The server validates that token with
OpenCTI `me()`, binds the resolved principal to the MCP request, and reuses the
same principal for every tool and resource call in that request. `get_current_identity`
returns the active OpenCTI principal. Successful token validations are cached
for five seconds to reduce duplicate SSE message checks while keeping a narrow
revocation window.

`get_server_capabilities` and `opencti://server/capabilities` expose the active
transport and identity mode, the OpenCTI backend version, configured limits,
the registered tool/resource inventory, and any intentionally disabled feature
families so clients can negotiate behavior before invoking it.
The disabled feature manifest is now empty for the shipped server surface.
Optional features such as tenant routing and external coordination report their
active configuration through capability fields instead of being misclassified
as missing functionality.
In `opencti_bearer` mode, the discovery surface is filtered against the
caller's OpenCTI capabilities and unauthorized tool/resource calls fail fast
with a stable `forbidden` response before the backend is invoked. The
capabilities payload includes the requirement manifest used for that
projection.

`get_runtime_metrics` exposes limiter state plus audit and idempotency counters
in server-token mode. In delegated identity mode it defaults to principal-local
audit counters retained inside the bounded actor stream cache and requires
OpenCTI `SETTINGS_SETACCESSES` (or `BYPASS`) for
`scope="all"` global metrics. In multi-tenant delegated deployments, unfiltered
`scope="all"` metrics and audit views are rejected because the selected OpenCTI
tenant does not confer cross-tenant operator authority; callers must stay on
`scope="self"` or provide an explicit actor filter within the selected tenant.
`get_recent_audit_events` exposes sanitized invocation receipts from the
configured audit backend. The HTTP app also provides `/live`, `/ready`, and
`/metrics` alongside the compatibility `/health` endpoint. Audit records
include actor, operation, outcome, and latency only; they never store tool
arguments or response payloads. `get_recent_audit_events` defaults to the
current actor. In delegated single-tenant mode, operators with OpenCTI
`SETTINGS_SETACCESSES` (or `BYPASS`) may request `scope="all"` for
cross-principal audit inspection.
`list_audit_events` adds newest-first opaque-cursor pagination for operator
workflows that need to traverse the bounded audit stream without offset drift
when new events arrive.
The high-cardinality knowledge list families also expose paginated variants:
`list_indicators_page`, `list_observables_page`,
`list_intelligence_entities_page`, `list_reports_page`, `list_cases_page`,
and `list_investigations_page`. These tools use signed opaque continuation
cursors backed by OpenCTI's native cursor model and return a stable envelope
containing `items`, `has_next_page`, `next_cursor`, and `total_count` when the
backend supplies it. Cursors are bound to the tool and filter set that produced
them, so callers must reuse the same query shape on continuation requests.
`list_cases_page` requires an explicit `case_type` of `incident`, `rfi`, or
`rft`; merged cross-type pagination remains on `list_cases` because OpenCTI
does not expose one stable cursor domain across all three case collections.
`/metrics` returns JSON by default for operational debugging and also supports
Prometheus text exposition when clients send `Accept: text/plain` or
`Accept: application/openmetrics-text`. The authentication and authorization
requirements are identical in both formats.
The readiness and liveness probe endpoints are intentionally unauthenticated so
load balancers can reach them without an MCP session. `/metrics` requires the
configured SSE API key in `server_token` mode, and in `opencti_bearer` mode it
requires an authenticated OpenCTI principal with `SETTINGS_SETACCESSES` or
`BYPASS`. Multi-tenant delegated deployments reject `/metrics` because the
server has no distinct cross-tenant operator role.

SSE deployments also expose `/changes`, an authenticated relay for OpenCTI's
native `/stream` feed. The relay forwards `Last-Event-ID` plus the supported
OpenCTI stream query parameters (`recover`, `listen-delete`,
`no-dependencies`, and `with-inferences`) so consumers can resume delivery
without the MCP server inventing its own event format.

When `MCP_TENANTS_JSON` is set, every non-probe SSE request may select a tenant
with `X-OpenCTI-Tenant`. Requests without that header use
`MCP_DEFAULT_TENANT_ID`. Delegated bearer validation, pycti client creation,
quota/idempotency principal scoping, and opaque pagination cursors are all bound
to the selected tenant so OpenCTI identity continues to flow from the platform
that actually owns the request.

For deployments that need a managed coordination service, set
`MCP_COORDINATION_BACKEND_FACTORY` to a trusted `module:callable`. The callable
receives the loaded server config and returns a
`opencti_mcp.coordination.CoordinationBackends` instance containing audit,
quota, and idempotency backend implementations. This keeps the runtime neutral
to the backing service while still allowing Redis, database, or hosted stores
to participate in the same verified contracts.

The capability payload also makes the identity model explicit:
`identity_provider.source` is always `opencti`. The server may accept a static
MCP API key in server-token mode or delegated OpenCTI bearer tokens in
`opencti_bearer` mode, but it does not act as an independent OAuth issuer.

Mutating tools expose three standard control arguments: `idempotency_key`,
`dry_run`, and `confirm`. Reusing one idempotency key with the same actor,
operation, and arguments replays the first completed result without calling
OpenCTI again; reusing it with different arguments fails with
`idempotency_conflict`. `dry_run=true` returns non-executing preview metadata
for the mutation request and does not evaluate tool-specific validity, and
operators can require `confirm=true` for every mutation with
`MCP_REQUIRE_MUTATION_CONFIRMATION=true`. The default in-memory backend is
process-local; set `MCP_IDEMPOTENCY_BACKEND=sqlite` with an absolute
`MCP_IDEMPOTENCY_SQLITE_PATH` to retain replay protection across restarts and
multiple server processes that share the same database file. The configured
TTL also bounds stale in-progress reservations so a crashed worker cannot hold
an idempotency key forever. When a mutating tool explicitly reports an unknown
submission outcome, the reservation is quarantined as indeterminate rather
than replayed as a completed result.

Audit retention and principal quota enforcement also default to in-memory
storage. Set `MCP_OBSERVABILITY_BACKEND=sqlite` with an absolute
`MCP_OBSERVABILITY_SQLITE_PATH` to keep sanitized audit history across restarts
and share quota windows across multiple server processes that point at the same
database file.

## Docker

Build the MCP server image from the repository root:

```bash
docker build -t opencti-mcp .
```

Run it against an existing OpenCTI instance:

```bash
docker run --rm \
  -e OPENCTI_URL=http://opencti:4000 \
  -e OPENCTI_TOKEN=<opencti-token> \
  -e MCP_TRANSPORT=sse \
  -e MCP_SSE_HOST=0.0.0.0 \
  -e MCP_SSE_PORT=8000 \
  -e MCP_API_KEY=<mcp-bearer-token> \
  -p 8000:8000 \
  opencti-mcp
```

## Docker Compose

The `docker-compose.yml` in this directory starts only the MCP server. Run
OpenCTI separately from a compatible OpenCTI checkout or an existing deployment,
then point the MCP server at that instance.

```bash
cp .env.example .env
```

Edit `.env` and set:

- `OPENCTI_URL` to an OpenCTI instance reachable from the MCP container
- `OPENCTI_TOKEN` to an OpenCTI API token
- `MCP_API_KEY` to a random bearer token for MCP clients

For a host-running development server, use:

```env
OPENCTI_URL=http://host.docker.internal:4000
```

For a containerized OpenCTI deployment, set `OPENCTI_URL` to the URL reachable
from the MCP container, such as `http://opencti:4000` when the service is
attached to the same Docker network.

Start the MCP server:

```bash
docker compose up -d --build
```

Default endpoint:

| Service | URL |
| --- | --- |
| MCP SSE endpoint | `http://127.0.0.1:8000/sse` |
| Readiness endpoint | `http://127.0.0.1:8000/health` |

Stop the MCP server:

```bash
docker compose down
```

## Recommended Workflows

The tools are intended to support both analyst-driven and agentic intelligence
workflows. Useful starting patterns include:

1. Resolve and summarize a seed:
   `resolve_entity` -> `summarize_entity` -> `expand_entity_context`
2. Investigate an observable:
   `lookup_observable` -> `summarize_observable_intelligence` ->
   `run_available_enrichments` -> `get_enrichment_status`
3. Investigate an indicator:
   `lookup_indicator` -> `summarize_indicator_intelligence` ->
   `find_relationship_paths`
4. Create an investigation workspace from a seed:
   `run_basic_investigation` or `start_investigation_from_entity`
5. Review handoff context:
   `summarize_report_intelligence` or `summarize_case_intelligence`

## Automation Guardrails

The MCP server supports agentic workflows without exposing broad
administration. It does not provide user, group, connector administration, or
delete tools. Automated workflows are bounded by explicit limits:

- Graph expansion and path finding clamp depth, fan-out, queue size, explored
  states, backend queries, and returned paths. Path and context-expansion
  responses report when traversal was truncated so callers can narrow filters.
- SSE streams and ordinary HTTP requests have separate concurrency pools, so
  connected clients cannot consume the slots needed to send their messages.
- Enrichment waits clamp timeout and polling intervals. Positive timeout values
  share one batch deadline across submission and polling, so connector count
  does not multiply the requested timeout; `timeout_seconds=0` submits the
  request and skips polling. If submission itself exceeds a positive deadline
  before work IDs are known, the response marks `submission_timed_out` and
  `submission_outcome_unknown`.
- Available enrichment execution is limited to active connectors matching the
  observable type, with an execution cap. Omitting a connector ID runs the
  compatible set, capped at 10 connectors per MCP call to bound external side
  effects and backend load, and returns per-connector outcomes plus
  de-duplicated work IDs.
- Investigation automation creates scoped investigation workspaces from the
  resolved seed and bounded related context before starting enrichment. Its
  phased response preserves the investigation when later connector work only
  partially succeeds.
- Typed searches return partial-result warnings when only part of a fan-out
  succeeds and an explicit error when every backend query fails.
- SSE transport enforces request body, rate, and concurrency limits.
- Blocking pycti operations run in a bounded worker pool, and timeoutable
  background offloads use a separate budget so they cannot consume the entire
  pool after callers time out.
- Object-reference writes accept at most 50 normalized object IDs per call.
- STIX export paths are bounded before materialization. Entity export supports
  both the single-object `simple` path and a bounded one-hop `full` graph of
  direct relationships plus opposite endpoints, report export includes direct
  report members up to `MCP_EXPORT_MAX_OBJECTS`, and investigation export
  builds a synthetic report bundle from one bounded workspace snapshot.
  Investigation responses use `investigated_entities_truncated` for omitted
  members and `investigated_entities_at_capacity` when the returned snapshot is
  already at the configured write limit. Exported investigation reports mirror
  those signals with `x_opencti_membership_truncated` and
  `x_opencti_membership_at_capacity`.

## Response Contracts

- `global_search` always returns `results`, `partial`, and `warnings`. Complete
  backend failure adds the standard `error` object while retaining those keys.
- Enrichment batches expose connector outcomes under `requests`; waiting tools
  add polled records under `work_statuses`. These arrays have distinct meanings
  and are not duplicated under compatibility aliases.
- `get_observable_indicators` always returns `results`, `partial`, and
  `warnings`, so failed or incomplete resolution of relationship endpoints is
  distinguishable from a complete empty result.
- `lookup_case` and `list_cases` always return `results`, `partial`, and
  `warnings`. Cross-type case queries identify any Incident, RFI, or RFT scope
  that could not be queried.
- `summarize_report_intelligence` and `summarize_case_intelligence` return
  counts for the summaries included in the response and set
  `objects_truncated` / `tasks_truncated` when additional linked members were
  omitted by the requested limit.
- `find_by_external_reference` returns at most 50 validated matches, and
  `get_entity_connectors` returns at most 50 recent enrichment work records.
- `opencti://report/{id}` and `opencti://case/{id}` resources also set
  `objects_truncated` / `tasks_truncated` when their bounded collections omit
  additional linked members.
- Direct entity, indicator, and observable reads return bounded public
  projections rather than unrestricted raw OpenCTI objects.
- Every error uses `error.code`, `error.message`, and `error.operation`.
  Additional context such as `work_id` or `entity_type` is placed alongside the
  error envelope.
- At the MCP protocol layer, structured error envelopes set `isError=true`, and
  JSON results are also exposed through `structuredContent`.

## Development

```bash
pip install -r requirements.txt
pip install -r test-requirements.txt
pip install -e .
```

Run checks:

```bash
pytest tests/ -v
black --check src/ tests/
isort --check-only src/ tests/
flake8 src/ tests/
mypy src/
```

Run the MCP coverage gate:

```bash
pytest tests/ --ignore=tests/integration \
  --cov=opencti_mcp --cov-report=term-missing --cov-fail-under=95
```

Live regression tests are available under `tests/integration/`. They are
skipped by default and require an OpenCTI API compatible with the installed
`pycti` client and a token allowed to create and update test objects. The suite
creates isolated regression objects with `MCP regression` or `mcp-regression-*`
names and removes tracked objects after the session. Set
`OPENCTI_MCP_PRESERVE_TEST_ARTIFACTS=true` only when retaining those objects is
useful for debugging. Connector work records remain subject to the platform's
normal retention policy. For release/CI validation, start OpenCTI from a
separate compatible OpenCTI checkout or point the runner at an existing
compatible instance:

```bash
python scripts/run-mcp-regression.py \
  --opencti-url http://127.0.0.1:4010 \
  --opencti-token <token> \
  --run-live-regression
```

Use `--use-external-opencti` as an explicit alias when targeting an already
running external OpenCTI instance. Use `--client-python-path <path>` only when you
intentionally want to test against a separate local `client-python` checkout
instead of the declared package dependency. To run only the fast 95% coverage
gate, omit `--run-live-regression`.

On Windows, `.\scripts\run-mcp-regression.ps1` remains available as a thin
wrapper around the cross-platform Python runner.

## Security

- Use a dedicated OpenCTI API token with least-privilege permissions.
- Do not expose SSE without either `MCP_API_KEY` or
  `MCP_IDENTITY_MODE=opencti_bearer` unless running an isolated local
  development environment.
- Bind SSE to `127.0.0.1` unless a reverse proxy provides TLS, authentication,
  request limits, and access controls.
- Leave `MCP_TRUST_PROXY_HEADERS=false` unless the MCP server only accepts
  traffic from a trusted reverse proxy that overwrites forwarded client IP
  headers. When enabling it, set `MCP_TRUSTED_PROXY_SOURCES` to the reverse
  proxy source IPs or CIDR ranges.
- Keep `OPENCTI_SSL_VERIFY=true` in production. Use a CA bundle path for
  private certificate authorities.
- Tool and resource handlers return sanitized errors to MCP clients and log
  detailed exceptions server-side.
- The SSE app enforces process-local request body, rate, and concurrency
  limits. These controls are not a replacement for reverse-proxy limits in
  shared or internet-facing deployments.
- The unauthenticated readiness endpoint uses separate rate and concurrency
  limits (120 requests per client per minute and two concurrent requests) plus
  a single-flight five-second cache and one lifecycle-managed asynchronous HTTP
  connection pool to bound upstream health probes.

## Architecture

```text
MCP client
    |
    | MCP protocol (stdio or SSE)
    v
opencti_mcp.server
    |
    | registers tools and resources
    v
opencti_mcp.tools / opencti_mcp.resources
    |
    | pycti
    v
OpenCTI GraphQL API
```

The server keeps one bootstrap OpenCTI connection plus an optional set of
additional SSE tenant routes declared through `MCP_TENANTS_JSON`. The default
tenant keeps the existing behavior. When tenant routing is enabled,
`X-OpenCTI-Tenant` selects the target backend for the request and the runtime
keeps that choice in request context. In `MCP_IDENTITY_MODE=server_token`, each
worker thread lazily creates and reuses pycti clients for the selected tenant.
In `MCP_IDENTITY_MODE=opencti_bearer`, each authenticated request gets
thread-local pycti clients bound to both the selected tenant and the caller's
OpenCTI identity, and those clients are released after the request completes.

Maintenance

ActivitySlowing
ResponsivenessNo issues