Skip to content

Commit 692a291

Browse files
authored
refactor(cli): extract argument parsing to separate module (#70)
1 parent 9b00656 commit 692a291

File tree

3 files changed

+260
-97
lines changed

3 files changed

+260
-97
lines changed

mcp_proxy_for_aws/cli.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Command-line interface argument parsing for MCP Proxy for AWS."""
16+
17+
import argparse
18+
import os
19+
from mcp_proxy_for_aws import __version__
20+
from mcp_proxy_for_aws.utils import within_range
21+
22+
23+
def parse_args():
24+
"""Parse command line arguments.
25+
26+
Returns:
27+
argparse.Namespace: Parsed command line arguments
28+
"""
29+
parser = argparse.ArgumentParser(
30+
description=f'MCP Proxy for AWS v{__version__}',
31+
formatter_class=argparse.RawDescriptionHelpFormatter,
32+
epilog="""
33+
Examples:
34+
# Run with your endpoint
35+
mcp-proxy-for-aws <SigV4 MCP endpoint URL>
36+
37+
# Run with custom service and profile
38+
mcp-proxy-for-aws <SigV4 MCP endpoint URL> --service <aws-service> --profile default
39+
40+
# Run with write permissions enabled
41+
mcp-proxy-for-aws <SigV4 MCP endpoint URL> --read-only
42+
""",
43+
)
44+
45+
parser.add_argument(
46+
'endpoint',
47+
help='SigV4 MCP endpoint URL',
48+
)
49+
50+
parser.add_argument(
51+
'--service',
52+
help='AWS service name for SigV4 signing (inferred from endpoint if not provided)',
53+
)
54+
55+
parser.add_argument(
56+
'--profile',
57+
help='AWS profile to use (uses AWS_PROFILE environment variable if not provided)',
58+
default=os.getenv('AWS_PROFILE'),
59+
)
60+
61+
parser.add_argument(
62+
'--region',
63+
help='AWS region to use (uses AWS_REGION environment variable if not provided, with final fallback to us-east-1)',
64+
default=None,
65+
)
66+
67+
parser.add_argument(
68+
'--read-only',
69+
action='store_true',
70+
help='Disable tools which may require write permissions (readOnlyHint True or unknown)',
71+
)
72+
73+
parser.add_argument(
74+
'--log-level',
75+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
76+
default='INFO',
77+
help='Set the logging level (default: INFO)',
78+
)
79+
80+
parser.add_argument(
81+
'--retries',
82+
type=int,
83+
default=0,
84+
choices=range(0, 11),
85+
metavar='[0-10]',
86+
help='Number of retries when calling endpoint mcp (default: 0) - setting this to 0 disables retries.',
87+
)
88+
89+
parser.add_argument(
90+
'--timeout',
91+
type=within_range(0),
92+
default=180.0,
93+
help='Timeout (seconds) when connecting to endpoint (default: 180)',
94+
)
95+
96+
parser.add_argument(
97+
'--connect-timeout',
98+
type=within_range(0),
99+
default=60.0,
100+
help='Connection timeout (seconds) when connecting to endpoint (default: 60)',
101+
)
102+
103+
parser.add_argument(
104+
'--read-timeout',
105+
type=within_range(0),
106+
default=120.0,
107+
help='Read timeout (seconds) when connecting to endpoint (default: 120)',
108+
)
109+
110+
parser.add_argument(
111+
'--write-timeout',
112+
type=within_range(0),
113+
default=180.0,
114+
help='Write timeout (seconds) when connecting to endpoint (default: 180)',
115+
)
116+
117+
return parser.parse_args()

mcp_proxy_for_aws/server.py

Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,19 @@
2222
5. Supporting tool refresh
2323
"""
2424

25-
import argparse
2625
import asyncio
2726
import httpx
2827
import logging
29-
import os
3028
from fastmcp.server.middleware.error_handling import RetryMiddleware
3129
from fastmcp.server.middleware.logging import LoggingMiddleware
3230
from fastmcp.server.server import FastMCP
33-
from mcp_proxy_for_aws import __version__
31+
from mcp_proxy_for_aws.cli import parse_args
3432
from mcp_proxy_for_aws.logging_config import configure_logging
3533
from mcp_proxy_for_aws.middleware.tool_filter import ToolFilteringMiddleware
3634
from mcp_proxy_for_aws.utils import (
3735
create_transport_with_sigv4,
3836
determine_aws_region,
3937
determine_service_name,
40-
within_range,
4138
)
4239
from typing import Any
4340

@@ -127,99 +124,6 @@ def add_logging_middleware(mcp: FastMCP, log_level: str) -> None:
127124
)
128125

129126

130-
def parse_args():
131-
"""Parse command line arguments."""
132-
parser = argparse.ArgumentParser(
133-
description=f'MCP Proxy for AWS v{__version__}',
134-
formatter_class=argparse.RawDescriptionHelpFormatter,
135-
epilog="""
136-
Examples:
137-
# Run with your endpoint
138-
mcp-proxy-for-aws <SigV4 MCP endpoint URL>
139-
140-
# Run with custom service and profile
141-
mcp-proxy-for-aws <SigV4 MCP endpoint URL> --service <aws-service> --profile default
142-
143-
# Run with write permissions enabled
144-
mcp-proxy-for-aws <SigV4 MCP endpoint URL> --read-only
145-
""",
146-
)
147-
148-
parser.add_argument(
149-
'endpoint',
150-
help='SigV4 MCP endpoint URL',
151-
)
152-
153-
parser.add_argument(
154-
'--service',
155-
help='AWS service name for SigV4 signing (inferred from endpoint if not provided)',
156-
)
157-
158-
parser.add_argument(
159-
'--profile',
160-
help='AWS profile to use (uses AWS_PROFILE environment variable if not provided)',
161-
default=os.getenv('AWS_PROFILE'),
162-
)
163-
164-
parser.add_argument(
165-
'--region',
166-
help='AWS region to use (uses AWS_REGION environment variable if not provided, with final fallback to us-east-1)',
167-
default=None,
168-
)
169-
170-
parser.add_argument(
171-
'--read-only',
172-
action='store_true',
173-
help='Disable tools which may require write permissions (readOnlyHint True or unknown)',
174-
)
175-
176-
parser.add_argument(
177-
'--log-level',
178-
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
179-
default='INFO',
180-
help='Set the logging level (default: INFO)',
181-
)
182-
183-
parser.add_argument(
184-
'--retries',
185-
type=int,
186-
default=0,
187-
choices=range(0, 11),
188-
metavar='[0-10]',
189-
help='Number of retries when calling endpoint mcp (default: 0) - setting this to 0 disables retries.',
190-
)
191-
192-
parser.add_argument(
193-
'--timeout',
194-
type=within_range(0),
195-
default=180.0,
196-
help='Timeout (seconds) when connecting to endpoint (default: 180)',
197-
)
198-
199-
parser.add_argument(
200-
'--connect-timeout',
201-
type=within_range(0),
202-
default=60.0,
203-
help='Connection timeout (seconds) when connecting to endpoint (default: 60)',
204-
)
205-
206-
parser.add_argument(
207-
'--read-timeout',
208-
type=within_range(0),
209-
default=120.0,
210-
help='Read timeout (seconds) when connecting to endpoint (default: 120)',
211-
)
212-
213-
parser.add_argument(
214-
'--write-timeout',
215-
type=within_range(0),
216-
default=180.0,
217-
help='Write timeout (seconds) when connecting to endpoint (default: 180)',
218-
)
219-
220-
return parser.parse_args()
221-
222-
223127
def main():
224128
"""Run the MCP server."""
225129
args = parse_args()

tests/unit/test_cli.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tests for CLI argument parsing."""
16+
17+
import pytest
18+
from mcp_proxy_for_aws.cli import parse_args
19+
from unittest.mock import patch
20+
21+
22+
class TestParseArgs:
23+
"""Tests for parse_args function."""
24+
25+
@patch('sys.argv', ['mcp-proxy-for-aws', 'https://test.example.com'])
26+
def test_parse_args_minimal(self):
27+
"""Test parsing with minimal required arguments."""
28+
args = parse_args()
29+
30+
assert args.endpoint == 'https://test.example.com'
31+
assert args.service is None
32+
assert args.profile is None
33+
assert args.region is None
34+
assert args.read_only is False
35+
assert args.log_level == 'INFO'
36+
assert args.retries == 0
37+
assert args.timeout == 180.0
38+
assert args.connect_timeout == 60.0
39+
assert args.read_timeout == 120.0
40+
assert args.write_timeout == 180.0
41+
42+
@patch(
43+
'sys.argv',
44+
[
45+
'mcp-proxy-for-aws',
46+
'https://test.example.com',
47+
'--service',
48+
'lambda',
49+
'--profile',
50+
'prod',
51+
'--region',
52+
'eu-west-1',
53+
'--read-only',
54+
'--log-level',
55+
'WARNING',
56+
'--retries',
57+
'3',
58+
'--timeout',
59+
'300',
60+
'--connect-timeout',
61+
'30',
62+
'--read-timeout',
63+
'90',
64+
'--write-timeout',
65+
'120',
66+
],
67+
)
68+
def test_parse_args_with_all_options(self):
69+
"""Test parsing with all options to verify no conflicts."""
70+
args = parse_args()
71+
72+
assert args.endpoint == 'https://test.example.com'
73+
assert args.service == 'lambda'
74+
assert args.profile == 'prod'
75+
assert args.region == 'eu-west-1'
76+
assert args.read_only is True
77+
assert args.log_level == 'WARNING'
78+
assert args.retries == 3
79+
assert args.timeout == 300.0
80+
assert args.connect_timeout == 30.0
81+
assert args.read_timeout == 90.0
82+
assert args.write_timeout == 120.0
83+
84+
@patch('sys.argv', ['mcp-proxy-for-aws'])
85+
def test_parse_args_missing_endpoint(self):
86+
"""Test parsing fails when endpoint is missing."""
87+
with pytest.raises(SystemExit):
88+
parse_args()
89+
90+
@patch.dict('os.environ', {'AWS_PROFILE': 'env-profile'})
91+
@patch('sys.argv', ['mcp-proxy-for-aws', 'https://test.example.com'])
92+
def test_parse_args_profile_from_env(self):
93+
"""Test that profile is read from AWS_PROFILE environment variable."""
94+
args = parse_args()
95+
96+
assert args.endpoint == 'https://test.example.com'
97+
assert args.profile == 'env-profile'
98+
99+
@patch.dict('os.environ', {'AWS_PROFILE': 'env-profile'})
100+
@patch(
101+
'sys.argv', ['mcp-proxy-for-aws', 'https://test.example.com', '--profile', 'cli-profile']
102+
)
103+
def test_parse_args_profile_cli_overrides_env(self):
104+
"""Test that CLI profile argument overrides environment variable."""
105+
args = parse_args()
106+
107+
assert args.endpoint == 'https://test.example.com'
108+
assert args.profile == 'cli-profile'
109+
110+
@patch('sys.argv', ['mcp-proxy-for-aws', 'https://test.example.com', '--retries', '0'])
111+
def test_parse_args_retries_minimum(self):
112+
"""Test parsing with minimum valid retries value (boundary)."""
113+
args = parse_args()
114+
115+
assert args.endpoint == 'https://test.example.com'
116+
assert args.retries == 0
117+
118+
@patch('sys.argv', ['mcp-proxy-for-aws', 'https://test.example.com', '--retries', '10'])
119+
def test_parse_args_retries_maximum(self):
120+
"""Test parsing with maximum valid retries value (boundary)."""
121+
args = parse_args()
122+
123+
assert args.endpoint == 'https://test.example.com'
124+
assert args.retries == 10
125+
126+
@patch('sys.argv', ['mcp-proxy-for-aws', 'https://test.example.com', '--retries', '11'])
127+
def test_parse_args_invalid_retries(self):
128+
"""Test parsing fails with retries value above maximum (boundary)."""
129+
with pytest.raises(SystemExit):
130+
parse_args()
131+
132+
@patch('sys.argv', ['mcp-proxy-for-aws', 'https://test.example.com', '--log-level', 'INVALID'])
133+
def test_parse_args_invalid_log_level(self):
134+
"""Test parsing fails with invalid log level (choices validation)."""
135+
with pytest.raises(SystemExit):
136+
parse_args()
137+
138+
@patch('sys.argv', ['mcp-proxy-for-aws', 'https://test.example.com', '--timeout', '-1'])
139+
def test_parse_args_negative_timeout(self):
140+
"""Test parsing fails with negative timeout (within_range validation)."""
141+
with pytest.raises(SystemExit):
142+
parse_args()

0 commit comments

Comments
 (0)