Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] - YYYY-MM-DD

## Changed
## [0.6.0] - 2025-11-25

### Added

- Amazon Bedrock AgentCore deployment support with `--stateless-http` flag
- New `PURPLEMCP_STATELESS_HTTP` environment variable for stateless HTTP mode
- New `PURPLEMCP_TRANSPORT_MODE` environment variable for transport configuration
- Comprehensive AWS Bedrock deployment guide (BEDROCK_AGENTCORE_DEPLOYMENT.md)
- IAM and trust policy templates for AWS Bedrock AgentCore

### Changed

- Updated default values for client details to be more accurate
- Transport mode now configurable via environment variable
- Improved documentation for environment variables in README

### Fixed

- Exception handling in server.py uses `Exception` instead of `BaseException`
- Type annotations for `stateless_http` field (removed unnecessary `| None`)
- Corrected `transport_mode` field description in Settings

## [0.5.1] - 2025-11-08

Expand Down Expand Up @@ -72,5 +90,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Automated CI/CD with GitHub Actions
- Comprehensive documentation (README, CONTRIBUTING, SECURITY)

[0.6.0]: https://github.com/Sentinel-One/purple-mcp/compare/v0.5.1...v0.6.0
[0.5.1]: https://github.com/Sentinel-One/purple-mcp/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/Sentinel-One/purple-mcp/releases/tag/v0.5.0
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@

Purple AI MCP Server allows you to access SentinelOne Services with any MCP client.

**Coming Soon**: In early 2026, we will allow you to connect to this service hosted by SentinelOne.

## Features

This server exposes SentinelOne's platform through the Model Context Protocol:
Expand Down Expand Up @@ -202,7 +200,8 @@ We suggest you **do not** expose Purple AI MCP on a network at this time, as the
## Environment Variables
- `PURPLEMCP_CONSOLE_TOKEN` - Service user token (Account or Site level)
- `PURPLEMCP_CONSOLE_BASE_URL` - Console URL (e.g., https://console.sentinelone.net)
- `PURPLEMCP_STATELESS_HTTP` - For use with deployment in Amazon Bedrock Agent Core - Detailed instructions can be found [here](BEDROCK_AGENTCORE_DEPLOYMENT.md)
- `PURPLEMCP_TRANSPORT_MODE` - MCP transport mode: `stdio` (default), `sse`, or `streamable-http`
- `PURPLEMCP_STATELESS_HTTP` - Enable stateless HTTP mode for serverless deployments (e.g., Amazon Bedrock AgentCore) - see [deployment guide](BEDROCK_AGENTCORE_DEPLOYMENT.md)


## Development
Expand Down
11 changes: 2 additions & 9 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ This guide documents the security expectations for every contributor and operato

- **Project maintainers** provide secure-by-default libraries, tools, and configuration primitives.
- **Operators and deployers** are responsible for securing runtime environments, network boundaries, secrets, observability pipelines, and user access.
- **Users running Purple MCP as a remote service** must place the instance behind a reverse proxy (for example, Nginx, Envoy, or an API gateway) that enforces strong authentication and authorization. Purple MCP does not ship its own auth layer.
- **Hosted MCP offering**: SentinelOne plans to launch an official hosted Purple MCP service in early 2026. Until that release, all external-facing deployments demand operator-managed network controls and authentication.
- **Users running Purple MCP as a remote service** must place the instance behind a reverse proxy (for example, Nginx, Envoy, or an API gateway) that enforces strong authentication and authorization. Purple MCP does not ship its own auth layer. All external-facing deployments require operator-managed network controls and authentication.

## Threat Model Overview

Expand Down Expand Up @@ -60,8 +59,7 @@ This guide documents the security expectations for every contributor and operato
- Terminate TLS at a reverse proxy that enforces strong client authentication (SAML/OIDC SSO, mutual TLS, signed API tokens).
- Implement rate limiting, audit logging, and IP allowlists at the proxy layer.
- Restrict network access to SentinelOne control planes and internal assets required by your workflows.
- Document all access paths and routinely review who can reach the MCP instance.
- Upcoming hosted service (early 2026) will provide managed authentication, centralized auditing, and turnkey deployments. Until then, the operator bears full responsibility for access control.
- Document all access paths and routinely review who can reach the MCP instance. The operator bears full responsibility for access control.

## Deployment Guidance

Expand All @@ -86,10 +84,6 @@ This guide documents the security expectations for every contributor and operato
- Leverage orchestrator features (Kubernetes NetworkPolicies, PodSecurityStandards, IAM roles for service accounts).
- Inject configuration via secrets and config maps—never bake secrets into container images.

### Anticipated Hosted MCP (Early 2026)

- A managed SentinelOne-hosted MCP service is planned to launch in early 2026, delivering integrated authentication, network isolation, and operational monitoring.

## Logging and Telemetry

- Logging is sanitized by default to prevent leakage of queries, tokens, or personally identifiable information.
Expand Down Expand Up @@ -136,6 +130,5 @@ This guide documents the security expectations for every contributor and operato

- [`CONTRIBUTING.md`](CONTRIBUTING.md) – Development workflow and coding standards.
- [`README.md`](README.md) – Project overview and setup instructions.
- SentinelOne internal security policies and the upcoming hosted MCP documentation (target release: early 2026).

Security is a continuous effort. Revisit this guide regularly, automate compliance checks where possible, and surface improvements to the team so that Purple MCP remains secure throughout its lifecycle.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ services:
MCP_MODE: streamable-http
MCP_HOST: 0.0.0.0
MCP_PORT: 8000
PURPLEMCP_STATELESS_HTTP: ${PURPLEMCP_STATELESS_HTTP}
PURPLEMCP_STATELESS_HTTP: ${PURPLEMCP_STATELESS_HTTP:-false}
# Required: SentinelOne Console configuration
PURPLEMCP_CONSOLE_BASE_URL: ${PURPLEMCP_CONSOLE_BASE_URL}
PURPLEMCP_CONSOLE_TOKEN: ${PURPLEMCP_CONSOLE_TOKEN}
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "purple-mcp"
version = "0.5.1"
version = "0.6.0"
description = "Purple AI MCP Server"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
1 change: 0 additions & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ docstring-code-format = true

[lint.per-file-ignores]
"__init__.py" = ["F401"] # unused-import: Allow unused imports in __init__.py files
"src/indigo_workshop/**" = ["C901"] # complex-structure: Allow complex functions in workshop code

[lint.isort]
combine-as-imports = true
Expand Down
2 changes: 1 addition & 1 deletion src/purple_mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
and interacting with AI-powered security analysis services.
"""

__version__ = "0.5.1"
__version__ = "0.6.0"
8 changes: 4 additions & 4 deletions src/purple_mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,15 @@ class Settings(BaseSettings):
validation_alias=LOGFIRE_TOKEN_ENV,
)

stateless_http: bool | None = Field(
stateless_http: bool = Field(
default=False,
description="Stateless mode (new transport per request)",
validation_alias=STATELESS_HTTP_ENV,
)

transport_mode: Literal["http", "streamable-http", "sse"] = Field(
default="sse",
description="Stateless mode (new transport per request)",
transport_mode: Literal["stdio", "http", "streamable-http", "sse"] = Field(
default="stdio",
description="MCP transport mode (stdio, http, streamable-http, or sse)",
validation_alias=TRANSPORT_MODE_ENV,
)

Expand Down
29 changes: 17 additions & 12 deletions src/purple_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"""

import contextlib
from typing import Literal

import fastmcp
from fastmcp.server.http import StarletteWithLifespan
Expand Down Expand Up @@ -142,21 +143,25 @@ async def health_check(request: Request) -> JSONResponse:
settings = None

# Use get_settings to ensure usage of lru_cache decorator.
with contextlib.suppress(BaseException):
with contextlib.suppress(Exception):
settings = get_settings()


def get_http_app(app: fastmcp.FastMCP, settings: Settings | None) -> StarletteWithLifespan:
"""Returns a http_app using environment variable settings."""
return (
app.http_app(transport=settings.transport_mode, stateless_http=settings.stateless_http)
if settings and settings.transport_mode in ("streamable-http", "http")
# stateless_http arg has no effect if using "sse" transport mode when instantiating a http_app, AND there
# is only a choice of three transport modes, hence if settings is None, it is safe to assume "sse" transport
# mode even if the settings object is None - this also preserves the original module-level implementation of
# http_app.
else app.http_app(transport="sse")
)
def get_http_app(
mcp_app: fastmcp.FastMCP[None], settings: Settings | None
) -> StarletteWithLifespan:
"""Returns a http_app using environment variable settings.

For stdio mode or when settings is None, defaults to SSE transport for the HTTP app.
The stateless_http setting only applies to streamable-http and http transports.
"""
if settings and settings.transport_mode in ("streamable-http", "http"):
# Type narrowing: transport_mode is "streamable-http" or "http" here
transport: Literal["http", "streamable-http"] = (
"streamable-http" if settings.transport_mode == "streamable-http" else "http"
)
return mcp_app.http_app(transport=transport, stateless_http=settings.stateless_http)
return mcp_app.http_app(transport="sse")


http_app = get_http_app(app, settings)
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class TestServerInitialization:
"""Tests for server initialization and configuration."""

def _test_http_app_mode(
self, mode: Literal["http", "streamable-http", "sse"], stateless_http: bool
self, mode: Literal["stdio", "http", "streamable-http", "sse"], stateless_http: bool
) -> None:
mock_settings = MagicMock()
mock_settings.transport_mode = mode
Expand All @@ -47,8 +47,8 @@ def _test_http_app_mode(
session_manager = cast(StreamableHTTPSessionManager, endpoint.session_manager)
assert session_manager.stateless == stateless_http
else:
# stateless setting doesn't apply to sse mode - the prop isn't accessible in any
# meaningful sense when http_app is initialized using sse.
# stateless setting doesn't apply to sse or stdio modes - for stdio the http_app
# falls back to sse, and for sse the prop isn't accessible in any meaningful sense.
pass

def test_server_name(self) -> None:
Expand All @@ -70,9 +70,11 @@ def test_http_app_permutations(self) -> None:
self._test_http_app_mode("http", stateless_http=True)
self._test_http_app_mode("sse", stateless_http=True)
self._test_http_app_mode("streamable-http", stateless_http=True)
self._test_http_app_mode("stdio", stateless_http=True)
self._test_http_app_mode("http", stateless_http=False)
self._test_http_app_mode("sse", stateless_http=False)
self._test_http_app_mode("streamable-http", stateless_http=False)
self._test_http_app_mode("stdio", stateless_http=False)


class TestHealthEndpoint:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.