2727# Third-party libraries
2828from dotenv import find_dotenv , load_dotenv
2929from mcp .server .fastmcp import FastMCP , Context , Image
30+ from mcp .server .fastmcp .exceptions import ToolError
3031
3132PROJECT_ROOT = os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
3233from mcp .types import Annotations , ImageContent , TextContent , ToolAnnotations
@@ -741,6 +742,8 @@ def with_account(readonly=False):
741742 - In multi-mode with explicit account: uses that account's client.
742743 - In multi-mode without account + readonly: fans out to all accounts
743744 concurrently, prefixes each result with [label], concatenates.
745+ ToolError failures are labelled alongside successful results; if every
746+ account raises ToolError, the combined failure is raised instead.
744747 - In multi-mode without account + NOT readonly: returns an error.
745748
746749 The wrapped function must accept ``account: str = None`` and use
@@ -765,14 +768,24 @@ async def wrapper(*args, **kwargs):
765768 async def _call_for (label ):
766769 kw = dict (kwargs )
767770 kw ["account" ] = label
768- return label , await fn (* args , ** kw )
771+ try :
772+ result = await fn (* args , ** kw )
773+ failed = False
774+ except ToolError as error :
775+ result = str (error )
776+ failed = True
777+ return label , result , failed
769778
770779 results = await asyncio .gather (* (_call_for (label ) for label in clients ))
771- if all (isinstance (result , str ) for _ , result in results ):
772- return "\n \n " .join (f"[{ label } ]\n { result } " for label , result in results )
780+ if all (failed for _ , _ , failed in results ):
781+ raise ToolError (
782+ "\n \n " .join (f"[{ label } ]\n { result } " for label , result , _ in results )
783+ )
784+ if all (isinstance (result , str ) for _ , result , _ in results ):
785+ return "\n \n " .join (f"[{ label } ]\n { result } " for label , result , _ in results )
773786
774787 account_labelled_content = []
775- for label , result in results :
788+ for label , result , _ in results :
776789 account_labelled_content .append (f"[{ label } ]" )
777790 account_labelled_content .extend (result if isinstance (result , list ) else [result ])
778791 return account_labelled_content
@@ -1194,13 +1207,20 @@ def log_and_format_error(
11941207 return f"An error occurred (code: { error_code } )."
11951208
11961209
1197- def validate_id (* param_names_to_validate ):
1210+ def validate_id (* param_names_to_validate , raise_errors = False ):
11981211 """
11991212 Decorator to validate chat_id and user_id parameters, including lists of IDs.
12001213 It checks for valid integer ranges, string representations of integers,
12011214 and username formats.
1215+ Set raise_errors to surface validation/privacy failures as MCP tool errors;
1216+ other tools retain their existing return contract by default.
12021217 """
12031218
1219+ def error_result (message ):
1220+ if raise_errors :
1221+ raise ToolError (message )
1222+ return message
1223+
12041224 def decorator (func ):
12051225 @wraps (func )
12061226 async def wrapper (* args , ** kwargs ):
@@ -1256,24 +1276,28 @@ def validate_single_id(value, p_name):
12561276 for item in param_value :
12571277 validated_item , error_msg = validate_single_id (item , param_name )
12581278 if error_msg :
1259- return log_and_format_error (
1260- func .__name__ ,
1261- ValidationError (error_msg ),
1262- prefix = "VALIDATION-001" ,
1263- user_message = error_msg ,
1264- ** {param_name : param_value },
1279+ return error_result (
1280+ log_and_format_error (
1281+ func .__name__ ,
1282+ ValidationError (error_msg ),
1283+ prefix = "VALIDATION-001" ,
1284+ user_message = error_msg ,
1285+ ** {param_name : param_value },
1286+ )
12651287 )
12661288 validated_list .append (validated_item )
12671289 kwargs [param_name ] = validated_list
12681290 else :
12691291 validated_value , error_msg = validate_single_id (param_value , param_name )
12701292 if error_msg :
1271- return log_and_format_error (
1272- func .__name__ ,
1273- ValidationError (error_msg ),
1274- prefix = "VALIDATION-001" ,
1275- user_message = error_msg ,
1276- ** {param_name : param_value },
1293+ return error_result (
1294+ log_and_format_error (
1295+ func .__name__ ,
1296+ ValidationError (error_msg ),
1297+ prefix = "VALIDATION-001" ,
1298+ user_message = error_msg ,
1299+ ** {param_name : param_value },
1300+ )
12771301 )
12781302 kwargs [param_name ] = validated_value
12791303
@@ -1297,12 +1321,14 @@ def validate_single_id(value, p_name):
12971321 pass
12981322 if not resolved_allowed :
12991323 err = check_chat_access (item )
1300- return log_and_format_error (
1301- func .__name__ ,
1302- ChatAccessDeniedError (err ),
1303- prefix = ErrorCategory .PRIVACY ,
1304- user_message = err ,
1305- ** {param_name : param_value },
1324+ return error_result (
1325+ log_and_format_error (
1326+ func .__name__ ,
1327+ ChatAccessDeniedError (err ),
1328+ prefix = ErrorCategory .PRIVACY ,
1329+ user_message = err ,
1330+ ** {param_name : param_value },
1331+ )
13061332 )
13071333
13081334 return await func (* args , ** kwargs )
0 commit comments