diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fa154e..921fbbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/README.md b/README.md index bd5223d..f02dc39 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 diff --git a/SECURITY.md b/SECURITY.md index 6b7ab45..dea954c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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 @@ -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 @@ -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. @@ -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. diff --git a/docker-compose.yml b/docker-compose.yml index 27ffd6a..b5b32a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} diff --git a/pyproject.toml b/pyproject.toml index 8abb53b..92e576c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/ruff.toml b/ruff.toml index 94e950e..eabf62c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -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 diff --git a/src/purple_mcp/__init__.py b/src/purple_mcp/__init__.py index 208f619..b074f6d 100644 --- a/src/purple_mcp/__init__.py +++ b/src/purple_mcp/__init__.py @@ -5,4 +5,4 @@ and interacting with AI-powered security analysis services. """ -__version__ = "0.5.1" +__version__ = "0.6.0" diff --git a/src/purple_mcp/config.py b/src/purple_mcp/config.py index 1bf48d1..822f66e 100644 --- a/src/purple_mcp/config.py +++ b/src/purple_mcp/config.py @@ -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, ) diff --git a/src/purple_mcp/server.py b/src/purple_mcp/server.py index c5099ad..6c00ed0 100644 --- a/src/purple_mcp/server.py +++ b/src/purple_mcp/server.py @@ -42,6 +42,7 @@ """ import contextlib +from typing import Literal import fastmcp from fastmcp.server.http import StarletteWithLifespan @@ -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) diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py index 7331280..ca4e166 100644 --- a/tests/unit/test_server.py +++ b/tests/unit/test_server.py @@ -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 @@ -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: @@ -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: diff --git a/uv.lock b/uv.lock index ae89a8a..108db9a 100644 --- a/uv.lock +++ b/uv.lock @@ -1864,7 +1864,7 @@ wheels = [ [[package]] name = "purple-mcp" -version = "0.5.1" +version = "0.6.0" source = { editable = "." } dependencies = [ { name = "click" },