1414
1515"""AWS Security Agent MCP Server implementation."""
1616
17+ import functools
1718import json
1819import os
1920import 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
172185def _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
207221async 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
260274async 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
357371async 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
407421async 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
461475async 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
517531async 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
544558async 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
576590async 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
593607async 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
614628async 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
655669async 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 } ' )
0 commit comments