Skip to content

Commit 8b29d03

Browse files
authored
Add stateless_http configuration option (#12)
1 parent adefcde commit 8b29d03

File tree

11 files changed

+422
-15
lines changed

11 files changed

+422
-15
lines changed

BEDROCK_AGENTCORE_DEPLOYMENT.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Purple AI MCP - Deployment on Amazon Bedrock Agent Core
2+
3+
This guide will take you through deploying the Purple AI MCP Server to Amazon Bedrock AgentCore.
4+
5+
## Prerequisites
6+
7+
**Obtain a Sentinelone Singularity Operations Center console token**
8+
9+
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.
10+
11+
**Prepare Environment Variables**
12+
13+
```bash
14+
PURPLEMCP_CONSOLE_BASE_URL=https://your-console.sentinelone.net
15+
PURPLEMCP_CONSOLE_TOKEN=your-token
16+
MCP_MODE=streamable-http
17+
PURPLEMCP_STATELESS_HTTP=True
18+
```
19+
20+
**Prepare AWS Environment**
21+
22+
### VPC Configuration
23+
24+
**Outbound internet access** - Outbound internet access should be permitted through an Internet Gateway or NAT Gateway. \
25+
**Security Groups** - Allow outbound HTTPS on port 443 to connect to the Sentinelone service.
26+
27+
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).
28+
29+
### IAM Configuration
30+
31+
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.
32+
33+
[Trust Policy](bedrock-agentcore-trust-policy.json)
34+
[IAM Policy](bedrock-agentcore-iam-policy.json)
35+
36+
## Agent Core Configuration Settings
37+
38+
When configuring the MCP in Bedrock Agent core, set the following.
39+
40+
1. **Name** - Give the deployment a suitable name and description.
41+
2. **Agent Source** - Default (ECR) as this is automatically populated via AWS Marketplace.
42+
3. **IAM Permissions** - Default service role or create a custom service role based on the IAM Configuration step [above](#iam-configuration).
43+
4. **Protocol** - Set to MCP.
44+
5. **Inbound Identity Type** - Select JWT (JSON Web Tokens) or IAM.
45+
- **JWT** - Requires IdP bearer tokens to invoke the agent.
46+
- **IAM** - Requires relevant IAM Permissions to invoke the agent.
47+
6. **Security Type** - Select VPC and set the relevant VPC, Subnets & Security Groups.
48+
7. **Advanced Settings** - Add environment variables as noted in the [prerequisites](#prerequisites).
49+
8. Click **Host Agent** to complete the setup.
50+
51+
The Agent will take around ~1-2 minutes to become active.
52+
53+
## Useful Links
54+
55+
[Production VPC Setup for Purple AI MCP](PRODUCTION_SETUP.md) \
56+
[AWS Bedrock AgentCore Overview](https://aws.amazon.com/bedrock/agentcore/) \
57+
[Granting a VPC Internet Access via Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html) \
58+
[Creating custom IAM Roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-service.html)
59+

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@ docker run -p 8000:8000 \
5858
purple-mcp:latest
5959
```
6060

61+
### Using Amazon Bedrock AgentCore
62+
```bash
63+
# Subscribe to Purple AI MCP Server via AWS Marketplace
64+
65+
#Prepare Environment Variables
66+
PURPLEMCP_CONSOLE_BASE_URL=https://your-console.sentinelone.net
67+
PURPLEMCP_CONSOLE_TOKEN=your-token
68+
MCP_MODE=streamable-http
69+
PURPLEMCP_STATELESS_HTTP=True
70+
```
71+
Follow instructions for Amazon Bedrock AgentCore Deployment [here](BEDROCK_AGENTCORE_DEPLOYMENT.md)
72+
73+
6174
For production deployments, see [Deployment Guide](DOCKER.md).
6275

6376
**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.
@@ -189,6 +202,7 @@ We suggest you **do not** expose Purple AI MCP on a network at this time, as the
189202
## Environment Variables
190203
- `PURPLEMCP_CONSOLE_TOKEN` - Service user token (Account or Site level)
191204
- `PURPLEMCP_CONSOLE_BASE_URL` - Console URL (e.g., https://console.sentinelone.net)
205+
- `PURPLEMCP_STATELESS_HTTP` - For use with deployment in Amazon Bedrock Agent Core - Detailed instructions can be found [here](BEDROCK_AGENTCORE_DEPLOYMENT.md)
192206

193207

194208
## Development

bedrock-agentcore-iam-policy.json

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
{
2+
"Version": "2012-10-17",
3+
"Statement": [
4+
{
5+
"Sid": "ECRImageAccess",
6+
"Effect": "Allow",
7+
"Action": [
8+
"ecr:BatchGetImage",
9+
"ecr:GetDownloadUrlForLayer"
10+
],
11+
"Resource": [
12+
"arn:aws:ecr:us-east-1:709825985650:repository/sentinelone/purple-ai-mcp-server"
13+
]
14+
},
15+
{
16+
"Effect": "Allow",
17+
"Action": [
18+
"logs:DescribeLogStreams",
19+
"logs:CreateLogGroup"
20+
],
21+
"Resource": [
22+
"arn:aws:logs:{{region}}:{{accountId}}:log-group:/aws/bedrock-agentcore/runtimes/*"
23+
]
24+
},
25+
{
26+
"Effect": "Allow",
27+
"Action": [
28+
"logs:DescribeLogGroups"
29+
],
30+
"Resource": [
31+
"arn:aws:logs:{{region}}:{{accountId}}:log-group:*"
32+
]
33+
},
34+
{
35+
"Effect": "Allow",
36+
"Action": [
37+
"logs:CreateLogStream",
38+
"logs:PutLogEvents"
39+
],
40+
"Resource": [
41+
"arn:aws:logs:{{region}}:{{accountId}}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"
42+
]
43+
},
44+
{
45+
"Sid": "ECRTokenAccess",
46+
"Effect": "Allow",
47+
"Action": [
48+
"ecr:GetAuthorizationToken"
49+
],
50+
"Resource": "*"
51+
},
52+
{
53+
"Effect": "Allow",
54+
"Action": [
55+
"xray:PutTraceSegments",
56+
"xray:PutTelemetryRecords",
57+
"xray:GetSamplingRules",
58+
"xray:GetSamplingTargets"
59+
],
60+
"Resource": [
61+
"*"
62+
]
63+
},
64+
{
65+
"Effect": "Allow",
66+
"Resource": "*",
67+
"Action": "cloudwatch:PutMetricData",
68+
"Condition": {
69+
"StringEquals": {
70+
"cloudwatch:namespace": "bedrock-agentcore"
71+
}
72+
}
73+
},
74+
{
75+
"Sid": "GetAgentAccessToken",
76+
"Effect": "Allow",
77+
"Action": [
78+
"bedrock-agentcore:GetWorkloadAccessToken",
79+
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
80+
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
81+
],
82+
"Resource": [
83+
"arn:aws:bedrock-agentcore:{{region}}:{{accountId}}:workload-identity-directory/default",
84+
"arn:aws:bedrock-agentcore:{{region}}:{{accountId}}:workload-identity-directory/default/workload-identity/hosted_agent_s26ce-*"
85+
]
86+
},
87+
{
88+
"Sid": "BedrockModelInvocation",
89+
"Effect": "Allow",
90+
"Action": [
91+
"bedrock:InvokeModel",
92+
"bedrock:InvokeModelWithResponseStream"
93+
],
94+
"Resource": [
95+
"arn:aws:bedrock:*::foundation-model/*",
96+
"arn:aws:bedrock:{{region}}:{{accountId}}:*"
97+
]
98+
}
99+
]
100+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"Version": "2012-10-17",
3+
"Statement": [
4+
{
5+
"Sid": "AssumeRolePolicy",
6+
"Effect": "Allow",
7+
"Principal": {
8+
"Service": "bedrock-agentcore.amazonaws.com"
9+
},
10+
"Action": "sts:AssumeRole",
11+
"Condition": {
12+
"StringEquals": {
13+
"aws:SourceAccount": "{{accountId}}"
14+
},
15+
"ArnLike": {
16+
"aws:SourceArn": "arn:aws:bedrock-agentcore:{{region}}:{{accountId}}:*"
17+
}
18+
}
19+
}
20+
]
21+
}

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ services:
3737
MCP_MODE: streamable-http
3838
MCP_HOST: 0.0.0.0
3939
MCP_PORT: 8000
40+
PURPLEMCP_STATELESS_HTTP: ${PURPLEMCP_STATELESS_HTTP}
4041
# Required: SentinelOne Console configuration
4142
PURPLEMCP_CONSOLE_BASE_URL: ${PURPLEMCP_CONSOLE_BASE_URL}
4243
PURPLEMCP_CONSOLE_TOKEN: ${PURPLEMCP_CONSOLE_TOKEN}

src/purple_mcp/cli.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,30 @@ def _setup_logging(verbose: bool) -> None:
7171

7272

7373
def _apply_environment_overrides(
74+
transport_mode: str | None,
7475
sdl_api_token: str | None,
7576
graphql_service_token: str | None,
7677
console_base_url: str | None,
7778
graphql_endpoint: str | None,
7879
alerts_graphql_endpoint: str | None,
80+
stateless_http: bool | None,
7981
) -> None:
8082
"""Apply CLI argument values to environment variables.
8183
8284
Populates os.environ with provided CLI arguments, allowing command-line
8385
configuration to override environment variable defaults.
8486
8587
Args:
88+
transport_mode: MCP transport mode to use.
8689
sdl_api_token: SDL API authentication token
8790
graphql_service_token: GraphQL service authentication token
8891
console_base_url: Base URL for the console
8992
graphql_endpoint: GraphQL endpoint path
9093
alerts_graphql_endpoint: Alerts GraphQL endpoint path
94+
stateless_http: Uses true stateless mode (new transport per request)
9195
"""
96+
if transport_mode:
97+
os.environ[f"{ENV_PREFIX}TRANSPORT_MODE"] = transport_mode
9298
if sdl_api_token:
9399
os.environ[f"{ENV_PREFIX}SDL_READ_LOGS_TOKEN"] = sdl_api_token
94100
if graphql_service_token:
@@ -102,6 +108,8 @@ def _apply_environment_overrides(
102108
and alerts_graphql_endpoint != "/web/api/v2.1/unifiedalerts/graphql"
103109
):
104110
os.environ[f"{ENV_PREFIX}ALERTS_GRAPHQL_ENDPOINT"] = alerts_graphql_endpoint
111+
if stateless_http:
112+
os.environ[f"{ENV_PREFIX}STATELESS_HTTP"] = str(stateless_http)
105113

106114

107115
def _check_unsupported_config() -> None:
@@ -240,6 +248,7 @@ def _run_uvicorn(
240248
port: int,
241249
verbose: bool,
242250
allow_remote_access: bool,
251+
stateless_http: bool,
243252
) -> None: # pragma: no cover - uvicorn is mocked in unit-tests
244253
"""Run the HTTP/SSE transport using *uvicorn*."""
245254
# Validate host binding for security
@@ -258,7 +267,8 @@ def _run_uvicorn(
258267
from purple_mcp.server import app
259268

260269
# Create the HTTP app and instrument it if Logfire is enabled
261-
http_app = app.http_app(transport=transport)
270+
# n.b. stateless_http has no effect if running in "sse" transport mode.
271+
http_app = app.http_app(transport=transport, stateless_http=stateless_http)
262272
instrument_starlette_app(http_app)
263273

264274
uvicorn.run(
@@ -280,21 +290,28 @@ def _run_mode(
280290
verbose: bool,
281291
no_banner: bool = False,
282292
allow_remote_access: bool = False,
293+
stateless_http: bool = False,
283294
) -> None:
284295
"""Dispatch to the appropriate server runner for *mode*."""
285296
mode_normalised = mode.lower()
286297

287298
runners: Mapping[str, Callable[[], None]] = {
288299
"stdio": lambda: _run_stdio(verbose, no_banner),
289300
"sse": lambda: _run_uvicorn(
290-
"sse", host=host, port=port, verbose=verbose, allow_remote_access=allow_remote_access
301+
"sse",
302+
host=host,
303+
port=port,
304+
verbose=verbose,
305+
allow_remote_access=allow_remote_access,
306+
stateless_http=stateless_http,
291307
),
292308
"streamable-http": lambda: _run_uvicorn(
293309
"streamable-http",
294310
host=host,
295311
port=port,
296312
verbose=verbose,
297313
allow_remote_access=allow_remote_access,
314+
stateless_http=stateless_http,
298315
),
299316
}
300317

@@ -308,6 +325,7 @@ def _run_mode(
308325
"-m",
309326
type=click.Choice(VALID_MODES, case_sensitive=False),
310327
default="stdio",
328+
envvar=f"{ENV_PREFIX}TRANSPORT_MODE",
311329
help="MCP transport mode to use",
312330
)
313331
@click.option(
@@ -365,6 +383,15 @@ def _run_mode(
365383
is_flag=True,
366384
help="Allow binding to non-loopback interfaces (SECURITY RISK: exposes unauthenticated tools)",
367385
)
386+
@click.option(
387+
"--stateless-http",
388+
is_flag=True,
389+
envvar=f"{ENV_PREFIX}STATELESS_HTTP",
390+
help=(
391+
"Uses true stateless mode (new transport per request). This flag only has an effect if running in"
392+
" http or streamable_http modes."
393+
),
394+
)
368395
def main(
369396
mode: str,
370397
host: str,
@@ -377,6 +404,7 @@ def main(
377404
verbose: bool,
378405
banner: bool,
379406
allow_remote_access: bool,
407+
stateless_http: bool,
380408
) -> None:
381409
"""Purple MCP Server - AI monitoring and analysis tool."""
382410
_setup_logging(verbose)
@@ -385,11 +413,13 @@ def main(
385413
_check_unsupported_config()
386414

387415
_apply_environment_overrides(
416+
transport_mode=mode,
388417
sdl_api_token=sdl_api_token,
389418
graphql_service_token=graphql_service_token,
390419
console_base_url=console_base_url,
391420
graphql_endpoint=graphql_endpoint,
392421
alerts_graphql_endpoint=alerts_graphql_endpoint,
422+
stateless_http=stateless_http,
393423
)
394424

395425
settings = _create_settings()
@@ -406,6 +436,7 @@ def main(
406436
verbose=verbose,
407437
no_banner=not banner,
408438
allow_remote_access=allow_remote_access,
439+
stateless_http=stateless_http,
409440
)
410441

411442

src/purple_mcp/config.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import logging
1010
import uuid
1111
from functools import lru_cache
12-
from typing import ClassVar, Final
12+
from typing import ClassVar, Final, Literal
1313
from urllib.parse import urlparse
1414

1515
from pydantic import Field, field_validator
@@ -45,6 +45,8 @@
4545
PURPLE_AI_CONSOLE_VERSION_ENV: Final[str] = f"{ENV_PREFIX}PURPLE_AI_CONSOLE_VERSION"
4646
ENVIRONMENT_ENV: Final[str] = f"{ENV_PREFIX}ENV"
4747
LOGFIRE_TOKEN_ENV: Final[str] = f"{ENV_PREFIX}LOGFIRE_TOKEN"
48+
STATELESS_HTTP_ENV = f"{ENV_PREFIX}STATELESS_HTTP"
49+
TRANSPORT_MODE_ENV = f"{ENV_PREFIX}TRANSPORT_MODE"
4850

4951

5052
class Settings(BaseSettings):
@@ -170,6 +172,18 @@ class Settings(BaseSettings):
170172
validation_alias=LOGFIRE_TOKEN_ENV,
171173
)
172174

175+
stateless_http: bool | None = Field(
176+
default=False,
177+
description="Stateless mode (new transport per request)",
178+
validation_alias=STATELESS_HTTP_ENV,
179+
)
180+
181+
transport_mode: Literal["http", "streamable-http", "sse"] = Field(
182+
default="sse",
183+
description="Stateless mode (new transport per request)",
184+
validation_alias=TRANSPORT_MODE_ENV,
185+
)
186+
173187
@field_validator("sentinelone_console_base_url")
174188
@classmethod
175189
def validate_console_base_url(cls, v: str) -> str:

0 commit comments

Comments
 (0)