Skip to content

Commit 4e79ddf

Browse files
Create Simple MCP Server for Integ tests (#29)
1 parent a046414 commit 4e79ddf

11 files changed

Lines changed: 387 additions & 97 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,6 @@ build
6969

7070
# Documentation
7171
/doc/_apidoc/
72+
73+
# Ignoring AgentCore config files for Simple MCP Server
74+
/tests/integ/mcp/simple_mcp_server/*

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ packages = ["aws_mcp_proxy"]
138138
exclude_dirs = ["venv", ".venv", "tests"]
139139

140140
[tool.pytest.ini_options]
141+
log_cli = true
142+
log_cli_level = "INFO"
141143
python_files = "test_*.py"
142144
python_classes = "Test*"
143145
python_functions = "test_*"

tests/integ/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
## Integration tests
2+
3+
This folder contains the Integration tests for the aws-mcp-proxy. To help with testing, we have also created a simple MCP Server and MCP Client to mimic a real scenario.
4+
5+
* `mcp/simple_mcp_server/mcp_server.py` - A simple MCP Server which supports different features to mimic the Customers remote MCP Server
6+
* `mcp/simple_mcp_client.py` - A simple MCP Client which uses the proxy to connect to a remote HTTP MCP Server
7+
* `test_proxy_simple_mcp_server.py` - Actual tests which uses the above to validate the Proxy is working correctly
8+
9+
## How to use?
10+
11+
These tests can be run against two types of Remote MCP Servers
12+
13+
1. Hosted in Bedrock AgentCore Runtime
14+
1. Against a Remote URL endpoint
15+
16+
#### Hosted in Bedrock AgentCore Runtime
17+
18+
The Simple MCP Server is ready to easily install on AgentCore Bedrock. It is recommended to follow this testing path to ensure sigv4 is working correctly.
19+
20+
The following instructions to install the Simple MCP Server came from [[Deploy MCP servers in AgentCore Runtime](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-mcp.html#runtime-mcp-create-server)].
21+
22+
```bash
23+
# Update Simple MCP Server to use stateless-http
24+
# - Edit mcp/simple_mcp_server/mcp_server.py
25+
# - Change "stateless_http=False" to "stateless_http=True"
26+
27+
# Install AgentCore CLI
28+
pip install bedrock-agentcore-starter-toolkit
29+
30+
# CD into simple_mcp_server folder
31+
cd integ/mcp/simple_mcp_server
32+
33+
# Create ECR image from simple server
34+
# The default options will work (OAuth is not required)
35+
agentcore configure -e mcp_server.py --protocol MCP
36+
37+
# Upload ECR image and host MCP Server
38+
agentcore launch --auto-update-on-conflict
39+
40+
# Set the ARN which was posted to the Terminal
41+
export AGENTCORE_RUNTIME_ARN={newly-created-mcp-server}
42+
```
43+
44+
Run test against the AgentCore hosted MCP Server
45+
46+
```bash
47+
uv run pytest -m integ
48+
```
49+
50+
51+
#### Against a Remote URL endpoint
52+
53+
To make testing locally faster, you can also run tests against a remote URL. Since this endpoint might not be hosted on AWS, the sigv4 code path might not be fully tested.
54+
55+
Only use this testing path to ensure the MCP Features are working correctly.
56+
57+
```bash
58+
# Run MCP Server locally
59+
uv run tests/integ/mcp/simple_mcp_server/mcp_server.py
60+
61+
# Set endpoint to test against
62+
export REMOTE_ENDPOINT_URL=http://127.0.0.1:8000/mcp
63+
```
64+
65+
```bash
66+
uv run pytest -m integ
67+
```

tests/integ/conftest.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import logging
2+
import os
3+
import pytest
4+
import pytest_asyncio
5+
from .mcp.simple_mcp_client import build_mcp_client
6+
from typing import TypedDict
7+
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class RemoteMCPServerConfiguration(TypedDict):
13+
"""Remote MCP Server endpoint config."""
14+
15+
endpoint: str
16+
region_name: str
17+
18+
19+
@pytest_asyncio.fixture(loop_scope='module', scope='module')
20+
async def mcp_client(
21+
remote_mcp_server_configuration: RemoteMCPServerConfiguration,
22+
):
23+
"""Create MCP Client fixture for using in Integ tests."""
24+
client = build_mcp_client(
25+
endpoint=remote_mcp_server_configuration['endpoint'],
26+
region_name=remote_mcp_server_configuration['region_name'],
27+
)
28+
29+
async with client:
30+
yield client
31+
32+
33+
@pytest.fixture(scope='module')
34+
async def is_using_agentcore():
35+
"""Boolean param if we are currently running against AgentCore Runtime."""
36+
if os.environ.get('AGENTCORE_RUNTIME_ARN'):
37+
return True
38+
else:
39+
return False
40+
41+
42+
@pytest.fixture(scope='module')
43+
def remote_mcp_server_configuration(is_using_agentcore: bool):
44+
"""Configuration to connect to remotely hosted MCP Server."""
45+
if is_using_agentcore:
46+
logger.info('Will use AgentCore MCP Server')
47+
return _build_agent_core_remote_configuration()
48+
else:
49+
logger.info('Will use remote MCP server defined with ENV variables')
50+
return _build_endpoint_environment_remote_configuration()
51+
52+
53+
def _build_agent_core_remote_configuration():
54+
logger.info('Using AgentCore runtime ARN for remote MCP Server')
55+
56+
runtime_arn = os.environ.get('AGENTCORE_RUNTIME_ARN')
57+
if not runtime_arn:
58+
raise RuntimeError('AGENTCORE_RUNTIME_ARN env variable not found')
59+
60+
agent_core_runtime_url_format = 'https://bedrock-agentcore.{region_name}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT'
61+
region_name = runtime_arn.split(':')[3]
62+
encoded_arn = runtime_arn.replace(':', '%3A').replace('/', '%2F')
63+
64+
endpoint = agent_core_runtime_url_format.format(
65+
region_name=region_name, encoded_arn=encoded_arn
66+
)
67+
68+
return RemoteMCPServerConfiguration(endpoint=endpoint, region_name=region_name)
69+
70+
71+
def _build_endpoint_environment_remote_configuration():
72+
logger.info('Using Endpoint environment variable for remote MCP Server')
73+
74+
remote_endpoint_url = os.environ.get('REMOTE_ENDPOINT_URL')
75+
if not remote_endpoint_url:
76+
raise RuntimeError('REMOTE_ENDPOINT_URL env variable not found')
77+
78+
region_name = os.environ.get('AWS_REGION')
79+
if not region_name:
80+
logger.warn('AWS_REGION param not set. Defaulting to us-east-1')
81+
region_name = 'us-east-1'
82+
83+
logger.info(f'Starting server with config - {remote_endpoint_url=} and {region_name=}')
84+
85+
return RemoteMCPServerConfiguration(
86+
endpoint=remote_endpoint_url,
87+
region_name=region_name,
88+
)

tests/integ/mcp/__init__.py

Whitespace-only changes.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import boto3
2+
import fastmcp
3+
import logging
4+
from fastmcp.client import StdioTransport
5+
from fastmcp.client.elicitation import ElicitResult
6+
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def build_mcp_client(endpoint: str, region_name: str) -> fastmcp.Client:
12+
"""Create a MCP Client using the aws-mcp-proxy against a remote MCP Server."""
13+
return fastmcp.Client(
14+
StdioTransport(
15+
**_build_mcp_config(
16+
endpoint=endpoint,
17+
region_name=region_name,
18+
)
19+
),
20+
elicitation_handler=_basic_elicitation_handler,
21+
timeout=10.0, # seconds
22+
)
23+
24+
25+
async def _basic_elicitation_handler(message: str, response_type: type, params, context):
26+
logger.info(f'Server asks: {message} with response_type {response_type}')
27+
28+
# Usually the Handler would expect an user Input to control flow via Accept, Decline, Cancel
29+
# But in this Integ test we only care that an Elicitation request went through the handler
30+
# and responded correctly.
31+
# As such, we are explicitly hardcoding the response based on the name of the ResponseType object
32+
33+
if 'Accept' in response_type.__name__:
34+
return response_type(value='Elicitation success')
35+
36+
if 'Decline' in response_type.__name__:
37+
return ElicitResult(action='decline')
38+
39+
raise RuntimeError(f'Unknown Response-type, rather failing - {response_type}')
40+
41+
42+
def _build_mcp_config(endpoint: str, region_name: str):
43+
credentials = boto3.Session().get_credentials()
44+
45+
environment_variables = {
46+
'AWS_REGION': region_name,
47+
'AWS_ACCESS_KEY_ID': credentials.access_key,
48+
'AWS_SECRET_ACCESS_KEY': credentials.secret_key,
49+
'AWS_SESSION_TOKEN': credentials.token,
50+
}
51+
52+
return {
53+
'command': 'aws-mcp-proxy',
54+
'args': [endpoint, '--log-level', 'DEBUG'],
55+
'env': environment_variables,
56+
}

tests/integ/mcp/simple_mcp_server/__init__.py

Whitespace-only changes.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import logging
2+
from dataclasses import dataclass
3+
from fastmcp import Context, FastMCP
4+
from typing import Any
5+
6+
7+
logger = logging.getLogger(__name__)
8+
9+
mcp = FastMCP[Any](
10+
name='Simple MCP Server',
11+
instructions=('Simple MCP Server used in Integ Tests'),
12+
)
13+
14+
##### Generic Tool Testing
15+
16+
17+
@mcp.tool
18+
def greet(name: str):
19+
"""MCP Tool which is very simple for testing."""
20+
return f'Hello {name}'
21+
22+
23+
##### Elicitation Testing
24+
25+
26+
@dataclass
27+
class ElicitationWithAccept:
28+
"""Class type when requesting Elicitation and expecting it to be accepted."""
29+
30+
value: str
31+
32+
33+
@dataclass
34+
class ElicitationWithDecline:
35+
"""Class type when requesting Elicitation and expecting it to be declined."""
36+
37+
value: str
38+
39+
40+
@mcp.tool
41+
async def elicit_for_my_name(elicitation_expected: str, ctx: Context):
42+
"""MCP Tool which supports elicitation."""
43+
response_type = ElicitationWithAccept
44+
45+
if 'Decline' in elicitation_expected:
46+
response_type = ElicitationWithDecline
47+
48+
result = await ctx.elicit(message='What is your name?', response_type=response_type)
49+
50+
if result.action == 'accept':
51+
return f'Nice to meet you - {result.data.value}'
52+
elif result.action == 'decline':
53+
return 'Information not provided'
54+
else:
55+
return 'cancelled'
56+
57+
58+
#### Server Setup
59+
60+
61+
def main():
62+
"""Main entrypoint for running this MCP Server."""
63+
logger.info('Starting Simple MCP Server')
64+
65+
mcp.run(
66+
transport='http',
67+
host='0.0.0.0',
68+
port=8000,
69+
# By default, this param is set to False to ensure the Elicitation feature is working
70+
# When deploying to AgentCore, this flag must be set to True for MCP to work
71+
stateless_http=False,
72+
)
73+
74+
75+
if __name__ == '__main__':
76+
main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fastmcp
2+
botocore

0 commit comments

Comments
 (0)