@@ -1861,9 +1861,9 @@ async def handle_tools_list(params: Dict[str, Any], session: MCPSession) -> Dict
18611861 },
18621862 ]
18631863
1864- # Filter tools by session scopes: hide write tools from read-only tokens
1864+ # Filter tools by session scopes: hide write tools from read-only or unknown tokens
18651865 auth_token = getattr (session , "_auth_token" , None )
1866- if auth_token and not auth_token .has_scope ("wazuh:write" ):
1866+ if not auth_token or not auth_token .has_scope ("wazuh:write" ):
18671867 tools = [t for t in tools if t ["name" ] not in WRITE_SCOPE_TOOLS ]
18681868
18691869 # Pagination support per MCP spec
@@ -1881,9 +1881,15 @@ async def handle_tools_call(params: Dict[str, Any], session: MCPSession) -> Dict
18811881 # Validate tool name
18821882 validate_input (tool_name , max_length = 100 )
18831883
1884- # Scope enforcement: check if the token has the required scope for this tool
1884+ # Scope enforcement: check if the token has the required scope for this tool.
1885+ # If auth_token is missing (should not happen in normal flow), deny write tools by default.
18851886 auth_token = getattr (session , "_auth_token" , None )
18861887 required_scope = _get_tool_scope (tool_name )
1888+ if required_scope == "wazuh:write" and not auth_token :
1889+ raise ValueError (
1890+ f"Insufficient permissions: tool '{ tool_name } ' requires '{ required_scope } ' scope. "
1891+ f"Authentication token not found on session."
1892+ )
18871893 if auth_token and not auth_token .has_scope (required_scope ):
18881894 raise ValueError (
18891895 f"Insufficient permissions: tool '{ tool_name } ' requires '{ required_scope } ' scope. "
@@ -2060,6 +2066,7 @@ def _tool_error(text: str) -> dict:
20602066 result = await wazuh_client .get_vulnerabilities (agent_id = agent_id , severity = severity , limit = limit )
20612067 if compact :
20622068 result = _compact_vulns_result (result )
2069+ result = _add_truncation_warning (result , limit )
20632070 _success = True
20642071 return _tool_result (f"Vulnerabilities:\n { json .dumps (result , indent = 2 if not compact else None )} " )
20652072
@@ -2070,6 +2077,7 @@ def _tool_error(text: str) -> dict:
20702077 result = await wazuh_client .get_critical_vulnerabilities (limit )
20712078 if compact :
20722079 result = _compact_vulns_result (result )
2080+ result = _add_truncation_warning (result , limit )
20732081 _success = True
20742082 return _tool_result (f"Critical Vulnerabilities:\n { json .dumps (result , indent = 2 if not compact else None )} " )
20752083
@@ -2564,7 +2572,7 @@ async def mcp_endpoint(
25642572 status_code = 404 , detail = "Session not found. Please start a new session with InitializeRequest."
25652573 )
25662574 if existing_session .is_expired ():
2567- await sessions .delete (mcp_session_id )
2575+ await sessions .remove (mcp_session_id )
25682576 _initialized_sessions .pop (mcp_session_id , None )
25692577 raise HTTPException (
25702578 status_code = 404 , detail = "Session expired. Please start a new session with InitializeRequest."
@@ -2776,25 +2784,25 @@ async def mcp_sse_endpoint(
27762784 headers = {"Retry-After" : str (retry_after )} if retry_after else {}
27772785 raise HTTPException (status_code = 429 , detail = "Rate limit exceeded" , headers = headers )
27782786
2779- # Track active connections
2787+ # Session validation: if client provides session ID but session doesn't exist, return 404
2788+ # Done BEFORE incrementing ACTIVE_CONNECTIONS to avoid counter leak on early errors.
2789+ if mcp_session_id :
2790+ existing_session = await sessions .get (mcp_session_id )
2791+ if not existing_session :
2792+ raise HTTPException (status_code = 404 , detail = "Session not found" )
2793+ session = existing_session
2794+ session .update_activity ()
2795+ await sessions .set (mcp_session_id , session )
2796+ else :
2797+ session = await get_or_create_session (None , origin )
2798+ session .authenticated = True # Mark as authenticated via bearer token
2799+ session ._auth_token = auth_token # Store token for scope checks in tool handlers
2800+
2801+ # Track active connections — only after validation passes.
2802+ # The SSE generator will decrement when the stream closes (track_connection=True).
27802803 ACTIVE_CONNECTIONS .inc ()
27812804
27822805 try :
2783- # Session validation: if client provides session ID but session doesn't exist, return 404
2784- if mcp_session_id :
2785- existing_session = await sessions .get (mcp_session_id )
2786- if not existing_session :
2787- raise HTTPException (status_code = 404 , detail = "Session not found" )
2788- session = existing_session
2789- session .update_activity ()
2790- await sessions .set (mcp_session_id , session )
2791- else :
2792- session = await get_or_create_session (None , origin )
2793- session .authenticated = True # Mark as authenticated via bearer token
2794- session ._auth_token = auth_token # Store token for scope checks in tool handlers
2795-
2796- # Return SSE stream (track_connection=True so ACTIVE_CONNECTIONS is
2797- # decremented when the stream actually closes, not when this function returns)
27982806 response = StreamingResponse (
27992807 generate_sse_events (session , track_connection = True ),
28002808 media_type = "text/event-stream" ,
@@ -2868,7 +2876,7 @@ async def mcp_streamable_http_endpoint(
28682876 status_code = 404 , detail = "Session not found. Please start a new session with InitializeRequest."
28692877 )
28702878 if existing_session .is_expired ():
2871- await sessions .delete (mcp_session_id )
2879+ await sessions .remove (mcp_session_id )
28722880 _initialized_sessions .pop (mcp_session_id , None )
28732881 raise HTTPException (
28742882 status_code = 404 , detail = "Session expired. Please start a new session with InitializeRequest."
0 commit comments