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
59 changes: 59 additions & 0 deletions BEDROCK_AGENTCORE_DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Purple AI MCP - Deployment on Amazon Bedrock Agent Core

This guide will take you through deploying the Purple AI MCP Server to Amazon Bedrock AgentCore.

## Prerequisites

**Obtain a Sentinelone Singularity Operations Center console token**

This can be found in Policy & Settings → User Management → Service Users in your console. Currently, this server only supports tokens that have access to a single Account or Site. If you need to access multiple sites, you will need to run multiple MCP servers with Account-specific or Site-specific tokens.

**Prepare Environment Variables**

```bash
PURPLEMCP_CONSOLE_BASE_URL=https://your-console.sentinelone.net
PURPLEMCP_CONSOLE_TOKEN=your-token
MCP_MODE=streamable-http
PURPLEMCP_STATELESS_HTTP=True
```

**Prepare AWS Environment**

### VPC Configuration

**Outbound internet access** - Outbound internet access should be permitted through an Internet Gateway or NAT Gateway. \
**Security Groups** - Allow outbound HTTPS on port 443 to connect to the Sentinelone service.

It is important to note that Purple AI MCP does not include built-in authentication. For network exposed AWS deployments, ensure the MCP Server is placed behind an Application Load Balancer (ALB) with the appropriate timeout settings. Detailed information on production setups can be found [here](PRODUCTION_SETUP.md#cloud-load-balancer-setup).

### IAM Configuration

When deploying Purple AI MCP via AWS Marketplace a 'default' service role will be automatically created. To use a customer-managed service role reference the IAM Policy below.

[Trust Policy](bedrock-agentcore-trust-policy.json)
[IAM Policy](bedrock-agentcore-iam-policy.json)

## Agent Core Configuration Settings

When configuring the MCP in Bedrock Agent core, set the following.

1. **Name** - Give the deployment a suitable name and description.
2. **Agent Source** - Default (ECR) as this is automatically populated via AWS Marketplace.
3. **IAM Permissions** - Default service role or create a custom service role based on the IAM Configuration step [above](#iam-configuration).
4. **Protocol** - Set to MCP.
5. **Inbound Identity Type** - Select JWT (JSON Web Tokens) or IAM.
- **JWT** - Requires IdP bearer tokens to invoke the agent.
- **IAM** - Requires relevant IAM Permissions to invoke the agent.
6. **Security Type** - Select VPC and set the relevant VPC, Subnets & Security Groups.
7. **Advanced Settings** - Add environment variables as noted in the [prerequisites](#prerequisites).
8. Click **Host Agent** to complete the setup.

The Agent will take around ~1-2 minutes to become active.

## Useful Links

[Production VPC Setup for Purple AI MCP](PRODUCTION_SETUP.md) \
[AWS Bedrock AgentCore Overview](https://aws.amazon.com/bedrock/agentcore/) \
[Granting a VPC Internet Access via Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html) \
[Creating custom IAM Roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-service.html)

14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ docker run -p 8000:8000 \
purple-mcp:latest
```

### Using Amazon Bedrock AgentCore
```bash
# Subscribe to Purple AI MCP Server via AWS Marketplace

#Prepare Environment Variables
PURPLEMCP_CONSOLE_BASE_URL=https://your-console.sentinelone.net
PURPLEMCP_CONSOLE_TOKEN=your-token
MCP_MODE=streamable-http
PURPLEMCP_STATELESS_HTTP=True
```
Follow instructions for Amazon Bedrock AgentCore Deployment [here](BEDROCK_AGENTCORE_DEPLOYMENT.md)


For production deployments, see [Deployment Guide](DOCKER.md).

**Note:** Purple AI MCP does not include built-in authentication. For network-exposed deployments, place it behind a reverse proxy or load balancer. See [Production Setup](PRODUCTION_SETUP.md) for cloud load balancer configurations (AWS ALB, GCP Cloud Load Balancing, Azure Application Gateway) or nginx examples for self-hosted deployments.
Expand Down Expand Up @@ -189,6 +202,7 @@ 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)


## Development
Expand Down
100 changes: 100 additions & 0 deletions bedrock-agentcore-iam-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ECRImageAccess",
"Effect": "Allow",
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
],
"Resource": [
"arn:aws:ecr:us-east-1:709825985650:repository/sentinelone/purple-ai-mcp-server"
]
},
{
"Effect": "Allow",
"Action": [
"logs:DescribeLogStreams",
"logs:CreateLogGroup"
],
"Resource": [
"arn:aws:logs:{{region}}:{{accountId}}:log-group:/aws/bedrock-agentcore/runtimes/*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:DescribeLogGroups"
],
"Resource": [
"arn:aws:logs:{{region}}:{{accountId}}:log-group:*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:{{region}}:{{accountId}}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"
]
},
{
"Sid": "ECRTokenAccess",
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Resource": "*",
"Action": "cloudwatch:PutMetricData",
"Condition": {
"StringEquals": {
"cloudwatch:namespace": "bedrock-agentcore"
}
}
},
{
"Sid": "GetAgentAccessToken",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:GetWorkloadAccessToken",
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
],
"Resource": [
"arn:aws:bedrock-agentcore:{{region}}:{{accountId}}:workload-identity-directory/default",
"arn:aws:bedrock-agentcore:{{region}}:{{accountId}}:workload-identity-directory/default/workload-identity/hosted_agent_s26ce-*"
]
},
{
"Sid": "BedrockModelInvocation",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:bedrock:{{region}}:{{accountId}}:*"
]
}
]
}
21 changes: 21 additions & 0 deletions bedrock-agentcore-trust-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeRolePolicy",
"Effect": "Allow",
"Principal": {
"Service": "bedrock-agentcore.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "{{accountId}}"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:bedrock-agentcore:{{region}}:{{accountId}}:*"
}
}
}
]
}
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ services:
MCP_MODE: streamable-http
MCP_HOST: 0.0.0.0
MCP_PORT: 8000
PURPLEMCP_STATELESS_HTTP: ${PURPLEMCP_STATELESS_HTTP}
# Required: SentinelOne Console configuration
PURPLEMCP_CONSOLE_BASE_URL: ${PURPLEMCP_CONSOLE_BASE_URL}
PURPLEMCP_CONSOLE_TOKEN: ${PURPLEMCP_CONSOLE_TOKEN}
Expand Down
35 changes: 33 additions & 2 deletions src/purple_mcp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,30 @@ def _setup_logging(verbose: bool) -> None:


def _apply_environment_overrides(
transport_mode: str | None,
sdl_api_token: str | None,
graphql_service_token: str | None,
console_base_url: str | None,
graphql_endpoint: str | None,
alerts_graphql_endpoint: str | None,
stateless_http: bool | None,
) -> None:
"""Apply CLI argument values to environment variables.

Populates os.environ with provided CLI arguments, allowing command-line
configuration to override environment variable defaults.

Args:
transport_mode: MCP transport mode to use.
sdl_api_token: SDL API authentication token
graphql_service_token: GraphQL service authentication token
console_base_url: Base URL for the console
graphql_endpoint: GraphQL endpoint path
alerts_graphql_endpoint: Alerts GraphQL endpoint path
stateless_http: Uses true stateless mode (new transport per request)
"""
if transport_mode:
os.environ[f"{ENV_PREFIX}TRANSPORT_MODE"] = transport_mode
if sdl_api_token:
os.environ[f"{ENV_PREFIX}SDL_READ_LOGS_TOKEN"] = sdl_api_token
if graphql_service_token:
Expand All @@ -102,6 +108,8 @@ def _apply_environment_overrides(
and alerts_graphql_endpoint != "/web/api/v2.1/unifiedalerts/graphql"
):
os.environ[f"{ENV_PREFIX}ALERTS_GRAPHQL_ENDPOINT"] = alerts_graphql_endpoint
if stateless_http:
os.environ[f"{ENV_PREFIX}STATELESS_HTTP"] = str(stateless_http)


def _check_unsupported_config() -> None:
Expand Down Expand Up @@ -240,6 +248,7 @@ def _run_uvicorn(
port: int,
verbose: bool,
allow_remote_access: bool,
stateless_http: bool,
) -> None: # pragma: no cover - uvicorn is mocked in unit-tests
"""Run the HTTP/SSE transport using *uvicorn*."""
# Validate host binding for security
Expand All @@ -258,7 +267,8 @@ def _run_uvicorn(
from purple_mcp.server import app

# Create the HTTP app and instrument it if Logfire is enabled
http_app = app.http_app(transport=transport)
# n.b. stateless_http has no effect if running in "sse" transport mode.
http_app = app.http_app(transport=transport, stateless_http=stateless_http)
instrument_starlette_app(http_app)

uvicorn.run(
Expand All @@ -280,21 +290,28 @@ def _run_mode(
verbose: bool,
no_banner: bool = False,
allow_remote_access: bool = False,
stateless_http: bool = False,
) -> None:
"""Dispatch to the appropriate server runner for *mode*."""
mode_normalised = mode.lower()

runners: Mapping[str, Callable[[], None]] = {
"stdio": lambda: _run_stdio(verbose, no_banner),
"sse": lambda: _run_uvicorn(
"sse", host=host, port=port, verbose=verbose, allow_remote_access=allow_remote_access
"sse",
host=host,
port=port,
verbose=verbose,
allow_remote_access=allow_remote_access,
stateless_http=stateless_http,
),
"streamable-http": lambda: _run_uvicorn(
"streamable-http",
host=host,
port=port,
verbose=verbose,
allow_remote_access=allow_remote_access,
stateless_http=stateless_http,
),
}

Expand All @@ -308,6 +325,7 @@ def _run_mode(
"-m",
type=click.Choice(VALID_MODES, case_sensitive=False),
default="stdio",
envvar=f"{ENV_PREFIX}TRANSPORT_MODE",
help="MCP transport mode to use",
)
@click.option(
Expand Down Expand Up @@ -365,6 +383,15 @@ def _run_mode(
is_flag=True,
help="Allow binding to non-loopback interfaces (SECURITY RISK: exposes unauthenticated tools)",
)
@click.option(
"--stateless-http",
is_flag=True,
envvar=f"{ENV_PREFIX}STATELESS_HTTP",
help=(
"Uses true stateless mode (new transport per request). This flag only has an effect if running in"
" http or streamable_http modes."
),
)
def main(
mode: str,
host: str,
Expand All @@ -377,6 +404,7 @@ def main(
verbose: bool,
banner: bool,
allow_remote_access: bool,
stateless_http: bool,
) -> None:
"""Purple MCP Server - AI monitoring and analysis tool."""
_setup_logging(verbose)
Expand All @@ -385,11 +413,13 @@ def main(
_check_unsupported_config()

_apply_environment_overrides(
transport_mode=mode,
sdl_api_token=sdl_api_token,
graphql_service_token=graphql_service_token,
console_base_url=console_base_url,
graphql_endpoint=graphql_endpoint,
alerts_graphql_endpoint=alerts_graphql_endpoint,
stateless_http=stateless_http,
)

settings = _create_settings()
Expand All @@ -406,6 +436,7 @@ def main(
verbose=verbose,
no_banner=not banner,
allow_remote_access=allow_remote_access,
stateless_http=stateless_http,
)


Expand Down
16 changes: 15 additions & 1 deletion src/purple_mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import logging
import uuid
from functools import lru_cache
from typing import ClassVar, Final
from typing import ClassVar, Final, Literal
from urllib.parse import urlparse

from pydantic import Field, field_validator
Expand Down Expand Up @@ -45,6 +45,8 @@
PURPLE_AI_CONSOLE_VERSION_ENV: Final[str] = f"{ENV_PREFIX}PURPLE_AI_CONSOLE_VERSION"
ENVIRONMENT_ENV: Final[str] = f"{ENV_PREFIX}ENV"
LOGFIRE_TOKEN_ENV: Final[str] = f"{ENV_PREFIX}LOGFIRE_TOKEN"
STATELESS_HTTP_ENV = f"{ENV_PREFIX}STATELESS_HTTP"
TRANSPORT_MODE_ENV = f"{ENV_PREFIX}TRANSPORT_MODE"


class Settings(BaseSettings):
Expand Down Expand Up @@ -170,6 +172,18 @@ class Settings(BaseSettings):
validation_alias=LOGFIRE_TOKEN_ENV,
)

stateless_http: bool | None = 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)",
validation_alias=TRANSPORT_MODE_ENV,
)

@field_validator("sentinelone_console_base_url")
@classmethod
def validate_console_base_url(cls, v: str) -> str:
Expand Down
Loading
Loading