@@ -264,6 +264,24 @@ def _to_snake(key: str) -> str:
264264
265265 return {_to_snake (k ): v for k , v in arguments .items ()}
266266
267+ @staticmethod
268+ def _extract_user_context (kwargs : Dict [str , Any ]) -> tuple :
269+ """Extract access-control parameters from handler kwargs.
270+
271+ ``handle_meta_tool_call`` passes ``user_email``, ``token_teams``,
272+ and ``request_headers`` through to every handler. This helper
273+ provides a single extraction point so handlers don't repeat the
274+ pattern.
275+
276+ Returns:
277+ Tuple of (user_email, token_teams, request_headers).
278+ """
279+ return (
280+ kwargs .get ("user_email" ),
281+ kwargs .get ("token_teams" ),
282+ kwargs .get ("request_headers" ),
283+ )
284+
267285 # ------------------------------------------------------------------
268286 # Implemented handlers
269287 # ------------------------------------------------------------------
@@ -297,6 +315,9 @@ async def _search_tools(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict[
297315 tags = arguments .get ("tags" , [])
298316 include_metrics = arguments .get ("include_metrics" , False )
299317
318+ # -- Extract user context for access control --
319+ user_email , token_teams , _ = self ._extract_user_context (kwargs )
320+
300321 # -- Step 1: Semantic search --
301322 semantic_results = []
302323 try :
@@ -309,39 +330,44 @@ async def _search_tools(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict[
309330 # -- Step 2: Keyword fallback search --
310331 keyword_results = []
311332 try :
333+ from mcpgateway .services .tool_service import ToolService as _KwToolService # pylint: disable=import-outside-toplevel
334+ _kw_ts = _KwToolService ()
335+
312336 db_gen = get_db ()
313337 db = next (db_gen )
314338 try :
315- search_pattern = f"%{ query } %"
316- keyword_tools = (
317- db .query (Tool )
318- .filter (
319- Tool .enabled .is_ (True ),
320- or_ (
321- Tool ._computed_name .ilike (search_pattern ),
322- Tool .description .ilike (search_pattern ),
323- ),
324- )
325- .limit (limit )
326- .all ()
339+ # Use ToolService.list_tools for consistent access control
340+ kw_result = await _kw_ts .list_tools (
341+ db = db ,
342+ include_inactive = False ,
343+ limit = limit ,
344+ user_email = user_email ,
345+ token_teams = token_teams ,
327346 )
347+ kw_tools_list , _ = kw_result if isinstance (kw_result , tuple ) else (kw_result , None )
328348
329349 query_lower = query .lower ()
330- for tool in keyword_tools :
350+ search_pattern = query_lower
351+ for tool in kw_tools_list :
352+ tool_name = getattr (tool , "name" , "" )
353+ tool_desc = getattr (tool , "description" , "" ) or ""
354+ # Filter to only matching tools
355+ if search_pattern not in tool_name .lower () and search_pattern not in tool_desc .lower ():
356+ continue
331357 # Score 1.0 for exact name match, 0.5 for partial match
332- if tool . _computed_name .lower () == query_lower :
358+ if tool_name .lower () == query_lower :
333359 score = 1.0
334- elif query_lower in tool . name .lower ():
360+ elif query_lower in tool_name .lower ():
335361 score = 0.7
336362 else :
337363 score = 0.5
338364
339365 keyword_results .append (
340366 ToolSearchResult (
341- tool_name = tool . name ,
342- description = tool . description ,
343- server_id = tool . gateway_id ,
344- server_name = tool . gateway . name if tool . gateway else None ,
367+ tool_name = tool_name ,
368+ description = tool_desc ,
369+ server_id = getattr ( tool , " gateway_id" , None ) ,
370+ server_name = None ,
345371 similarity_score = score ,
346372 )
347373 )
@@ -414,6 +440,9 @@ async def _get_similar_tools(self, arguments: Dict[str, Any], **kwargs: Any) ->
414440 tool_name = arguments .get ("tool_name" , "" )
415441 limit = arguments .get ("limit" , 10 )
416442
443+ # -- Extract user context for access control --
444+ user_email , token_teams , _ = self ._extract_user_context (kwargs )
445+
417446 if not tool_name :
418447 return GetSimilarToolsResponse (
419448 reference_tool = tool_name ,
@@ -501,6 +530,34 @@ async def _get_similar_tools(self, arguments: Dict[str, Any], **kwargs: Any) ->
501530 # -- Step 4: Filter out the reference tool itself --
502531 similar_results = [r for r in similar_results if r .tool_name != tool_name ][:limit ]
503532
533+ # -- Step 4.5: Apply access-control filtering --
534+ # Build set of tool names the user can access, then discard the rest.
535+ if user_email is not None or token_teams is not None :
536+ try :
537+ from mcpgateway .services .tool_service import ToolService as _AcToolService # pylint: disable=import-outside-toplevel
538+
539+ _ac_ts = _AcToolService ()
540+ db_gen = get_db ()
541+ db = next (db_gen )
542+ try :
543+ ac_result = await _ac_ts .list_tools (
544+ db = db ,
545+ include_inactive = False ,
546+ limit = 0 ,
547+ user_email = user_email ,
548+ token_teams = token_teams ,
549+ )
550+ ac_tools_list , _ = ac_result if isinstance (ac_result , tuple ) else (ac_result , None )
551+ accessible_names = {getattr (t , "name" , "" ) for t in ac_tools_list }
552+ similar_results = [r for r in similar_results if r .tool_name in accessible_names ]
553+ finally :
554+ try :
555+ next (db_gen )
556+ except StopIteration :
557+ pass
558+ except Exception as e :
559+ logger .warning (f"Access control filtering failed for similar tools: { e } " )
560+
504561 # -- Step 5: Apply scope filtering --
505562 filtered_results = self ._apply_scope_filtering (similar_results , arguments .get ("scope" ))
506563
@@ -738,6 +795,9 @@ async def _list_tools(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict[st
738795 sort_order = arguments .get ("sort_order" , "desc" )
739796 include_schema = arguments .get ("include_schema" , False )
740797
798+ # -- Extract user context for access control --
799+ user_email , token_teams , _ = self ._extract_user_context (kwargs )
800+
741801 # -- Step 1: Query tools from database using ToolService --
742802 # First-Party
743803 from mcpgateway .services .tool_service import ToolService
@@ -759,6 +819,8 @@ async def _list_tools(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict[st
759819 tags = tags if tags else None ,
760820 gateway_id = server_id ,
761821 limit = query_limit ,
822+ user_email = user_email ,
823+ token_teams = token_teams ,
762824 )
763825
764826 # Extract tools from result (could be tuple or dict)
@@ -838,6 +900,8 @@ async def _stub_describe_tool(self, arguments: Dict[str, Any], **kwargs: Any) ->
838900 # First-Party
839901 from mcpgateway .services .meta_tool_service import MetaToolService
840902
903+ user_email , token_teams , _ = self ._extract_user_context (kwargs )
904+
841905 try :
842906 db_gen = get_db ()
843907 db = next (db_gen )
@@ -847,6 +911,8 @@ async def _stub_describe_tool(self, arguments: Dict[str, Any], **kwargs: Any) ->
847911 tool_name = arguments .get ("tool_name" , "" ),
848912 include_metrics = arguments .get ("include_metrics" , False ),
849913 scope = arguments .get ("scope" ),
914+ user_email = user_email ,
915+ token_teams = token_teams ,
850916 )
851917 return result .model_dump (by_alias = True )
852918 finally :
@@ -1278,18 +1344,26 @@ async def _list_resources(self, arguments: Dict[str, Any], **kwargs: Any) -> Dic
12781344 ListResourcesResponse as dict.
12791345 """
12801346 from mcpgateway .db import Resource # pylint: disable=import-outside-toplevel
1347+ from mcpgateway .services .resource_service import ResourceService as _RsService # pylint: disable=import-outside-toplevel
12811348
12821349 limit = arguments .get ("limit" , 50 )
12831350 offset = arguments .get ("offset" , 0 )
12841351 tags = arguments .get ("tags" , [])
12851352 mime_type = arguments .get ("mime_type" )
12861353
1354+ # Extract user context for access control
1355+ user_email , token_teams , _ = self ._extract_user_context (kwargs )
1356+
12871357 try :
12881358 db_gen = get_db ()
12891359 db = next (db_gen )
12901360 try :
12911361 query = db .query (Resource ).filter (Resource .enabled .is_ (True ))
12921362
1363+ # Apply access control
1364+ _rs = _RsService ()
1365+ query = await _rs ._apply_access_control (query , db , user_email , token_teams )
1366+
12931367 if mime_type :
12941368 query = query .filter (Resource .mime_type == mime_type )
12951369
@@ -1347,6 +1421,7 @@ async def _read_resource(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict
13471421 """
13481422 from mcpgateway .db import Resource # pylint: disable=import-outside-toplevel
13491423 from mcpgateway .services .observability_service import ObservabilityService , current_trace_id # pylint: disable=import-outside-toplevel
1424+ from mcpgateway .services .resource_service import ResourceService as _RsService # pylint: disable=import-outside-toplevel
13501425
13511426 uri = arguments .get ("uri" , "" )
13521427 if not uri :
@@ -1356,6 +1431,9 @@ async def _read_resource(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict
13561431 text = "Error: uri is required" ,
13571432 ).model_dump (by_alias = True )
13581433
1434+ # Extract user context for access control
1435+ user_email , token_teams , _ = self ._extract_user_context (kwargs )
1436+
13591437 start_time = time .monotonic ()
13601438 success = False
13611439 error_message = None
@@ -1400,6 +1478,16 @@ async def _read_resource(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict
14001478 text = error_message ,
14011479 ).model_dump (by_alias = True )
14021480
1481+ # Check access control
1482+ _rs = _RsService ()
1483+ if not await _rs ._check_resource_access (db , resource , user_email , token_teams ):
1484+ error_message = f"Resource not found: { uri } "
1485+ return ReadResourceResponse (
1486+ uri = uri ,
1487+ name = "" ,
1488+ text = error_message ,
1489+ ).model_dump (by_alias = True )
1490+
14031491 text_content = resource .text_content
14041492 if text_content is None and resource .binary_content is not None :
14051493 text_content = "(binary content — not displayable as text)"
@@ -1457,16 +1545,25 @@ async def _list_prompts(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict[
14571545 ListPromptsResponse as dict.
14581546 """
14591547 from mcpgateway .db import Prompt # pylint: disable=import-outside-toplevel
1548+ from mcpgateway .services .prompt_service import PromptService as _PsService # pylint: disable=import-outside-toplevel
14601549
14611550 limit = arguments .get ("limit" , 50 )
14621551 offset = arguments .get ("offset" , 0 )
14631552 tags = arguments .get ("tags" , [])
14641553
1554+ # Extract user context for access control
1555+ user_email , token_teams , _ = self ._extract_user_context (kwargs )
1556+
14651557 try :
14661558 db_gen = get_db ()
14671559 db = next (db_gen )
14681560 try :
14691561 query = db .query (Prompt ).filter (Prompt .enabled .is_ (True ))
1562+
1563+ # Apply access control
1564+ _ps = _PsService ()
1565+ query = await _ps ._apply_access_control (query , db , user_email , token_teams )
1566+
14701567 all_prompts = query .order_by (Prompt .created_at .desc ()).all ()
14711568
14721569 # Apply tag filtering in Python (tags stored as JSON)
@@ -1519,6 +1616,7 @@ async def _get_prompt(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict[st
15191616 """
15201617 from mcpgateway .db import Prompt # pylint: disable=import-outside-toplevel
15211618 from mcpgateway .services .observability_service import ObservabilityService , current_trace_id # pylint: disable=import-outside-toplevel
1619+ from mcpgateway .services .prompt_service import PromptService as _PsService # pylint: disable=import-outside-toplevel
15221620
15231621 name = arguments .get ("name" , "" )
15241622 prompt_args = arguments .get ("arguments" , {})
@@ -1530,6 +1628,9 @@ async def _get_prompt(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict[st
15301628 description = "Error: name is required" ,
15311629 ).model_dump (by_alias = True )
15321630
1631+ # Extract user context for access control
1632+ user_email , token_teams , _ = self ._extract_user_context (kwargs )
1633+
15331634 start_time = time .monotonic ()
15341635 success = False
15351636 error_message = None
@@ -1575,6 +1676,16 @@ async def _get_prompt(self, arguments: Dict[str, Any], **kwargs: Any) -> Dict[st
15751676 description = error_message ,
15761677 ).model_dump (by_alias = True )
15771678
1679+ # Check access control
1680+ _ps = _PsService ()
1681+ if not await _ps ._check_prompt_access (db , prompt , user_email , token_teams ):
1682+ error_message = f"Prompt not found: { name } "
1683+ return GetPromptResponse (
1684+ name = name ,
1685+ template = "" ,
1686+ description = error_message ,
1687+ ).model_dump (by_alias = True )
1688+
15781689 rendered = None
15791690 if prompt_args :
15801691 try :
0 commit comments