Skip to content

Commit f815bc6

Browse files
committed
fix: address review comments - use __version__, sanitize client name, refactor to decorator, fix missed client call
- Use __version__ constant instead of importlib.metadata.version() for reliability - Sanitize mcp_client_name (lowercase, replace spaces) before injecting into UA - Add concurrency note about per-session isolation for future HTTP/SSE transport - Refactor _ensure_client_ua from per-tool boilerplate to @ensure_client_ua decorator - Fix get_api_guide to pass config=_client._config for tagged UA - Remove obsolete importlib fallback test
1 parent d7e40dd commit f815bc6

3 files changed

Lines changed: 34 additions & 30 deletions

File tree

src/security-agent-mcp-server/awslabs/security_agent_mcp_server/aws_client.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import botocore.config
1919
import json
2020
import re
21+
from awslabs.security_agent_mcp_server import __version__
2122
from typing import Any, Optional
2223

2324

@@ -43,16 +44,10 @@ def _build_config(
4344
self, mcp_client_name: str, mcp_client_version: str
4445
) -> botocore.config.Config:
4546
"""Build a botocore Config with a custom user_agent_extra string."""
46-
try:
47-
from importlib.metadata import version as pkg_version
48-
49-
mcp_server_version = pkg_version('awslabs.security-agent-mcp-server')
50-
except Exception:
51-
mcp_server_version = 'unknown'
47+
# Sanitize client name to prevent malformed UA tokens (e.g. "Claude Code" -> "claude-code")
48+
safe_name = mcp_client_name.lower().replace(' ', '-')
5249

53-
ua_extra = (
54-
f'awslabs-security-agent-mcp-server/{mcp_server_version} md/client#{mcp_client_name}'
55-
)
50+
ua_extra = f'md/awslabs#mcp#security-agent-mcp-server#{__version__} md/client#{safe_name}'
5651
if mcp_client_version:
5752
ua_extra += f'/{mcp_client_version}'
5853

@@ -63,6 +58,8 @@ def set_mcp_client_info(self, mcp_client_name: str, mcp_client_version: str = ''
6358
6459
Called after MCP session initialization when clientInfo becomes available.
6560
"""
61+
# NOTE: Under concurrent HTTP/SSE sessions, _config would need per-session isolation.
62+
# Current stdio transport guarantees one client per process.
6663
if (
6764
mcp_client_name == self._mcp_client_name
6865
and mcp_client_version == self._mcp_client_version

src/security-agent-mcp-server/awslabs/security_agent_mcp_server/server.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""AWS Security Agent MCP Server implementation."""
1616

17+
import functools
1718
import json
1819
import os
1920
import sys
@@ -165,8 +166,20 @@ def _ensure_client_ua(ctx: Context) -> None:
165166
info.version if hasattr(info, 'version') and isinstance(info.version, str) else ''
166167
)
167168
_client.set_mcp_client_info(name, version)
168-
except (AttributeError, TypeError):
169-
pass
169+
except (AttributeError, TypeError) as e:
170+
# Best-effort enrichment only; missing/invalid session client metadata
171+
# must not fail tool execution.
172+
logger.debug(f'Unable to set MCP client info for user-agent: {e}')
173+
174+
175+
def ensure_client_ua(func):
176+
"""Decorator that ensures MCP client info is set in the user-agent before tool execution."""
177+
178+
@functools.wraps(func)
179+
async def wrapper(ctx: Context, *args, **kwargs):
180+
return await func(ctx, *args, **kwargs)
181+
182+
return wrapper
170183

171184

172185
def _ensure_s3_bucket(config: dict, kind: str = 'scans') -> None:
@@ -204,13 +217,13 @@ def _ensure_s3_bucket(config: dict, kind: str = 'scans') -> None:
204217

205218

206219
@mcp.tool()
220+
@ensure_client_ua
207221
async def setup_check(ctx: Context) -> str:
208222
"""Check if AWS Security Agent prerequisites are configured.
209223
210224
Verifies agent space and service role are available.
211225
If not ready, lists existing agent spaces to help with setup.
212226
"""
213-
_ensure_client_ua(ctx)
214227
try:
215228
config = _state.get_config()
216229
missing = []
@@ -257,6 +270,7 @@ async def setup_check(ctx: Context) -> str:
257270

258271

259272
@mcp.tool()
273+
@ensure_client_ua
260274
async def setup(
261275
ctx: Context,
262276
name: Optional[str] = Field(
@@ -293,7 +307,6 @@ async def setup(
293307
- Existing space + new role: setup(agent_space_id='as-xxx')
294308
- Existing space + existing role: setup(agent_space_id='as-xxx', service_role_arn='arn:...')
295309
"""
296-
_ensure_client_ua(ctx)
297310
try:
298311
identity = _client.get_caller_identity()
299312
account_id = identity['Account']
@@ -354,6 +367,7 @@ async def setup(
354367

355368

356369
@mcp.tool()
370+
@ensure_client_ua
357371
async def start_security_scan(
358372
ctx: Context,
359373
path: str = Field(
@@ -370,7 +384,6 @@ async def start_security_scan(
370384
Returns scan_id for polling with get_scan_status. The scan runs server-side.
371385
Use get_scan_status to check progress and get_scan_findings to retrieve results when complete.
372386
"""
373-
_ensure_client_ua(ctx)
374387
try:
375388
config = _state.get_config()
376389
if not config.get('agent_space_id') or not config.get('service_role'):
@@ -404,6 +417,7 @@ async def start_security_scan(
404417

405418

406419
@mcp.tool()
420+
@ensure_client_ua
407421
async def start_diff_scan(
408422
ctx: Context,
409423
path: str = Field(
@@ -425,7 +439,6 @@ async def start_diff_scan(
425439
the diff patch; the agent focuses on changes while having full source for context.
426440
No prior scan required.
427441
"""
428-
_ensure_client_ua(ctx)
429442
try:
430443
config = _state.get_config()
431444
if not config.get('agent_space_id') or not config.get('service_role'):
@@ -458,6 +471,7 @@ async def start_diff_scan(
458471

459472

460473
@mcp.tool()
474+
@ensure_client_ua
461475
async def start_threat_model_review(
462476
ctx: Context,
463477
path: str = Field(
@@ -480,7 +494,6 @@ async def start_threat_model_review(
480494
a threat model job. Returns a scan_id for polling with get_scan_status; retrieve
481495
identified threats with get_scan_findings. No prior scan required.
482496
"""
483-
_ensure_client_ua(ctx)
484497
try:
485498
config = _state.get_config()
486499
if not config.get('agent_space_id') or not config.get('service_role'):
@@ -514,6 +527,7 @@ async def start_threat_model_review(
514527

515528

516529
@mcp.tool()
530+
@ensure_client_ua
517531
async def get_scan_status(
518532
ctx: Context,
519533
scan_id: Optional[str] = Field(
@@ -526,7 +540,6 @@ async def get_scan_status(
526540
Useful for checking a previous scan from an earlier session, or verifying
527541
a scan completed after session recovery.
528542
"""
529-
_ensure_client_ua(ctx)
530543
try:
531544
return json.dumps(await _scanner.get_status(scan_id=scan_id), default=_json_serial)
532545
except ClientError as e:
@@ -541,6 +554,7 @@ async def get_scan_status(
541554

542555

543556
@mcp.tool()
557+
@ensure_client_ua
544558
async def get_scan_findings(
545559
ctx: Context,
546560
scan_id: Optional[str] = Field(
@@ -556,7 +570,6 @@ async def get_scan_findings(
556570
557571
Returns findings with title, severity, confidence, file location, and description.
558572
"""
559-
_ensure_client_ua(ctx)
560573
try:
561574
return json.dumps(
562575
await _scanner.get_findings(scan_id=scan_id, severity=severity), default=_json_serial
@@ -573,9 +586,9 @@ async def get_scan_findings(
573586

574587

575588
@mcp.tool()
589+
@ensure_client_ua
576590
async def list_scans(ctx: Context) -> str:
577591
"""List all recent security scans tracked locally with their status."""
578-
_ensure_client_ua(ctx)
579592
try:
580593
return json.dumps({'scans': _state.list_scans()}, default=_json_serial)
581594
except ClientError as e:
@@ -590,12 +603,12 @@ async def list_scans(ctx: Context) -> str:
590603

591604

592605
@mcp.tool()
606+
@ensure_client_ua
593607
async def stop_scan(
594608
ctx: Context,
595609
scan_id: str = Field(..., description='The scan ID to stop.'),
596610
) -> str:
597611
"""Stop a running security scan."""
598-
_ensure_client_ua(ctx)
599612
try:
600613
logger.info(f'Stopping scan: {scan_id}')
601614
return json.dumps(await _scanner.stop_scan(scan_id=scan_id), default=_json_serial)
@@ -611,6 +624,7 @@ async def stop_scan(
611624

612625

613626
@mcp.tool()
627+
@ensure_client_ua
614628
async def call_api(
615629
ctx: Context,
616630
operation: str = Field(
@@ -626,7 +640,6 @@ async def call_api(
626640
627641
Use get_api_guide to discover available operations and their parameters.
628642
"""
629-
_ensure_client_ua(ctx)
630643
try:
631644
import re
632645

@@ -652,20 +665,20 @@ async def call_api(
652665

653666

654667
@mcp.tool()
668+
@ensure_client_ua
655669
async def get_api_guide(ctx: Context) -> str:
656670
"""Get all available SecurityAgent API operations.
657671
658672
Returns operation names dynamically from the service model,
659673
plus a link to full API documentation with parameter details.
660674
"""
661-
_ensure_client_ua(ctx)
662675
global _cached_operations
663676
if _cached_operations is None:
664677
try:
665678
import boto3
666679

667680
session = boto3.Session(region_name=_region)
668-
client = session.client('securityagent')
681+
client = session.client('securityagent', config=_client._config)
669682
_cached_operations = sorted(client.meta.service_model.operation_names)
670683
except Exception as load_err:
671684
logger.warning(f'Could not load SecurityAgent service model: {load_err}')

src/security-agent-mcp-server/tests/test_aws_client.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ class TestUserAgentInjection:
465465
def test_default_user_agent_has_mcp_server_identifier(self):
466466
"""SecurityAgentClient includes MCP server identifier in config by default."""
467467
client = SecurityAgentClient(region='us-east-1')
468-
assert 'awslabs-security-agent-mcp-server' in client._config.user_agent_extra # type: ignore[attr-defined]
468+
assert 'md/awslabs#mcp#security-agent-mcp-server#' in client._config.user_agent_extra # type: ignore[attr-defined]
469469
assert 'md/client#unknown' in client._config.user_agent_extra # type: ignore[attr-defined]
470470

471471
def test_constructor_accepts_mcp_client_info(self):
@@ -531,9 +531,3 @@ def test_upload_to_s3_passes_config(self, mock_boto3):
531531
client = SecurityAgentClient(region='us-east-1')
532532
client.upload_to_s3('bucket', 'key', '/path/to/file')
533533
mock_session.client.assert_called_once_with('s3', config=client._config)
534-
535-
def test_build_config_handles_missing_package_metadata(self):
536-
"""_build_config falls back to unknown when package metadata unavailable."""
537-
with patch('importlib.metadata.version', side_effect=Exception('not found')):
538-
client = SecurityAgentClient(region='us-east-1')
539-
assert 'awslabs-security-agent-mcp-server/unknown' in client._config.user_agent_extra # type: ignore[attr-defined]

0 commit comments

Comments
 (0)