From d002d319f7161968fff61deebac0974c72a8e1cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n=20Soler?= Date: Thu, 21 Aug 2025 14:37:57 +0200 Subject: [PATCH 01/11] refactor: AppConfig via dependency injection --- main.py | 12 +- tools/cli_scanner/tool.py | 61 +++--- tools/events_feed/tool.py | 62 +++--- tools/inventory/tool.py | 24 +-- tools/sysdig_sage/tool.py | 33 ++- tools/vulnerability_management/tool.py | 45 +++-- utils/app_config.py | 41 +++- utils/mcp_server.py | 270 ++++++++++++------------- utils/middleware/auth.py | 42 +++- utils/query_helpers.py | 101 +++++++-- utils/sysdig/client_config.py | 6 +- uv.lock | 150 +++++++++++++- 12 files changed, 550 insertions(+), 297 deletions(-) diff --git a/main.py b/main.py index 1abc108..ff19bbb 100644 --- a/main.py +++ b/main.py @@ -14,17 +14,19 @@ # Register all tools so they attach to the MCP server from utils.mcp_server import run_stdio, run_http +# Load environment variables from .env +load_dotenv() + +app_config = get_app_config() + # Set up logging logging.basicConfig( format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", - level=os.environ.get("LOGLEVEL", "ERROR"), + level=app_config.log_level(), ) log = logging.getLogger(__name__) -# Load environment variables from .env -load_dotenv() -app_config = get_app_config() def handle_signals(): @@ -40,7 +42,7 @@ def signal_handler(sig, frame): def main(): # Choose transport: "stdio" or "sse" (HTTP/SSE) handle_signals() - transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() + transport = os.environ.get("MCP_TRANSPORT", app_config.transport()) log.info(""" ▄▖ ▌▘ ▖ ▖▄▖▄▖ ▄▖ ▚ ▌▌▛▘▛▌▌▛▌ ▛▖▞▌▌ ▙▌ ▚ █▌▛▘▌▌█▌▛▘ diff --git a/tools/cli_scanner/tool.py b/tools/cli_scanner/tool.py index 25ec306..1d83dfe 100644 --- a/tools/cli_scanner/tool.py +++ b/tools/cli_scanner/tool.py @@ -9,14 +9,9 @@ import subprocess from typing import Literal, Optional -from utils.app_config import get_app_config +from utils.app_config import AppConfig -logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR")) -log = logging.getLogger(__name__) - -# Load app config (expects keys: mcp.host, mcp.port, mcp.transport) -app_config = get_app_config() class CLIScannerTool: @@ -24,23 +19,27 @@ class CLIScannerTool: A class to encapsulate the tools for interacting with the Sysdig CLI Scanner. """ - cmd: str = "sysdig-cli-scanner" - default_args: list = [ - "--loglevel=err", - "--apiurl=" + app_config["sysdig"]["host"], - ] - iac_default_args: list = [ - "--iac", - "--group-by=violation", - "--recursive", - ] - - exit_code_explained: str = """ - 0: Scan evaluation "pass" - 1: Scan evaluation "fail" - 2: Invalid parameters - 3: Internal error - """ + def __init__(self, app_config: AppConfig): + self.app_config = app_config + logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=app_config.log_level()) + self.log = logging.getLogger(__name__) + self.cmd: str = "sysdig-cli-scanner" + self.default_args: list = [ + "--loglevel=err", + "--apiurl=" + app_config.sysdig_endpoint(), + ] + self.iac_default_args: list = [ + "--iac", + "--group-by=violation", + "--recursive", + ] + + self.exit_code_explained: str = """ + 0: Scan evaluation "pass" + 1: Scan evaluation "fail" + 2: Invalid parameters + 3: Internal error + """ def check_sysdig_cli_installed(self) -> None: """ @@ -49,7 +48,7 @@ def check_sysdig_cli_installed(self) -> None: try: # Attempt to run 'sysdig-cli-scanner --version' to check if it's installed result = subprocess.run([self.cmd, "--version"], capture_output=True, text=True, check=True) - log.info(f"Sysdig CLI Scanner is installed: {result.stdout.strip()}") + self.log.info(f"Sysdig CLI Scanner is installed: {result.stdout.strip()}") except subprocess.CalledProcessError as e: error: dict = { "error": "Sysdig CLI Scanner is not installed or not in the $PATH. Check the docs to install it here: https://docs.sysdig.com/en/sysdig-secure/install-vulnerability-cli-scanner/#deployment" @@ -64,14 +63,14 @@ def check_env_credentials(self) -> None: EnvironmentError: If the SYSDIG_SECURE_TOKEN or SYSDIG_HOST environment variables are not set. """ sysdig_secure_token = os.environ.get("SYSDIG_SECURE_TOKEN") - sysdig_host = os.environ.get("SYSDIG_HOST", app_config["sysdig"]["host"]) + sysdig_host = os.environ.get("SYSDIG_HOST", self.app_config.sysdig_endpoint()) if not sysdig_secure_token: - log.error("SYSDIG_SECURE_TOKEN environment variable is not set.") + self.log.error("SYSDIG_SECURE_TOKEN environment variable is not set.") raise EnvironmentError("SYSDIG_SECURE_TOKEN environment variable is not set.") else: os.environ["SECURE_API_TOKEN"] = sysdig_secure_token # Ensure the token is set in the environment if not sysdig_host: - log.error("SYSDIG_HOST environment variable is not set.") + self.log.error("SYSDIG_HOST environment variable is not set.") raise EnvironmentError("SYSDIG_HOST environment variable is not set.") def run_sysdig_cli_scanner( @@ -124,7 +123,7 @@ def run_sysdig_cli_scanner( # Prepare the command based on the mode if mode == "iac": - log.info("Running Sysdig CLI Scanner in IaC mode.") + self.log.info("Running Sysdig CLI Scanner in IaC mode.") extra_iac_args = [ f"--group-by={iac_group_by}", f"--severity-threshold={iac_severity_threshold}", @@ -135,7 +134,7 @@ def run_sysdig_cli_scanner( extra_iac_args = [arg for arg in extra_iac_args if arg] cmd = [self.cmd] + self.default_args + self.iac_default_args + extra_iac_args + [path_to_scan] else: - log.info("Running Sysdig CLI Scanner in vulnerability mode.") + self.log.info("Running Sysdig CLI Scanner in vulnerability mode.") # Default to vulnerability mode extra_args = [ "--standalone" if standalone else "", @@ -161,9 +160,9 @@ def run_sysdig_cli_scanner( } # Handle non-zero exit codes speically exit code 1 except subprocess.CalledProcessError as e: - log.warning(f"Sysdig CLI Scanner returned non-zero exit code: {e.returncode}") + self.log.warning(f"Sysdig CLI Scanner returned non-zero exit code: {e.returncode}") if e.returncode in [2, 3]: - log.error(f"Sysdig CLI Scanner encountered an error: {e.stderr.strip()}") + self.log.error(f"Sysdig CLI Scanner encountered an error: {e.stderr.strip()}") result: dict = { "error": "Error running Sysdig CLI Scanner", "exit_code": e.returncode, diff --git a/tools/events_feed/tool.py b/tools/events_feed/tool.py index 43a6e1d..22daa6d 100644 --- a/tools/events_feed/tool.py +++ b/tools/events_feed/tool.py @@ -20,15 +20,9 @@ from fastmcp.server.dependencies import get_http_request from utils.query_helpers import create_standard_response from utils.sysdig.client_config import get_configuration -from utils.app_config import get_app_config +from utils.app_config import AppConfig from utils.sysdig.api import initialize_api_client -logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR")) -log = logging.getLogger(__name__) - -# Load app config (expects keys: mcp.host, mcp.port, mcp.transport) -app_config = get_app_config() - class EventsFeedTools: """ @@ -36,6 +30,11 @@ class EventsFeedTools: This class provides methods to retrieve event information and list runtime events. """ + def __init__(self, app_config: AppConfig): + self.app_config = app_config + logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) + self.log = logging.getLogger(__name__) + def init_client(self, old_api: bool = False) -> SecureEventsApi | OldSysdigApi: """ Initializes the SecureEventsApi client from the request state. @@ -48,16 +47,16 @@ def init_client(self, old_api: bool = False) -> SecureEventsApi | OldSysdigApi: """ secure_events_api: SecureEventsApi = None old_sysdig_api: OldSysdigApi = None - transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() + transport = self.app_config.transport() if transport in ["streamable-http", "sse"]: # Try to get the HTTP request - log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") + self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") request: Request = get_http_request() secure_events_api = request.state.api_instances["secure_events"] old_sysdig_api = request.state.api_instances["old_sysdig_api"] else: # If running in STDIO mode, we need to initialize the API client from environment variables - log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") + self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") cfg = get_configuration() api_client = initialize_api_client(cfg) secure_events_api = SecureEventsApi(api_client) @@ -167,7 +166,7 @@ def tool_list_runtime_events( to=to_ts, var_from=from_ts, filter=filter_expr, limit=limit, cursor=cursor ) duration_ms = (time.time() - start_time) * 1000 - log.debug(f"Execution time: {duration_ms:.2f} ms") + self.log.debug(f"Execution time: {duration_ms:.2f} ms") response = create_standard_response( results=api_response, @@ -175,7 +174,7 @@ def tool_list_runtime_events( ) return response except ToolError as e: - log.error(f"Exception when calling SecureEventsApi->get_events_v1: {e}\n") + self.log.error(f"Exception when calling SecureEventsApi->get_events_v1: {e}\n") raise e # A tool to retrieve all the process-tree information for a specific event.Add commentMore actions @@ -199,26 +198,39 @@ def tool_get_event_process_tree(self, event_id: str) -> dict: # Get process tree tree = old_api_client.request_process_tree_trees(event_id) - # Parse the response - branches = create_standard_response(results=branches, execution_time_ms=(time.time() - start_time) * 1000) - tree = create_standard_response(results=tree, execution_time_ms=(time.time() - start_time) * 1000) + # Parse the response (tolerates empty bodies) + branches_std = create_standard_response(results=branches, execution_time_ms=(time.time() - start_time) * 1000) + tree_std = create_standard_response(results=tree, execution_time_ms=(time.time() - start_time) * 1000) execution_time = (time.time() - start_time) * 1000 - response = ( - { - "branches": branches.get("results", []), - "tree": tree.get("results", []), - "metadata": { - "execution_time_ms": execution_time, - "timestamp": datetime.utcnow().isoformat() + "Z", - }, + response = { + "branches": branches_std.get("results", {}), + "tree": tree_std.get("results", {}), + "metadata": { + "execution_time_ms": execution_time, + "timestamp": datetime.utcnow().isoformat() + "Z", }, - ) + } return response + except ApiException as e: + if e.status == 404: + # Process tree not available for this event + return { + "branches": {}, + "tree": {}, + "metadata": { + "execution_time_ms": (time.time() - start_time) * 1000, + "timestamp": datetime.utcnow().isoformat() + "Z", + "note": "Process tree not available for this event" + }, + } + else: + self.log.error(f"Exception when calling process tree API: {e}") + raise ToolError(f"Failed to get process tree: {e}") except ToolError as e: - log.error(f"Exception when calling Sysdig Sage API to get process tree: {e}") + self.log.error(f"Exception when calling Sysdig Sage API to get process tree: {e}") raise e # Prompts diff --git a/tools/inventory/tool.py b/tools/inventory/tool.py index c057150..d96f1c5 100644 --- a/tools/inventory/tool.py +++ b/tools/inventory/tool.py @@ -3,33 +3,29 @@ """ import logging -import os import time from typing import Annotated from pydantic import Field from fastmcp.server.dependencies import get_http_request from fastmcp.exceptions import ToolError from starlette.requests import Request -from sysdig_client import ApiException from sysdig_client.api import InventoryApi from utils.sysdig.client_config import get_configuration -from utils.app_config import get_app_config +from utils.app_config import AppConfig from utils.sysdig.api import initialize_api_client from utils.query_helpers import create_standard_response -# Configure logging -logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR")) -log = logging.getLogger(__name__) - -# Load app config (expects keys: mcp.host, mcp.port, mcp.transport) -app_config = get_app_config() - class InventoryTools: """ A class to encapsulate the tools for interacting with the Sysdig Secure Inventory API. This class provides methods to list resources and retrieve a single resource by its hash. """ + def __init__(self, app_config: AppConfig): + self.app_config = app_config + # Configure logging + logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) + self.log = logging.getLogger(__name__) def init_client(self) -> InventoryApi: """ @@ -40,15 +36,15 @@ def init_client(self) -> InventoryApi: InventoryApi: An instance of the InventoryApi client. """ inventory_api: InventoryApi = None - transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() + transport = self.app_config.transport() if transport in ["streamable-http", "sse"]: # Try to get the HTTP request - log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") + self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") request: Request = get_http_request() inventory_api = request.state.api_instances["inventory"] else: # If running in STDIO mode, we need to initialize the API client from environment variables - log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") + self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") cfg = get_configuration() api_client = initialize_api_client(cfg) inventory_api = InventoryApi(api_client) @@ -206,5 +202,5 @@ def tool_get_resource( return response except ToolError as e: - log.error(f"Exception when calling InventoryApi->get_resource: {e}") + self.log.error(f"Exception when calling InventoryApi->get_resource: {e}") raise e diff --git a/tools/sysdig_sage/tool.py b/tools/sysdig_sage/tool.py index 186d8f0..5c567a5 100644 --- a/tools/sysdig_sage/tool.py +++ b/tools/sysdig_sage/tool.py @@ -13,15 +13,10 @@ from starlette.requests import Request from fastmcp.server.dependencies import get_http_request from utils.sysdig.client_config import get_configuration -from utils.app_config import get_app_config +from utils.app_config import AppConfig from utils.sysdig.api import initialize_api_client from utils.query_helpers import create_standard_response -logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR")) -log = logging.getLogger(__name__) - -app_config = get_app_config() - class SageTools: """ @@ -29,6 +24,10 @@ class SageTools: This class provides methods to generate SysQL queries based on natural language questions and execute them against the Sysdig API. """ + def __init__(self, app_config: AppConfig): + self.app_config = app_config + logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) + self.log = logging.getLogger(__name__) def init_client(self) -> OldSysdigApi: """ @@ -39,15 +38,15 @@ def init_client(self) -> OldSysdigApi: OldSysdigApi: An instance of the OldSysdigApi client. """ old_sysdig_api: OldSysdigApi = None - transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() + transport = self.app_config.transport() if transport in ["streamable-http", "sse"]: # Try to get the HTTP request - log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") + self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") request: Request = get_http_request() old_sysdig_api = request.state.api_instances["old_sysdig_api"] else: # If running in STDIO mode, we need to initialize the API client from environment variables - log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") + self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") cfg = get_configuration(old_api=True) api_client = initialize_api_client(cfg) old_sysdig_api = OldSysdigApi(api_client) @@ -80,24 +79,24 @@ async def tool_sage_to_sysql(self, question: str) -> dict: if sysql_response.status > 299: raise ToolError(f"Sysdig Sage returned an error: {sysql_response.status} - {sysql_response.data}") except ToolError as e: - log.error(f"Failed to generate SysQL query: {e}") + self.log.error(f"Failed to generate SysQL query: {e}") raise e json_resp = sysql_response.json() if sysql_response.data else {} - syslq_query: str = json_resp.get("text", "") - if not syslq_query: + sysql_query: str = json_resp.get("text", "") + if not sysql_query: return {"error": "Sysdig Sage did not return a query"} # 2) Execute generated SysQL query try: - log.debug(f"Executing SysQL query: {syslq_query}") - results = old_sysdig_api.execute_sysql_query(syslq_query) + self.log.debug(f"Executing SysQL query: {sysql_query}") + results = old_sysdig_api.execute_sysql_query(sysql_query) execution_time = (time.time() - start_time) * 1000 - log.debug(f"SysQL query executed in {execution_time} ms") + self.log.debug(f"SysQL query executed in {execution_time} ms") response = create_standard_response( - results=results, execution_time_ms=execution_time, metadata_kwargs={"question": question, "sysql": syslq_query} + results=results, execution_time_ms=execution_time, metadata_kwargs={"question": question, "sysql": sysql_query} ) return response except ToolError as e: - log.error(f"Failed to execute SysQL query: {e}") + self.log.error(f"Failed to execute SysQL query: {e}") raise e diff --git a/tools/vulnerability_management/tool.py b/tools/vulnerability_management/tool.py index c9f93dd..23ad212 100644 --- a/tools/vulnerability_management/tool.py +++ b/tools/vulnerability_management/tool.py @@ -15,15 +15,10 @@ from sysdig_client.api import VulnerabilityManagementApi from fastmcp.server.dependencies import get_http_request from utils.sysdig.client_config import get_configuration -from utils.app_config import get_app_config +from utils.app_config import AppConfig from utils.sysdig.api import initialize_api_client from utils.query_helpers import create_standard_response -# Configure logging -logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR")) -log = logging.getLogger(__name__) - -app_config = get_app_config() class VulnerabilityManagementTools: @@ -33,6 +28,12 @@ class VulnerabilityManagementTools: and vulnerability policies. """ + def __init__(self, app_config: AppConfig): + self.app_config = app_config + # Configure logging + logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) + self.log = logging.getLogger(__name__) + def init_client(self) -> VulnerabilityManagementApi: """ Initializes the VulnerabilityManagementApi client from the request state. @@ -42,15 +43,15 @@ def init_client(self) -> VulnerabilityManagementApi: VulnerabilityManagementApi: An instance of the VulnerabilityManagementApi client. """ vulnerability_management_api: VulnerabilityManagementApi = None - transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() + transport = self.app_config.transport() if transport in ["streamable-http", "sse"]: # Try to get the HTTP request - log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") + self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") request: Request = get_http_request() vulnerability_management_api = request.state.api_instances["vulnerability_management"] else: # If running in STDIO mode, we need to initialize the API client from environment variables - log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") + self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") cfg = get_configuration() api_client = initialize_api_client(cfg) vulnerability_management_api = VulnerabilityManagementApi(api_client) @@ -153,7 +154,7 @@ def tool_list_runtime_vulnerabilities( ) # Capture next page cursor if available duration_ms = (time.time() - start_time) * 1000 - log.debug(f"Execution time: {duration_ms:.2f} ms") + self.log.debug(f"Execution time: {duration_ms:.2f} ms") response = create_standard_response( results=api_response, @@ -161,7 +162,7 @@ def tool_list_runtime_vulnerabilities( ) return response except ToolError as e: - log.error(f"Exception when calling VulnerabilityManagementApi->scanner_api_service_list_runtime_results: {e}") + self.log.error(f"Exception when calling VulnerabilityManagementApi->scanner_api_service_list_runtime_results: {e}") raise e def tool_list_accepted_risks( @@ -193,7 +194,7 @@ def tool_list_accepted_risks( ) # Capture next page cursor if available duration_ms = (time.time() - start_time) * 1000 - log.debug(f"Execution time: {duration_ms:.2f} ms") + self.log.debug(f"Execution time: {duration_ms:.2f} ms") response = create_standard_response( results=api_response, @@ -201,7 +202,7 @@ def tool_list_accepted_risks( ) return response except ToolError as e: - log.error(f"Exception when calling VulnerabilityManagementApi->get_accepted_risks_v1: {e}") + self.log.error(f"Exception when calling VulnerabilityManagementApi->get_accepted_risks_v1: {e}") raise e def tool_get_accepted_risk(self, accepted_risk_id: str) -> dict: @@ -219,7 +220,7 @@ def tool_get_accepted_risk(self, accepted_risk_id: str) -> dict: response = vulnerability_api.get_accepted_risk_v1(accepted_risk_id) return response.model_dump_json() if hasattr(response, "dict") else response except ToolError as e: - log.error(f"Exception when calling VulnerabilityManagementApi->get_accepted_risk_v1: {e}") + self.log.error(f"Exception when calling VulnerabilityManagementApi->get_accepted_risk_v1: {e}") raise e def tool_list_registry_scan_results( @@ -274,7 +275,7 @@ def tool_list_registry_scan_results( filter=filter, limit=limit, cursor=cursor ) duration_ms = (time.time() - start_time) * 1000 - log.debug(f"Execution time: {duration_ms:.2f} ms") + self.log.debug(f"Execution time: {duration_ms:.2f} ms") response = create_standard_response( results=api_response, @@ -282,7 +283,7 @@ def tool_list_registry_scan_results( ) return response except ToolError as e: - log.error(f"Exception when calling VulnerabilityManagementApi->scanner_api_service_list_registry_results: {e}") + self.log.error(f"Exception when calling VulnerabilityManagementApi->scanner_api_service_list_registry_results: {e}") raise e def tool_get_vulnerability_policy( @@ -303,7 +304,7 @@ def tool_get_vulnerability_policy( response: GetPolicyResponse = vulnerability_api.secure_vulnerability_v1_policies_policy_id_get(policy_id) return response.model_dump_json() if hasattr(response, "dict") else response except ToolError as e: - log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_policy_id_get: {e}") + self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_policy_id_get: {e}") raise e def tool_list_vulnerability_policies( @@ -334,7 +335,7 @@ def tool_list_vulnerability_policies( # Capture next page cursor if available duration_ms = (time.time() - start_time) * 1000 - log.debug(f"Execution time: {duration_ms:.2f} ms") + self.log.debug(f"Execution time: {duration_ms:.2f} ms") response = create_standard_response( results=api_response, @@ -342,7 +343,7 @@ def tool_list_vulnerability_policies( ) return response except ToolError as e: - log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}") + self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}") raise e def tool_list_pipeline_scan_results( @@ -400,7 +401,7 @@ def tool_list_pipeline_scan_results( cursor=cursor, filter=filter, limit=limit ) duration_ms = (time.time() - start_time) * 1000 - log.debug(f"Execution time: {duration_ms:.2f} ms") + self.log.debug(f"Execution time: {duration_ms:.2f} ms") response = create_standard_response( results=api_response, @@ -408,7 +409,7 @@ def tool_list_pipeline_scan_results( ) return response except ToolError as e: - log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}") + self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}") raise e def tool_get_scan_result(self, scan_id: str) -> dict: @@ -426,7 +427,7 @@ def tool_get_scan_result(self, scan_id: str) -> dict: resp: ScanResultResponse = vulnerability_api.secure_vulnerability_v1_results_result_id_get(scan_id) return resp.model_dump_json() if hasattr(resp, "dict") else resp except ToolError as e: - log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_results_result_id_get: {e}") + self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_results_result_id_get: {e}") raise e def explore_vulnerabilities_prompt(self, filters: str) -> PromptMessage: diff --git a/utils/app_config.py b/utils/app_config.py index 2550598..24796ae 100644 --- a/utils/app_config.py +++ b/utils/app_config.py @@ -17,6 +17,40 @@ APP_CONFIG_FILE: str = os.getenv("APP_CONFIG_FILE", "./app_config.yaml") +class AppConfig: + """ + A class to encapsulate the application configuration. + """ + + def __init__(self, config: dict): + self.app_config = config + + def sysdig_endpoint(self) -> str: + """ + Get the Sysdig endpoint from the app config + """ + return self.app_config["sysdig"]["host"] + + def transport(self) -> str: + """ + Get the transport protocol (lower case) from the app config + """ + return os.environ.get("MCP_TRANSPORT", self.app_config["mcp"]["transport"]).lower() + + @staticmethod + def log_level() -> str: + """ + Get the log level from the app config + """ + return os.environ.get("LOGLEVEL", "ERROR") + + def port(self) -> int: + """ + Get the port from the app config + """ + return self.app_config["mcp"]["port"] + + def env_constructor(loader, node): return os.environ[node.value[0:]] @@ -36,7 +70,7 @@ def check_config_file_exists() -> bool: return False -def load_app_config() -> dict: +def load_app_config() -> AppConfig: """ Load the app config from the YAML file @@ -55,10 +89,11 @@ def load_app_config() -> dict: app_config: dict = yaml.safe_load(file) except Exception as exc: logging.error(exc) - return app_config + + return AppConfig(app_config) -def get_app_config() -> dict: +def get_app_config() -> AppConfig: """ Get the the overall app config This function uses a singleton pattern to ensure the config is loaded only once. diff --git a/utils/mcp_server.py b/utils/mcp_server.py index de0ee84..5c1b01b 100644 --- a/utils/mcp_server.py +++ b/utils/mcp_server.py @@ -14,7 +14,7 @@ from fastapi import FastAPI from fastmcp import FastMCP from fastmcp.resources import HttpResource, TextResource -from utils.middleware.auth import CustomAuthMiddleware +from utils.middleware.auth import create_auth_middleware from starlette.middleware import Middleware from tools.events_feed.tool import EventsFeedTools from tools.inventory.tool import InventoryTools @@ -23,21 +23,21 @@ from tools.cli_scanner.tool import CLIScannerTool # Application config loader -from utils.app_config import get_app_config +from utils.app_config import get_app_config, AppConfig + +# Load app config (expects keys: mcp.host, mcp.port, mcp.transport) +app_config = get_app_config() # Set up logging logging.basicConfig( format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", - level=os.environ.get("LOGLEVEL", "ERROR"), + level=app_config.log_level(), ) log = logging.getLogger(__name__) -# Load app config (expects keys: mcp.host, mcp.port, mcp.transport) -app_config = get_app_config() - _mcp_instance: Optional[FastMCP] = None -middlewares = [Middleware(CustomAuthMiddleware)] +middlewares = [Middleware(create_auth_middleware(app_config))] MCP_MOUNT_PATH = "/sysdig-mcp-server" @@ -52,9 +52,9 @@ def create_simple_mcp_server() -> FastMCP: return FastMCP( name="Sysdig MCP Server", instructions="Provides Sysdig Secure tools and resources.", - host=app_config["mcp"]["host"], - port=app_config["mcp"]["port"], - tags=["sysdig", "mcp", os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower()], + host=app_config.sysdig_endpoint(), + port=app_config.port(), + tags=["sysdig", "mcp", app_config.transport()], ) @@ -77,7 +77,7 @@ def run_stdio(): """ mcp = get_mcp() # Add tools to the MCP server - add_tools(mcp=mcp, allowed_tools=app_config["mcp"]["allowed_tools"], transport_type=app_config["mcp"]["transport"]) + add_tools(mcp=mcp) # Add resources to the MCP server add_resources(mcp) try: @@ -94,11 +94,13 @@ def run_http(): """Run the MCP server over HTTP/SSE transport via Uvicorn.""" mcp = get_mcp() # Add tools to the MCP server - add_tools(mcp=mcp, allowed_tools=app_config["mcp"]["allowed_tools"], transport_type=app_config["mcp"]["transport"]) + transport = app_config.transport() + + add_tools(mcp=mcp) # Add resources to the MCP server add_resources(mcp) + # Mount the MCP HTTP/SSE app at 'MCP_MOUNT_PATH' - transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() mcp_app = mcp.http_app(transport=transport, middleware=middlewares) suffix_path = mcp.settings.streamable_http_path if transport == "streamable-http" else mcp.settings.sse_path app = FastAPI(lifespan=mcp_app.lifespan) @@ -117,15 +119,15 @@ async def health_check(request: Request) -> Response: return JSONResponse({"status": "ok"}) log.info( - f"Starting {mcp.name} at http://{app_config['app']['host']}:{app_config['app']['port']}{MCP_MOUNT_PATH}{suffix_path}" + f"Starting {mcp.name} at http://{app_config.sysdig_endpoint()}:{app_config['app']['port']}{MCP_MOUNT_PATH}{suffix_path}" ) # Use Uvicorn's Config and Server classes for more control config = uvicorn.Config( app, - host=app_config["app"]["host"], - port=app_config["app"]["port"], + host=app_config.sysdig_endpoint(), + port=app_config.port(), timeout_graceful_shutdown=1, - log_level=os.environ.get("LOGLEVEL", app_config["app"]["log_level"]).lower(), + log_level=app_config.log_level().lower(), ) server = uvicorn.Server(config) @@ -141,7 +143,7 @@ async def health_check(request: Request) -> Response: os._exit(1) -def add_tools(mcp: FastMCP, allowed_tools: list, transport_type: Literal["stdio", "streamable-http"] = "stdio") -> None: +def add_tools(mcp: FastMCP) -> None: """ Add tools to the MCP server based on the allowed tools and transport type. Args: @@ -149,133 +151,127 @@ def add_tools(mcp: FastMCP, allowed_tools: list, transport_type: Literal["stdio" allowed_tools (list): List of tools to register. transport_type (Literal["stdio", "streamable-http"]): The transport type for the MCP server. """ + # Register the events feed tools + events_feed_tools = EventsFeedTools(app_config) + log.info("Adding Events Feed Tools...") + mcp.add_tool( + events_feed_tools.tool_get_event_info, + name="get_event_info", + description="Retrieve detailed information for a specific security event by its ID", + ) + mcp.add_tool( + events_feed_tools.tool_list_runtime_events, + name="list_runtime_events", + description="List runtime security events from the last given hours, optionally filtered by severity level.", + ) - if "events-feed" in allowed_tools: - # Register the events feed tools - events_feed_tools = EventsFeedTools() - log.info("Adding Events Feed Tools...") - mcp.add_tool( - events_feed_tools.tool_get_event_info, - name="get_event_info", - description="Retrieve detailed information for a specific security event by its ID", - ) - mcp.add_tool( - events_feed_tools.tool_list_runtime_events, - name="list_runtime_events", - description="List runtime security events from the last given hours, optionally filtered by severity level.", - ) - - mcp.add_prompt( - events_feed_tools.investigate_event_prompt, - name="investigate_event", - description="Prompt to investigate a security event based on its severity and time range.", - tags={"analysis", "secure_feeds"}, - ) - mcp.add_tool( - events_feed_tools.tool_get_event_process_tree, - name="get_event_process_tree", - description=( - """ - Retrieve the process tree for a specific security event by its ID. Not every event has a process tree, - so this may return an empty tree. + mcp.add_prompt( + events_feed_tools.investigate_event_prompt, + name="investigate_event", + description="Prompt to investigate a security event based on its severity and time range.", + tags={"analysis", "secure_feeds"}, + ) + mcp.add_tool( + events_feed_tools.tool_get_event_process_tree, + name="get_event_process_tree", + description=( """ - ), - ) + Retrieve the process tree for a specific security event by its ID. Not every event has a process tree, + so this may return an empty tree. + """ + ), + ) # Register the Sysdig Inventory tools - if "inventory" in allowed_tools: - # Register the Sysdig Inventory tools - log.info("Adding Sysdig Inventory Tools...") - inventory_tools = InventoryTools() - mcp.add_tool( - inventory_tools.tool_list_resources, - name="list_resources", - description=( - """ - List inventory resources based on Sysdig Filter Query Language expression with optional pagination.' - """ - ), - ) - mcp.add_tool( - inventory_tools.tool_get_resource, - name="get_resource", - description="Retrieve a single inventory resource by its unique hash identifier.", - ) + log.info("Adding Sysdig Inventory Tools...") + inventory_tools = InventoryTools(app_config) + mcp.add_tool( + inventory_tools.tool_list_resources, + name="list_resources", + description=( + """ + List inventory resources based on Sysdig Filter Query Language expression with optional pagination.' + """ + ), + ) + mcp.add_tool( + inventory_tools.tool_get_resource, + name="get_resource", + description="Retrieve a single inventory resource by its unique hash identifier.", + ) - if "vulnerability-management" in allowed_tools: - # Register the Sysdig Vulnerability Management tools - log.info("Adding Sysdig Vulnerability Management Tools...") - vulnerability_tools = VulnerabilityManagementTools() - mcp.add_tool( - vulnerability_tools.tool_list_runtime_vulnerabilities, - name="list_runtime_vulnerabilities", - description=( - """ - List runtime vulnerability assets scan results from Sysdig Vulnerability Management API - (Supports pagination using cursor). - """ - ), - ) - mcp.add_tool( - vulnerability_tools.tool_list_accepted_risks, - name="list_accepted_risks", - description="List all accepted risks. Supports filtering and pagination.", - ) - mcp.add_tool( - vulnerability_tools.tool_get_accepted_risk, - name="get_accepted_risk", - description="Retrieve a specific accepted risk by its ID.", - ) - mcp.add_tool( - vulnerability_tools.tool_list_registry_scan_results, - name="list_registry_scan_results", - description="List registry scan results. Supports filtering and pagination.", - ) - mcp.add_tool( - vulnerability_tools.tool_get_vulnerability_policy, - name="get_vulnerability_policy_by_id", - description="Retrieve a specific vulnerability policy by its ID.", - ) - mcp.add_tool( - vulnerability_tools.tool_list_vulnerability_policies, - name="list_vulnerability_policies", - description="List all vulnerability policies. Supports filtering, pagination, and sorting.", - ) - mcp.add_tool( - vulnerability_tools.tool_list_pipeline_scan_results, - name="list_pipeline_scan_results", - description="List pipeline scan results (e.g., built images). Supports pagination and filtering.", - ) - mcp.add_tool( - vulnerability_tools.tool_get_scan_result, - name="get_scan_result", - description="Retrieve a specific scan result (registry/runtime/pipeline).", - ) - mcp.add_prompt( - vulnerability_tools.explore_vulnerabilities_prompt, - name="explore_vulnerabilities", - description="Prompt to explore vulnerabilities based on filters", - tags={"vulnerability", "exploration"}, - ) + # Register the Sysdig Vulnerability Management tools + log.info("Adding Sysdig Vulnerability Management Tools...") + vulnerability_tools = VulnerabilityManagementTools(app_config) + mcp.add_tool( + vulnerability_tools.tool_list_runtime_vulnerabilities, + name="list_runtime_vulnerabilities", + description=( + """ + List runtime vulnerability assets scan results from Sysdig Vulnerability Management API + (Supports pagination using cursor). + """ + ), + ) + mcp.add_tool( + vulnerability_tools.tool_list_accepted_risks, + name="list_accepted_risks", + description="List all accepted risks. Supports filtering and pagination.", + ) + mcp.add_tool( + vulnerability_tools.tool_get_accepted_risk, + name="get_accepted_risk", + description="Retrieve a specific accepted risk by its ID.", + ) + mcp.add_tool( + vulnerability_tools.tool_list_registry_scan_results, + name="list_registry_scan_results", + description="List registry scan results. Supports filtering and pagination.", + ) + mcp.add_tool( + vulnerability_tools.tool_get_vulnerability_policy, + name="get_vulnerability_policy_by_id", + description="Retrieve a specific vulnerability policy by its ID.", + ) + mcp.add_tool( + vulnerability_tools.tool_list_vulnerability_policies, + name="list_vulnerability_policies", + description="List all vulnerability policies. Supports filtering, pagination, and sorting.", + ) + mcp.add_tool( + vulnerability_tools.tool_list_pipeline_scan_results, + name="list_pipeline_scan_results", + description="List pipeline scan results (e.g., built images). Supports pagination and filtering.", + ) + mcp.add_tool( + vulnerability_tools.tool_get_scan_result, + name="get_scan_result", + description="Retrieve a specific scan result (registry/runtime/pipeline).", + ) + mcp.add_prompt( + vulnerability_tools.explore_vulnerabilities_prompt, + name="explore_vulnerabilities", + description="Prompt to explore vulnerabilities based on filters", + tags={"vulnerability", "exploration"}, + ) - if "sysdig-sage" in allowed_tools: - # Register the Sysdig Sage tools - log.info("Adding Sysdig Sage Tools...") - sysdig_sage_tools = SageTools() - mcp.add_tool( - sysdig_sage_tools.tool_sage_to_sysql, - name="sysdig_sysql_sage_query", - description=( - """ - Query Sysdig Sage to generate a SysQL query based on a natural language question, - execute it against the Sysdig API, and return the results. - """ - ), - ) + # Register the Sysdig Sage tools + log.info("Adding Sysdig Sage Tools...") + sysdig_sage_tools = SageTools(app_config) + mcp.add_tool( + sysdig_sage_tools.tool_sage_to_sysql, + name="sysdig_sysql_sage_query", + description=( + """ + Query Sysdig Sage to generate a SysQL query based on a natural language question, + execute it against the Sysdig API, and return the results. + """ + ), + ) - if "sysdig-cli-scanner" in allowed_tools: + if app_config.transport() == "stdio": # Register the tools for STDIO transport - cli_scanner_tool = CLIScannerTool() + cli_scanner_tool = CLIScannerTool(app_config) log.info("Adding Sysdig CLI Scanner Tool...") mcp.add_tool( cli_scanner_tool.run_sysdig_cli_scanner, diff --git a/utils/middleware/auth.py b/utils/middleware/auth.py index 67ae8b6..0c34702 100644 --- a/utils/middleware/auth.py +++ b/utils/middleware/auth.py @@ -8,17 +8,12 @@ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request from starlette.responses import Response +from starlette.types import ASGIApp + from utils.sysdig.api import initialize_api_client, get_sysdig_api_instances from utils.sysdig.client_config import get_configuration from utils.sysdig.old_sysdig_api import OldSysdigApi -from utils.app_config import get_app_config - -# Set up logging -logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR")) -log = logging.getLogger(__name__) - -# Load app config (expects keys: mcp.host, mcp.port, mcp.transport) -app_config = get_app_config() +from utils.app_config import AppConfig class CustomAuthMiddleware(BaseHTTPMiddleware): @@ -26,6 +21,13 @@ class CustomAuthMiddleware(BaseHTTPMiddleware): Custom middleware for handling token-based authentication in the MCP server and initializing Sysdig API clients. """ + def __init__(self, app: ASGIApp, app_config: AppConfig): + super().__init__(app) + self.app_config = app_config + # Set up logging + logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) + self.log = logging.getLogger(__name__) + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: """ Dispatch method to handle incoming requests, validate the Authorization header, @@ -46,9 +48,9 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) - # Extract releavant information from the request headers token = auth_header.removeprefix("Bearer ").strip() session_id = request.headers.get("mcp-session-id", "") - base_url = request.headers.get("X-Sysdig-Host", app_config["sysdig"]["host"]) or str(request.base_url) - log.info(f"Received request with session ID: {session_id}") - log.info(f"Using Sysdig API base URL: {base_url}") + base_url = request.headers.get("X-Sysdig-Host", self.app_config.sysdig_endpoint()) or str(request.base_url) + self.log.info(f"Received request with session ID: {session_id}") + self.log.info(f"Using Sysdig API base URL: {base_url}") # Initialize the API client with the token and base URL cfg = get_configuration(token, base_url) @@ -66,3 +68,21 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) - return response except Exception as e: return Response(f"Internal server error: {str(e)}", status_code=500) + + +def create_auth_middleware(app_config: AppConfig): + """ + Factory function to create the CustomAuthMiddleware with injected app_config. + + Args: + app_config (AppConfig): The application configuration object + + Returns: + A middleware class that can be instantiated by Starlette + """ + + class ConfiguredAuthMiddleware(CustomAuthMiddleware): + def __init__(self, app: ASGIApp): + super().__init__(app, app_config) + + return ConfiguredAuthMiddleware \ No newline at end of file diff --git a/utils/query_helpers.py b/utils/query_helpers.py index 1ab58d0..827c0f4 100644 --- a/utils/query_helpers.py +++ b/utils/query_helpers.py @@ -4,34 +4,95 @@ from datetime import datetime from sysdig_client.rest import RESTResponseType, ApiException +import json +import logging +log = logging.getLogger(__name__) -def create_standard_response(results: RESTResponseType, execution_time_ms: str, **metadata_kwargs) -> dict: + +def _parse_response_to_obj(results: RESTResponseType | dict | list | str | bytes) -> dict | list: + """Best-effort conversion of various response types into a Python object. + Returns {} on empty/non-JSON bodies. """ - Creates a standard response format for API calls. - Args: - results (RESTResponseType): The results from the API call. - execution_time_ms (str): The execution time in milliseconds. - **metadata_kwargs: Additional metadata to include in the response. + # Already a Python structure + if results is None: + return {} + if isinstance(results, (dict, list)): + return results + + # `requests.Response`-like: has .json() / .text + if hasattr(results, "json") and hasattr(results, "text"): + try: + return results.json() + except Exception: + txt = getattr(results, "text", "") or "" + txt = txt.strip() + if not txt: + return {} + try: + return json.loads(txt) + except Exception: + log.debug("create_standard_response: non-JSON text: %r", txt[:200]) + return {} + + # urllib3.HTTPResponse-like: has .data (bytes) + if hasattr(results, "data"): + data = getattr(results, "data", b"") or b"" + if not data: + return {} + try: + return json.loads(data.decode("utf-8")) + except Exception: + log.debug("create_standard_response: non-JSON bytes: %r", data[:200]) + return {} + + # Pydantic v2 BaseModel + if hasattr(results, "model_dump"): + try: + return results.model_dump() + except Exception: + return {} + + # Raw JSON string/bytes + if isinstance(results, (bytes, str)): + s = results.decode("utf-8") if isinstance(results, bytes) else results + s = s.strip() + if not s: + return {} + try: + return json.loads(s) + except Exception: + log.debug("create_standard_response: raw string not JSON: %r", s[:200]) + return {} - Returns: - dict: A dictionary containing the results and metadata. + # Fallback + return {} - Raises: - ApiException: If the API call returned an error status code. + +def create_standard_response(results: RESTResponseType, execution_time_ms: float, **metadata_kwargs) -> dict: + """ + Creates a standard response format for API calls. Tolerates empty/non-JSON bodies. + Raises ApiException if the HTTP status is >= 300 (when available). """ - response: dict = {} - if results.status > 299: + status = getattr(results, "status", 200) + reason = getattr(results, "reason", "") + + # Propagate API errors if we have status + if hasattr(results, "status") and status > 299: raise ApiException( - status=results.status, - reason=results.reason, - data=results.data, + status=status, + reason=reason, + data=getattr(results, "data", None), ) - else: - response = results.json() if results.data else {} + + parsed = _parse_response_to_obj(results) return { - "results": response, - "metadata": {"execution_time_ms": execution_time_ms, "timestamp": datetime.utcnow().isoformat() + "Z", **metadata_kwargs}, - "status_code": results.status, + "results": parsed, + "metadata": { + "execution_time_ms": execution_time_ms, + "timestamp": datetime.utcnow().isoformat() + "Z", + **metadata_kwargs, + }, + "status_code": status, } diff --git a/utils/sysdig/client_config.py b/utils/sysdig/client_config.py index 7849ded..f0a7c28 100644 --- a/utils/sysdig/client_config.py +++ b/utils/sysdig/client_config.py @@ -11,12 +11,12 @@ # Application config loader from utils.app_config import get_app_config +app_config = get_app_config() + # Set up logging -logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR")) +logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=app_config.log_level()) log = logging.getLogger(__name__) -app_config = get_app_config() - # Lazy-load the Sysdig client configuration def get_configuration( diff --git a/uv.lock b/uv.lock index 731cd15..173795d 100644 --- a/uv.lock +++ b/uv.lock @@ -25,6 +25,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + [[package]] name = "certifi" version = "2025.6.15" @@ -173,16 +182,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.12" +version = "0.116.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, ] [[package]] @@ -310,6 +319,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + [[package]] name = "locket" version = "1.0.0" @@ -333,12 +369,13 @@ wheels = [ [[package]] name = "mcp" -version = "1.9.4" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "httpx-sse" }, + { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-multipart" }, @@ -346,9 +383,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f", size = 333294, upload-time = "2025-06-12T08:20:30.158Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/1a/d90e42be23a7e6dd35c03e35c7c63fe1036f082d3bb88114b66bd0f2467e/mcp-1.10.0.tar.gz", hash = "sha256:91fb1623c3faf14577623d14755d3213db837c5da5dae85069e1b59124cbe0e9", size = 392961, upload-time = "2025-06-26T13:51:19.025Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232, upload-time = "2025-06-12T08:20:28.551Z" }, + { url = "https://files.pythonhosted.org/packages/0f/52/e1c43c4b5153465fd5d3b4b41bf2d4c7731475e9f668f38d68f848c25c9a/mcp-1.10.0-py3-none-any.whl", hash = "sha256:925c45482d75b1b6f11febddf9736d55edf7739c7ea39b583309f6651cbc9e5c", size = 150894, upload-time = "2025-06-26T13:51:17.342Z" }, ] [package.optional-dependencies] @@ -584,6 +621,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + [[package]] name = "requests" version = "2.32.4" @@ -612,6 +663,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] +[[package]] +name = "rpds-py" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" }, + { url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" }, + { url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" }, + { url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" }, + { url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" }, + { url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" }, + { url = "https://files.pythonhosted.org/packages/81/d2/dfdfd42565a923b9e5a29f93501664f5b984a802967d48d49200ad71be36/rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", size = 362133, upload-time = "2025-08-07T08:24:04.508Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/0a2e2460c4b66021d349ce9f6331df1d6c75d7eea90df9785d333a49df04/rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", size = 347128, upload-time = "2025-08-07T08:24:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/35/8d/7d1e4390dfe09d4213b3175a3f5a817514355cb3524593380733204f20b9/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", size = 384027, upload-time = "2025-08-07T08:24:06.841Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/78499d1a62172891c8cd45de737b2a4b84a414b6ad8315ab3ac4945a5b61/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc", size = 399973, upload-time = "2025-08-07T08:24:08.143Z" }, + { url = "https://files.pythonhosted.org/packages/10/a1/1c67c1d8cc889107b19570bb01f75cf49852068e95e6aee80d22915406fc/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe", size = 515295, upload-time = "2025-08-07T08:24:09.711Z" }, + { url = "https://files.pythonhosted.org/packages/df/27/700ec88e748436b6c7c4a2262d66e80f8c21ab585d5e98c45e02f13f21c0/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9", size = 406737, upload-time = "2025-08-07T08:24:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/33/cc/6b0ee8f0ba3f2df2daac1beda17fde5cf10897a7d466f252bd184ef20162/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c", size = 385898, upload-time = "2025-08-07T08:24:12.798Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/c927b37d7d33c0a0ebf249cc268dc2fcec52864c1b6309ecb960497f2285/rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295", size = 405785, upload-time = "2025-08-07T08:24:14.906Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/8ed50746d909dcf402af3fa58b83d5a590ed43e07251d6b08fad1a535ba6/rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43", size = 419760, upload-time = "2025-08-07T08:24:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/d3/60/2b2071aee781cb3bd49f94d5d35686990b925e9b9f3e3d149235a6f5d5c1/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432", size = 561201, upload-time = "2025-08-07T08:24:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/98/1f/27b67304272521aaea02be293fecedce13fa351a4e41cdb9290576fc6d81/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b", size = 591021, upload-time = "2025-08-07T08:24:18.999Z" }, + { url = "https://files.pythonhosted.org/packages/db/9b/a2fadf823164dd085b1f894be6443b0762a54a7af6f36e98e8fcda69ee50/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d", size = 556368, upload-time = "2025-08-07T08:24:20.54Z" }, + { url = "https://files.pythonhosted.org/packages/24/f3/6d135d46a129cda2e3e6d4c5e91e2cc26ea0428c6cf152763f3f10b6dd05/rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd", size = 221236, upload-time = "2025-08-07T08:24:22.144Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/65d7494f5448ecc755b545d78b188440f81da98b50ea0447ab5ebfdf9bd6/rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2", size = 232634, upload-time = "2025-08-07T08:24:23.642Z" }, + { url = "https://files.pythonhosted.org/packages/70/d9/23852410fadab2abb611733933401de42a1964ce6600a3badae35fbd573e/rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac", size = 222783, upload-time = "2025-08-07T08:24:25.098Z" }, + { url = "https://files.pythonhosted.org/packages/15/75/03447917f78512b34463f4ef11066516067099a0c466545655503bed0c77/rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774", size = 359154, upload-time = "2025-08-07T08:24:26.249Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fc/4dac4fa756451f2122ddaf136e2c6aeb758dc6fdbe9ccc4bc95c98451d50/rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b", size = 343909, upload-time = "2025-08-07T08:24:27.405Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/723c1ed8e6f57ed9d8c0c07578747a2d3d554aaefc1ab89f4e42cfeefa07/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd", size = 379340, upload-time = "2025-08-07T08:24:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/98/16/7e3740413de71818ce1997df82ba5f94bae9fff90c0a578c0e24658e6201/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb", size = 391655, upload-time = "2025-08-07T08:24:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/e0/63/2a9f510e124d80660f60ecce07953f3f2d5f0b96192c1365443859b9c87f/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433", size = 513017, upload-time = "2025-08-07T08:24:31.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4e/cf6ff311d09776c53ea1b4f2e6700b9d43bb4e99551006817ade4bbd6f78/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615", size = 402058, upload-time = "2025-08-07T08:24:32.613Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/5e36096d474cb10f2a2d68b22af60a3bc4164fd8db15078769a568d9d3ac/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8", size = 383474, upload-time = "2025-08-07T08:24:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/db/a2/3dff02805b06058760b5eaa6d8cb8db3eb3e46c9e452453ad5fc5b5ad9fe/rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858", size = 400067, upload-time = "2025-08-07T08:24:35.021Z" }, + { url = "https://files.pythonhosted.org/packages/67/87/eed7369b0b265518e21ea836456a4ed4a6744c8c12422ce05bce760bb3cf/rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5", size = 412085, upload-time = "2025-08-07T08:24:36.267Z" }, + { url = "https://files.pythonhosted.org/packages/8b/48/f50b2ab2fbb422fbb389fe296e70b7a6b5ea31b263ada5c61377e710a924/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9", size = 555928, upload-time = "2025-08-07T08:24:37.573Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/b18eb51045d06887666c3560cd4bbb6819127b43d758f5adb82b5f56f7d1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79", size = 585527, upload-time = "2025-08-07T08:24:39.391Z" }, + { url = "https://files.pythonhosted.org/packages/be/03/a3dd6470fc76499959b00ae56295b76b4bdf7c6ffc60d62006b1217567e1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", size = 554211, upload-time = "2025-08-07T08:24:40.6Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d1/ee5fd1be395a07423ac4ca0bcc05280bf95db2b155d03adefeb47d5ebf7e/rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", size = 216624, upload-time = "2025-08-07T08:24:42.204Z" }, + { url = "https://files.pythonhosted.org/packages/1c/94/4814c4c858833bf46706f87349c37ca45e154da7dbbec9ff09f1abeb08cc/rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", size = 230007, upload-time = "2025-08-07T08:24:43.329Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a5/8fffe1c7dc7c055aa02df310f9fb71cfc693a4d5ccc5de2d3456ea5fb022/rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb", size = 362595, upload-time = "2025-08-07T08:24:44.478Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c7/4e4253fd2d4bb0edbc0b0b10d9f280612ca4f0f990e3c04c599000fe7d71/rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f", size = 347252, upload-time = "2025-08-07T08:24:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/3d1a954d30f0174dd6baf18b57c215da03cf7846a9d6e0143304e784cddc/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64", size = 384886, upload-time = "2025-08-07T08:24:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/52/3c5835f2df389832b28f9276dd5395b5a965cea34226e7c88c8fbec2093c/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015", size = 399716, upload-time = "2025-08-07T08:24:48.174Z" }, + { url = "https://files.pythonhosted.org/packages/40/73/176e46992461a1749686a2a441e24df51ff86b99c2d34bf39f2a5273b987/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0", size = 517030, upload-time = "2025-08-07T08:24:49.52Z" }, + { url = "https://files.pythonhosted.org/packages/79/2a/7266c75840e8c6e70effeb0d38922a45720904f2cd695e68a0150e5407e2/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89", size = 408448, upload-time = "2025-08-07T08:24:50.727Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5f/a7efc572b8e235093dc6cf39f4dbc8a7f08e65fdbcec7ff4daeb3585eef1/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d", size = 387320, upload-time = "2025-08-07T08:24:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/a2/eb/9ff6bc92efe57cf5a2cb74dee20453ba444b6fdc85275d8c99e0d27239d1/rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51", size = 407414, upload-time = "2025-08-07T08:24:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bd/3b9b19b00d5c6e1bd0f418c229ab0f8d3b110ddf7ec5d9d689ef783d0268/rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c", size = 420766, upload-time = "2025-08-07T08:24:55.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/521a7b1079ce16258c70805166e3ac6ec4ee2139d023fe07954dc9b2d568/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4", size = 562409, upload-time = "2025-08-07T08:24:57.17Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bf/65db5bfb14ccc55e39de8419a659d05a2a9cd232f0a699a516bb0991da7b/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e", size = 590793, upload-time = "2025-08-07T08:24:58.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/b8/82d368b378325191ba7aae8f40f009b78057b598d4394d1f2cdabaf67b3f/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e", size = 558178, upload-time = "2025-08-07T08:24:59.756Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ff/f270bddbfbc3812500f8131b1ebbd97afd014cd554b604a3f73f03133a36/rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6", size = 222355, upload-time = "2025-08-07T08:25:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/fdab055b1460c02ed356a0e0b0a78c1dd32dc64e82a544f7b31c9ac643dc/rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a", size = 234007, upload-time = "2025-08-07T08:25:02.268Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a8/694c060005421797a3be4943dab8347c76c2b429a9bef68fb2c87c9e70c7/rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d", size = 223527, upload-time = "2025-08-07T08:25:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f9/77f4c90f79d2c5ca8ce6ec6a76cb4734ee247de6b3a4f337e289e1f00372/rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828", size = 359469, upload-time = "2025-08-07T08:25:04.648Z" }, + { url = "https://files.pythonhosted.org/packages/c0/22/b97878d2f1284286fef4172069e84b0b42b546ea7d053e5fb7adb9ac6494/rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669", size = 343960, upload-time = "2025-08-07T08:25:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b0/dfd55b5bb480eda0578ae94ef256d3061d20b19a0f5e18c482f03e65464f/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd", size = 380201, upload-time = "2025-08-07T08:25:07.513Z" }, + { url = "https://files.pythonhosted.org/packages/28/22/e1fa64e50d58ad2b2053077e3ec81a979147c43428de9e6de68ddf6aff4e/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec", size = 392111, upload-time = "2025-08-07T08:25:09.149Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/43ab7a43e97aedf6cea6af70fdcbe18abbbc41d4ae6cdec1bfc23bbad403/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303", size = 515863, upload-time = "2025-08-07T08:25:10.431Z" }, + { url = "https://files.pythonhosted.org/packages/38/9b/9bd59dcc636cd04d86a2d20ad967770bf348f5eb5922a8f29b547c074243/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b", size = 402398, upload-time = "2025-08-07T08:25:11.819Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/f099328c6c85667aba6b66fa5c35a8882db06dcd462ea214be72813a0dd2/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410", size = 384665, upload-time = "2025-08-07T08:25:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c5/9c1f03121ece6634818490bd3c8be2c82a70928a19de03467fb25a3ae2a8/rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156", size = 400405, upload-time = "2025-08-07T08:25:14.417Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b8/e25d54af3e63ac94f0c16d8fe143779fe71ff209445a0c00d0f6984b6b2c/rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2", size = 413179, upload-time = "2025-08-07T08:25:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d1/406b3316433fe49c3021546293a04bc33f1478e3ec7950215a7fce1a1208/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1", size = 556895, upload-time = "2025-08-07T08:25:17.061Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bc/3697c0c21fcb9a54d46ae3b735eb2365eea0c2be076b8f770f98e07998de/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42", size = 585464, upload-time = "2025-08-07T08:25:18.406Z" }, + { url = "https://files.pythonhosted.org/packages/63/09/ee1bb5536f99f42c839b177d552f6114aa3142d82f49cef49261ed28dbe0/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae", size = 555090, upload-time = "2025-08-07T08:25:20.461Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2c/363eada9e89f7059199d3724135a86c47082cbf72790d6ba2f336d146ddb/rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5", size = 218001, upload-time = "2025-08-07T08:25:21.761Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3f/d6c216ed5199c9ef79e2a33955601f454ed1e7420a93b89670133bca5ace/rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", size = 230993, upload-time = "2025-08-07T08:25:23.34Z" }, +] + [[package]] name = "ruff" version = "0.12.1" @@ -732,7 +864,7 @@ wheels = [ [[package]] name = "sysdig-mcp-server" -version = "0.1.4" +version = "0.1.5" source = { editable = "." } dependencies = [ { name = "dask" }, @@ -758,9 +890,9 @@ dev = [ [package.metadata] requires-dist = [ { name = "dask", specifier = "==2025.4.1" }, - { name = "fastapi", specifier = "==0.115.12" }, + { name = "fastapi", specifier = "==0.116.1" }, { name = "fastmcp", specifier = "==2.5.1" }, - { name = "mcp", extras = ["cli"], specifier = "==1.9.4" }, + { name = "mcp", extras = ["cli"], specifier = "==1.10.0" }, { name = "oauthlib", specifier = "==3.2.2" }, { name = "python-dotenv", specifier = ">=1.1.0" }, { name = "pyyaml", specifier = "==6.0.2" }, From c9408a8b8a2c8ca6dd12ec7388b06695d1181d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= <59057379+alecron@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:06:07 +0200 Subject: [PATCH 02/11] Update utils/mcp_server.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- utils/mcp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/mcp_server.py b/utils/mcp_server.py index 5c1b01b..801f718 100644 --- a/utils/mcp_server.py +++ b/utils/mcp_server.py @@ -119,7 +119,7 @@ async def health_check(request: Request) -> Response: return JSONResponse({"status": "ok"}) log.info( - f"Starting {mcp.name} at http://{app_config.sysdig_endpoint()}:{app_config['app']['port']}{MCP_MOUNT_PATH}{suffix_path}" + f"Starting {mcp.name} at http://{app_config.sysdig_endpoint()}:{app_config.port()}{MCP_MOUNT_PATH}{suffix_path}" ) # Use Uvicorn's Config and Server classes for more control config = uvicorn.Config( From 3653d48748d919598c1ba4e70d3da15e816660a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n=20Soler?= Date: Fri, 22 Aug 2025 15:36:51 +0200 Subject: [PATCH 03/11] fix: Linter and Tests errors --- tests/events_feed_test.py | 18 ++++++++++++++---- tools/events_feed/tool.py | 3 +++ tools/vulnerability_management/tool.py | 16 ++++++++++++---- utils/app_config.py | 25 ++++++++++++++++++------- utils/query_helpers.py | 16 +++++++++++++--- 5 files changed, 60 insertions(+), 18 deletions(-) diff --git a/tests/events_feed_test.py b/tests/events_feed_test.py index 0ce2a1a..1b738ff 100644 --- a/tests/events_feed_test.py +++ b/tests/events_feed_test.py @@ -4,8 +4,9 @@ from http import HTTPStatus from tools.events_feed.tool import EventsFeedTools +from utils.app_config import AppConfig from .conftest import util_load_json -from unittest.mock import MagicMock, AsyncMock +from unittest.mock import MagicMock, AsyncMock, create_autospec import os # Get the absolute path of the current module file @@ -17,19 +18,28 @@ EVENT_INFO_RESPONSE = util_load_json(f"{module_directory}/test_data/events_feed/event_info_response.json") +def mock_app_config() -> AppConfig: + mock_cfg = create_autospec(AppConfig, instance=True) + + mock_cfg.sysdig_endpoint.return_value = "https://us2.app.sysdig.com" + mock_cfg.transport.return_value = "stdio" + mock_cfg.log_level.return_value = "DEBUG" + mock_cfg.port.return_value = 8080 + + return mock_cfg + def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds) -> None: """Test the get_event_info tool method. Args: mock_success_response (MagicMock | AsyncMock): Mocked response object. mock_creds: Mocked credentials. """ - # Override the environment variable for MCP transport - os.environ["MCP_TRANSPORT"] = "stdio" # Successful response mock_success_response.return_value.json.return_value = EVENT_INFO_RESPONSE mock_success_response.return_value.status_code = HTTPStatus.OK - tools_client = EventsFeedTools() + tools_client = EventsFeedTools(app_config=mock_app_config()) + # Pass the mocked Context object result: dict = tools_client.tool_get_event_info("12345") results: dict = result["results"] diff --git a/tools/events_feed/tool.py b/tools/events_feed/tool.py index 22daa6d..3addf56 100644 --- a/tools/events_feed/tool.py +++ b/tools/events_feed/tool.py @@ -189,6 +189,9 @@ def tool_get_event_process_tree(self, event_id: str) -> dict: Returns: dict: A dictionary containing the process tree information for the specified event. + + Raises: + ToolError: If there is an error constructing or processing the response. """ try: start_time = time.time() diff --git a/tools/vulnerability_management/tool.py b/tools/vulnerability_management/tool.py index 23ad212..21bad08 100644 --- a/tools/vulnerability_management/tool.py +++ b/tools/vulnerability_management/tool.py @@ -304,7 +304,9 @@ def tool_get_vulnerability_policy( response: GetPolicyResponse = vulnerability_api.secure_vulnerability_v1_policies_policy_id_get(policy_id) return response.model_dump_json() if hasattr(response, "dict") else response except ToolError as e: - self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_policy_id_get: {e}") + self.log.error( + f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_policy_id_get: {e}" + ) raise e def tool_list_vulnerability_policies( @@ -343,7 +345,9 @@ def tool_list_vulnerability_policies( ) return response except ToolError as e: - self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}") + self.log.error( + f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}" + ) raise e def tool_list_pipeline_scan_results( @@ -409,7 +413,9 @@ def tool_list_pipeline_scan_results( ) return response except ToolError as e: - self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}") + self.log.error( + f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}" + ) raise e def tool_get_scan_result(self, scan_id: str) -> dict: @@ -427,7 +433,9 @@ def tool_get_scan_result(self, scan_id: str) -> dict: resp: ScanResultResponse = vulnerability_api.secure_vulnerability_v1_results_result_id_get(scan_id) return resp.model_dump_json() if hasattr(resp, "dict") else resp except ToolError as e: - self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_results_result_id_get: {e}") + self.log.error( + f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_results_result_id_get: {e}" + ) raise e def explore_vulnerabilities_prompt(self, filters: str) -> PromptMessage: diff --git a/utils/app_config.py b/utils/app_config.py index 24796ae..2b39400 100644 --- a/utils/app_config.py +++ b/utils/app_config.py @@ -28,27 +28,38 @@ def __init__(self, config: dict): def sysdig_endpoint(self) -> str: """ Get the Sysdig endpoint from the app config + + Returns: + str: The Sysdig API host (e.g., "https://us2.app.sysdig.com"). """ - return self.app_config["sysdig"]["host"] + return os.environ.get("SYSDIG_HOST", self.app_config["sysdig"]["host"]) def transport(self) -> str: """ Get the transport protocol (lower case) from the app config + + Returns: + str: The transport protocol (e.g., "stdio", "streamable-http", or "sse"). """ return os.environ.get("MCP_TRANSPORT", self.app_config["mcp"]["transport"]).lower() - @staticmethod - def log_level() -> str: + def log_level(self) -> str: """ - Get the log level from the app config + Get the log level from the environment or defaults. + + Returns: + str: The log level string (e.g., "DEBUG", "INFO", "WARNING", "ERROR"). """ return os.environ.get("LOGLEVEL", "ERROR") def port(self) -> int: """ Get the port from the app config + + Returns: + int: The MCP server port. """ - return self.app_config["mcp"]["port"] + return os.environ.get("SYSDIG_MCP_PORT", self.app_config["mcp"]["port"]) def env_constructor(loader, node): @@ -75,7 +86,7 @@ def load_app_config() -> AppConfig: Load the app config from the YAML file Returns: - dict: The app config loaded from the YAML file + AppConfig: The loaded application configuration wrapper. """ if not check_config_file_exists(): log.error("Config file does not exist") @@ -100,7 +111,7 @@ def get_app_config() -> AppConfig: If the config is already loaded, it returns the existing config. Returns: - dict: The app config loaded from the YAML file, or an empty dict if the file does not exist or is invalid. + AppConfig: The singleton application configuration wrapper. """ global _app_config if _app_config is None: diff --git a/utils/query_helpers.py b/utils/query_helpers.py index 827c0f4..82cae96 100644 --- a/utils/query_helpers.py +++ b/utils/query_helpers.py @@ -12,7 +12,9 @@ def _parse_response_to_obj(results: RESTResponseType | dict | list | str | bytes) -> dict | list: """Best-effort conversion of various response types into a Python object. - Returns {} on empty/non-JSON bodies. + + Returns: + dict | list: Parsed JSON-compatible object. Returns {} on empty or non-JSON bodies. """ # Already a Python structure if results is None: @@ -69,10 +71,18 @@ def _parse_response_to_obj(results: RESTResponseType | dict | list | str | bytes return {} -def create_standard_response(results: RESTResponseType, execution_time_ms: float, **metadata_kwargs) -> dict: +def create_standard_response(results: RESTResponseType, execution_time_ms: float | str, **metadata_kwargs) -> dict: """ Creates a standard response format for API calls. Tolerates empty/non-JSON bodies. - Raises ApiException if the HTTP status is >= 300 (when available). + + Returns: + dict: A dictionary with keys: + - results: parsed body as dict or list (possibly empty {}) + - metadata: includes execution_time_ms (float) and ISO8601 UTC timestamp + - status_code: HTTP status code (int) + + Raises: + ApiException: If the HTTP status is >= 300 when status information is available. """ status = getattr(results, "status", 200) reason = getattr(results, "reason", "") From 544358b7b621c276a2ee25369cb859bf6883fde5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Mon, 25 Aug 2025 16:58:29 +0200 Subject: [PATCH 04/11] refactor: Remove app_config.yaml file and update env vars --- README.md | 57 ++++------------ app_config.yaml | 20 ------ main.py | 2 +- tools/cli_scanner/tool.py | 9 +-- tools/events_feed/tool.py | 3 - utils/app_config.py | 115 ++++++++++++++++----------------- utils/mcp_server.py | 15 ++--- utils/sysdig/client_config.py | 42 +++--------- utils/sysdig/old_sysdig_api.py | 2 +- 9 files changed, 89 insertions(+), 176 deletions(-) delete mode 100644 app_config.yaml diff --git a/README.md b/README.md index e0e77d1..5d69334 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ - [Requirements](#requirements) - [UV Setup](#uv-setup) - [Configuration](#configuration) - - [`app_config.yaml`](#app_configyaml) - [Environment Variables](#environment-variables) - [Running the Server](#running-the-server) - [Docker](#docker) @@ -85,21 +84,6 @@ Get up and running with the Sysdig MCP Server quickly using our pre-built Docker ## Available Tools -You can select what group of tools to add when running the server by adding/removing them from the `mcp.allowed_tools` list in the app_config.yaml file - -```yaml -... -mcp: - transport: stdio - ... - allowed_tools: - - "events-feed" - - "inventory" - - "vulnerability-management" - - "sysdig-sage" - - "sysdig-cli-scanner" # Only available in stdio local transport mode -``` -
Events Feed @@ -168,13 +152,9 @@ mcp: You can use [uv](https://github.com/astral-sh/uv) as a drop-in replacement for pip to create the virtual environment and install dependencies. -If you don't have `uv` installed, you can install it via (Linux and MacOS users): +If you don't have `uv` installed, you can install it following the instructions that you can find on the `README` of the project. -```bash -curl -Ls https://astral.sh/uv/install.sh | sh -``` - -To set up the environment: +If you want to develop, set up the environment using: ```bash uv venv @@ -185,25 +165,19 @@ This will create a virtual environment using `uv` and install the required depen ## Configuration -The application can be configured via the `app_config.yaml` file and environment variables. - -### `app_config.yaml` - -This file contains the main configuration for the application, including: - -- **app**: Host, port, and log level for the MCP server. -- **sysdig**: The Sysdig Secure host to connect to. -- **mcp**: Transport protocol (stdio, sse, streamable-http), URL, host, and port for the MCP server. - -> You can set the path for the app_config.yaml using the `APP_CONFIG_FILE=/path/to/app_config.yaml` env var. By default the app will search the file in the root of the app. - -### Environment Variables - -The following environment variables are required for configuring the Sysdig SDK: +The following environment variables are **required** for configuring the Sysdig SDK: - `SYSDIG_HOST`: The URL of your Sysdig Secure instance (e.g., `https://us2.app.sysdig.com`). - `SYSDIG_SECURE_TOKEN`: Your Sysdig Secure API token. +You can also set the following variables to override the default configuration: + +- `SYSDIG_MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`. +- `SYSDIG_MCP_MOUNT_PATH`: The URL prefix for the Streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server` +- `LOGLEVEL`: Log Level of the application (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Defaults to: `INFO` +- `SYSDIG_MCP_LISTENING_PORT`: The port for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `8080` +- `SYSDIG_MCP_LISTENING_HOST`: The host for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `localhost` + You can find your API token in the Sysdig Secure UI under **Settings > Sysdig Secure API**. Make sure to copy the token as it will not be shown again. ![API_TOKEN_CONFIG](./docs/assets/settings-config-token.png) @@ -211,10 +185,6 @@ You can find your API token in the Sysdig Secure UI under **Settings > Sysdig Se You can set these variables in your shell or in a `.env` file. -You can also use `MCP_TRANSPORT` to override the transport protocol set in `app_config.yaml`. - -> All of this env variables have precedence over the fields configured in the app_config.yaml. - ## Running the Server You can run the MCP server using either Docker, `uv` or install it in your K8s cluster with helm. @@ -255,7 +225,6 @@ sysdig: secureAPIToken: "" mcp: transport: "streamable-http" - # You can set the Sysdig Tenant URL at this level or below in the app_config configmap host: "https://us2.app.sysdig.com" # "https://eu1.app.sysdig.com" configMap: @@ -312,7 +281,7 @@ To use the MCP server with a client like Claude or Cursor, you need to provide t When using the `sse` or `streamable-http` transport, the server requires a Bearer token for authentication. The token is passed in the `Authorization` header of the HTTP request. -Additionally, you can specify the Sysdig Secure host by providing the `X-Sysdig-Host` header. If this header is not present, the server will use the value from `app_config.yaml`. +Additionally, you can specify the Sysdig Secure host by providing the `X-Sysdig-Host` header. If this header is not present, the server will use the value from the env variable. Example headers: @@ -323,7 +292,7 @@ X-Sysdig-Host: ### URL -If you are running the server with the `sse` or `streamable-http` transport, the URL will be `http://:/sysdig-mcp-server/mcp`, where `` and `` are the values configured in `app_config.yaml` or the Docker run command. +If you are running the server with the `sse` or `streamable-http` transport, the URL will be `http://:/sysdig-mcp-server/mcp`. For example, if you are running the server locally on port 8080, the URL will be `http://localhost:8080/sysdig-mcp-server/mcp`. diff --git a/app_config.yaml b/app_config.yaml deleted file mode 100644 index 3c04fab..0000000 --- a/app_config.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -app: - host: "localhost" - port: 8080 - log_level: "ERROR" - -sysdig: - host: "https://us2.app.sysdig.com" - # public_api_url: "https://" - -mcp: - transport: stdio - host: "localhost" - port: 8080 - allowed_tools: - - "events-feed" - - "sysdig-cli-scanner" # Only available in stdio local transport mode - - "vulnerability-management" - - "inventory" - - "sysdig-sage" diff --git a/main.py b/main.py index ff19bbb..c30bf81 100644 --- a/main.py +++ b/main.py @@ -42,7 +42,7 @@ def signal_handler(sig, frame): def main(): # Choose transport: "stdio" or "sse" (HTTP/SSE) handle_signals() - transport = os.environ.get("MCP_TRANSPORT", app_config.transport()) + transport = app_config.transport() log.info(""" ▄▖ ▌▘ ▖ ▖▄▖▄▖ ▄▖ ▚ ▌▌▛▘▛▌▌▛▌ ▛▖▞▌▌ ▙▌ ▚ █▌▛▘▌▌█▌▛▘ diff --git a/tools/cli_scanner/tool.py b/tools/cli_scanner/tool.py index 1d83dfe..55472f6 100644 --- a/tools/cli_scanner/tool.py +++ b/tools/cli_scanner/tool.py @@ -11,9 +11,6 @@ from utils.app_config import AppConfig - - - class CLIScannerTool: """ A class to encapsulate the tools for interacting with the Sysdig CLI Scanner. @@ -62,8 +59,8 @@ def check_env_credentials(self) -> None: Raises: EnvironmentError: If the SYSDIG_SECURE_TOKEN or SYSDIG_HOST environment variables are not set. """ - sysdig_secure_token = os.environ.get("SYSDIG_SECURE_TOKEN") - sysdig_host = os.environ.get("SYSDIG_HOST", self.app_config.sysdig_endpoint()) + sysdig_secure_token = self.app_config.sysdig_secure_token() + sysdig_host = self.app_config.sysdig_endpoint() if not sysdig_secure_token: self.log.error("SYSDIG_SECURE_TOKEN environment variable is not set.") raise EnvironmentError("SYSDIG_SECURE_TOKEN environment variable is not set.") @@ -158,7 +155,7 @@ def run_sysdig_cli_scanner( "output": output_result + result.stderr.strip(), "exit_codes_explained": self.exit_code_explained, } - # Handle non-zero exit codes speically exit code 1 + # Handle non-zero exit codes specially exit code 1 except subprocess.CalledProcessError as e: self.log.warning(f"Sysdig CLI Scanner returned non-zero exit code: {e.returncode}") if e.returncode in [2, 3]: diff --git a/tools/events_feed/tool.py b/tools/events_feed/tool.py index 3addf56..77aafaf 100644 --- a/tools/events_feed/tool.py +++ b/tools/events_feed/tool.py @@ -138,9 +138,6 @@ def tool_list_runtime_events( Args: cursor (Optional[str]): Cursor for pagination. scope_hours (int): Number of hours back from now to include events. Defaults to 1. - severity_level (Optional[str]): One of "info", "low", "medium", "high". If provided, filters by that severity. - If None, includes all severities. - cluster_name (Optional[str]): Name of the Kubernetes cluster to filter events. If None, includes all clusters. limit (int): Maximum number of events to return. Defaults to 50. filter_expr (Optional[str]): An optional filter expression to further narrow down events. diff --git a/utils/app_config.py b/utils/app_config.py index 2b39400..0404047 100644 --- a/utils/app_config.py +++ b/utils/app_config.py @@ -3,110 +3,107 @@ It will load a singleton configuration object that can be accessed throughout the application. """ -import yaml -import logging import os from typing import Optional -# Set up logging -logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR")) -log = logging.getLogger(__name__) - # app_config singleton _app_config: Optional[dict] = None -APP_CONFIG_FILE: str = os.getenv("APP_CONFIG_FILE", "./app_config.yaml") - class AppConfig: """ A class to encapsulate the application configuration. """ - def __init__(self, config: dict): - self.app_config = config - def sysdig_endpoint(self) -> str: """ - Get the Sysdig endpoint from the app config + Get the Sysdig endpoint. + Raises: + RuntimeError: If no SYSDIG_HOST environment variable is set. Returns: str: The Sysdig API host (e.g., "https://us2.app.sysdig.com"). """ - return os.environ.get("SYSDIG_HOST", self.app_config["sysdig"]["host"]) + if "SYSDIG_HOST" not in os.environ: + raise RuntimeError("Variable `SYSDIG_HOST` must be defined.") + + return os.environ.get("SYSDIG_HOST") + + def sysdig_secure_token(self) -> str: + """ + Get the Sysdig secure token. + + Raises: + RuntimeError: If no SYSDIG_SECURE_TOKEN environment variable is set. + Returns: + str: The Sysdig secure token. + """ + if "SYSDIG_SECURE_TOKEN" not in os.environ: + raise RuntimeError("Variable `SYSDIG_SECURE_TOKEN` must be defined.") + return os.environ.get("SYSDIG_SECURE_TOKEN") + + # MCP Config Vars def transport(self) -> str: """ - Get the transport protocol (lower case) from the app config + Get the transport protocol (lower case). + Valid values are: "stdio", "streamable-http", or "sse". + Defaults to "stdio". + Raises: + ValueError: If no transport protocol environment variable is set. Returns: str: The transport protocol (e.g., "stdio", "streamable-http", or "sse"). """ - return os.environ.get("MCP_TRANSPORT", self.app_config["mcp"]["transport"]).lower() + transport = os.environ.get("SYSDIG_TRANSPORT", "stdio").lower() + + if transport not in ("stdio", "streamable-http", "sse"): + raise ValueError("Invalid transport protocol. Valid values are: stdio, streamable-http, sse.") + + return transport def log_level(self) -> str: """ - Get the log level from the environment or defaults. + Get the log level from the environment or defaults to INFO. Returns: str: The log level string (e.g., "DEBUG", "INFO", "WARNING", "ERROR"). """ - return os.environ.get("LOGLEVEL", "ERROR") + return os.environ.get("LOGLEVEL", "INFO") def port(self) -> int: """ - Get the port from the app config + Get the port for the remote MCP Server Deployment ("streamable-http", or "sse" transports). + Defaults to `8080`. Returns: int: The MCP server port. """ - return os.environ.get("SYSDIG_MCP_PORT", self.app_config["mcp"]["port"]) - - -def env_constructor(loader, node): - return os.environ[node.value[0:]] + return os.environ.get("SYSDIG_MCP_LISTENING_PORT", "8080") + # + def host(self) -> str: + """ + Get the host for the remote MCP Server deployment ("streamable-http", or "sse" transports). + Defaults to "localhost". -def check_config_file_exists() -> bool: - """ - Check if the config file exists - - Returns: - bool: True if the config file exists, False otherwise - """ - if os.path.exists(APP_CONFIG_FILE): - log.debug("Config file exists") - return True - else: - log.error("Config file does not exist") - return False - + Returns: + str: The host string (e.g., "localhost"). + """ + return os.environ.get("SYSDIG_MCP_LISTENING_HOST", "localhost") -def load_app_config() -> AppConfig: - """ - Load the app config from the YAML file + def mcp_mount_path(self) -> str: + """ + Get the string value for the remote MCP Mount Path. - Returns: - AppConfig: The loaded application configuration wrapper. - """ - if not check_config_file_exists(): - log.error("Config file does not exist") - return {} - # Load the config file - app_config: dict = {} - log.debug(f"Loading app config from YAML file: {APP_CONFIG_FILE}") - with open(APP_CONFIG_FILE, "r", encoding="utf8") as file: - try: - yaml.add_constructor("!env", env_constructor, Loader=yaml.SafeLoader) - app_config: dict = yaml.safe_load(file) - except Exception as exc: - logging.error(exc) - - return AppConfig(app_config) + Returns: + str: The MCP mount path. + """ + return os.environ.get("MCP_MOUNT_PATH", "/sysdig-mcp-server") def get_app_config() -> AppConfig: """ - Get the the overall app config + Get the overall app config This function uses a singleton pattern to ensure the config is loaded only once. If the config is already loaded, it returns the existing config. @@ -115,5 +112,5 @@ def get_app_config() -> AppConfig: """ global _app_config if _app_config is None: - _app_config = load_app_config() + _app_config = AppConfig() return _app_config diff --git a/utils/mcp_server.py b/utils/mcp_server.py index 801f718..07c45ab 100644 --- a/utils/mcp_server.py +++ b/utils/mcp_server.py @@ -39,8 +39,6 @@ middlewares = [Middleware(create_auth_middleware(app_config))] -MCP_MOUNT_PATH = "/sysdig-mcp-server" - def create_simple_mcp_server() -> FastMCP: """ @@ -52,7 +50,7 @@ def create_simple_mcp_server() -> FastMCP: return FastMCP( name="Sysdig MCP Server", instructions="Provides Sysdig Secure tools and resources.", - host=app_config.sysdig_endpoint(), + host=app_config.host(), port=app_config.port(), tags=["sysdig", "mcp", app_config.transport()], ) @@ -100,11 +98,11 @@ def run_http(): # Add resources to the MCP server add_resources(mcp) - # Mount the MCP HTTP/SSE app at 'MCP_MOUNT_PATH' + # Mount the MCP HTTP/SSE app mcp_app = mcp.http_app(transport=transport, middleware=middlewares) suffix_path = mcp.settings.streamable_http_path if transport == "streamable-http" else mcp.settings.sse_path app = FastAPI(lifespan=mcp_app.lifespan) - app.mount(MCP_MOUNT_PATH, mcp_app) + app.mount(app_config.mcp_mount_path(), mcp_app) @app.get("/healthz", response_class=Response) async def health_check(request: Request) -> Response: @@ -119,7 +117,7 @@ async def health_check(request: Request) -> Response: return JSONResponse({"status": "ok"}) log.info( - f"Starting {mcp.name} at http://{app_config.sysdig_endpoint()}:{app_config.port()}{MCP_MOUNT_PATH}{suffix_path}" + f"Starting {mcp.name} at http://{app_config.host()}:{app_config.port()}{app_config.mcp_mount_path()}{suffix_path}" ) # Use Uvicorn's Config and Server classes for more control config = uvicorn.Config( @@ -145,11 +143,10 @@ async def health_check(request: Request) -> Response: def add_tools(mcp: FastMCP) -> None: """ - Add tools to the MCP server based on the allowed tools and transport type. + Registers the tools to the MCP Server. + Args: mcp (FastMCP): The FastMCP server instance. - allowed_tools (list): List of tools to register. - transport_type (Literal["stdio", "streamable-http"]): The transport type for the MCP server. """ # Register the events feed tools events_feed_tools = EventsFeedTools(app_config) diff --git a/utils/sysdig/client_config.py b/utils/sysdig/client_config.py index f0a7c28..3203467 100644 --- a/utils/sysdig/client_config.py +++ b/utils/sysdig/client_config.py @@ -37,10 +37,10 @@ def get_configuration( ValueError: If the Sysdig host URL is not provided or is invalid. """ # Check if the token and sysdig_host_url are provided, otherwise fetch from environment variables - if not token and not sysdig_host_url: - env_vars = get_api_env_vars() - token = env_vars["SYSDIG_SECURE_TOKEN"] - sysdig_host_url = env_vars["SYSDIG_HOST"] + if not token: + token = app_config.sysdig_secure_token() + if not sysdig_host_url: + sysdig_host_url = app_config.sysdig_endpoint() if not old_api: """ Client expecting the public API URL in the format https://api.{region}.sysdig.com. We will check the following: @@ -50,13 +50,11 @@ def get_configuration( """ sysdig_host_url = _get_public_api_url(sysdig_host_url) if not sysdig_host_url: - sysdig_host_url = app_config.get("sysdig", {}).get("public_api_url") - if not sysdig_host_url: - raise ValueError( - "No valid Sysdig public API URL found. Please check your Sysdig host URL or" - "explicitly set the public API URL in the app config 'sysdig.public_api_url'." - "The expected format is https://api.{region}.sysdig.com." - ) + raise ValueError( + "No valid Sysdig public API URL found. Please check your Sysdig host URL or" + "explicitly set the public API URL in the app config 'sysdig.public_api_url'." + "The expected format is https://api.{region}.sysdig.com." + ) log.info(f"Using public API URL: {sysdig_host_url}") configuration = sysdig_client.Configuration( @@ -66,28 +64,6 @@ def get_configuration( return configuration -def get_api_env_vars() -> dict: - """ - Get the necessary environment variables for the Sysdig API client. - - Returns: - dict: A dictionary containing the required environment variables. - Raises: - ValueError: If any of the required environment variables are not set. - """ - required_vars = ["SYSDIG_SECURE_TOKEN", "SYSDIG_HOST"] - env_vars = {} - for var in required_vars: - value = os.environ.get(var) - if not value: - log.error(f"Missing required environment variable: {var}") - raise ValueError(f"Environment variable {var} is not set. Please set it before running the application.") - env_vars[var] = value - log.info("All required environment variables are set.") - - return env_vars - - def _get_public_api_url(base_url: str) -> str: """ Maps a Sysdig base URL to its corresponding public API URL. diff --git a/utils/sysdig/old_sysdig_api.py b/utils/sysdig/old_sysdig_api.py index eedf21c..a2e84b6 100644 --- a/utils/sysdig/old_sysdig_api.py +++ b/utils/sysdig/old_sysdig_api.py @@ -10,7 +10,7 @@ class OldSysdigApi: """ - Wrapper for Old non public Sysdig API. + Wrapper for Old non-public Sysdig API. """ def __init__(self, api_client: ApiClient): From 7bbe1bbcc7964159b4840b7df0574c30d003360a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Tue, 26 Aug 2025 11:10:44 +0200 Subject: [PATCH 05/11] refactor: Move mcp_server to a new class --- main.py | 9 +- utils/mcp_server.py | 536 +++++++++++++++++++++----------------------- 2 files changed, 260 insertions(+), 285 deletions(-) diff --git a/main.py b/main.py index c30bf81..f6d5134 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from utils.app_config import get_app_config # Register all tools so they attach to the MCP server -from utils.mcp_server import run_stdio, run_http +from utils.mcp_server import SysdigMCPServer # Load environment variables from .env load_dotenv() @@ -49,12 +49,15 @@ def main(): ▄▌▙▌▄▌▙▌▌▙▌ ▌▝ ▌▙▖▌ ▄▌▙▖▌ ▚▘▙▖▌ ▄▌ ▄▌ """) + + mcp_server = SysdigMCPServer(app_config=app_config) + if transport == "stdio": # Run MCP server over STDIO (local) - run_stdio() + mcp_server.run_stdio() else: # Run MCP server over streamable HTTP by default - run_http() + mcp_server.run_http() if __name__ == "__main__": diff --git a/utils/mcp_server.py b/utils/mcp_server.py index 07c45ab..3e7eae1 100644 --- a/utils/mcp_server.py +++ b/utils/mcp_server.py @@ -23,308 +23,280 @@ from tools.cli_scanner.tool import CLIScannerTool # Application config loader -from utils.app_config import get_app_config, AppConfig +from utils.app_config import AppConfig -# Load app config (expects keys: mcp.host, mcp.port, mcp.transport) -app_config = get_app_config() +class SysdigMCPServer: -# Set up logging -logging.basicConfig( - format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", - level=app_config.log_level(), -) -log = logging.getLogger(__name__) - -_mcp_instance: Optional[FastMCP] = None - -middlewares = [Middleware(create_auth_middleware(app_config))] - - -def create_simple_mcp_server() -> FastMCP: - """ - Instantiate and configure the FastMCP server. - - Returns: - FastMCP: An instance of the FastMCP server configured with Sysdig Secure tools and resources. - """ - return FastMCP( - name="Sysdig MCP Server", - instructions="Provides Sysdig Secure tools and resources.", - host=app_config.host(), - port=app_config.port(), - tags=["sysdig", "mcp", app_config.transport()], - ) - - -def get_mcp() -> FastMCP: - """ - Return a singleton FastMCP instance. - - Returns: - FastMCP: The singleton instance of the FastMCP server. - """ - global _mcp_instance - if _mcp_instance is None: - _mcp_instance = create_simple_mcp_server() - return _mcp_instance - - -def run_stdio(): - """ - Run the MCP server using STDIO transport. - """ - mcp = get_mcp() - # Add tools to the MCP server - add_tools(mcp=mcp) - # Add resources to the MCP server - add_resources(mcp) - try: - asyncio.run(mcp.run_stdio_async()) - except KeyboardInterrupt: - log.info("Keyboard interrupt received, forcing immediate exit") - os._exit(0) - except Exception as e: - log.error(f"Exception received, forcing immediate exit: {str(e)}") - os._exit(1) - - -def run_http(): - """Run the MCP server over HTTP/SSE transport via Uvicorn.""" - mcp = get_mcp() - # Add tools to the MCP server - transport = app_config.transport() - - add_tools(mcp=mcp) - # Add resources to the MCP server - add_resources(mcp) - - # Mount the MCP HTTP/SSE app - mcp_app = mcp.http_app(transport=transport, middleware=middlewares) - suffix_path = mcp.settings.streamable_http_path if transport == "streamable-http" else mcp.settings.sse_path - app = FastAPI(lifespan=mcp_app.lifespan) - app.mount(app_config.mcp_mount_path(), mcp_app) + def __init__(self, app_config: AppConfig): + self.app_config = app_config + # Set up logging + logging.basicConfig( + format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", + level=self.app_config.log_level(), + ) + self.log = logging.getLogger(__name__) + self.middlewares = [Middleware(create_auth_middleware(app_config))] + self.mcp_instance: Optional[FastMCP] = FastMCP( + name="Sysdig MCP Server", + instructions="Provides Sysdig Secure tools and resources.", + host=app_config.host(), + port=app_config.port(), + tags=["sysdig", "mcp", app_config.transport()], + ) + # Add tools to the MCP server + self.add_tools() + # Add resources to the MCP server + self.add_resources() - @app.get("/healthz", response_class=Response) - async def health_check(request: Request) -> Response: + def run_stdio(self): """ - Health check endpoint. - - Args: - request (Request): The incoming HTTP request. - Returns: - Response: A JSON response indicating the server status. + Run the MCP server using STDIO transport. """ - return JSONResponse({"status": "ok"}) - - log.info( - f"Starting {mcp.name} at http://{app_config.host()}:{app_config.port()}{app_config.mcp_mount_path()}{suffix_path}" - ) - # Use Uvicorn's Config and Server classes for more control - config = uvicorn.Config( - app, - host=app_config.sysdig_endpoint(), - port=app_config.port(), - timeout_graceful_shutdown=1, - log_level=app_config.log_level().lower(), - ) - server = uvicorn.Server(config) + try: + asyncio.run(self.mcp_instance.run_stdio_async()) + except KeyboardInterrupt: + self.log.info("Keyboard interrupt received, forcing immediate exit") + os._exit(0) + except Exception as e: + self.log.error(f"Exception received, forcing immediate exit: {str(e)}") + os._exit(1) + + + def run_http(self): + """Run the MCP server over HTTP/SSE transport via Uvicorn.""" + + # Add tools to the MCP server + transport = self.app_config.transport() + + # Mount the MCP HTTP/SSE app + mcp_app = self.mcp_instance.http_app(transport=transport, middleware=self.middlewares) + suffix_path = ( + self.mcp_instance.settings.streamable_http_path if transport == "streamable-http" + else self.mcp_instance.settings.sse_path) + app = FastAPI(lifespan=mcp_app.lifespan) + app.mount(self.app_config.mcp_mount_path(), mcp_app) + + @app.get("/healthz", response_class=Response) + async def health_check(request: Request) -> Response: + """ + Health check endpoint. - # Override the default behavior - server.force_exit = True # This makes Ctrl+C force exit - try: - asyncio.run(server.serve()) - except KeyboardInterrupt: - log.info("Keyboard interrupt received, forcing immediate exit") - os._exit(0) - except Exception as e: - log.error(f"Exception received, forcing immediate exit: {str(e)}") - os._exit(1) + Args: + request (Request): The incoming HTTP request. + Returns: + Response: A JSON response indicating the server status. + """ + return JSONResponse({"status": "ok"}) + self.log.info( + f"Starting {self.mcp_instance.name} at http://{self.app_config.host()}:{self.app_config.port()}{self.app_config.mcp_mount_path()}{suffix_path}" + ) + # Use Uvicorn's Config and Server classes for more control + config = uvicorn.Config( + app, + host=self.app_config.sysdig_endpoint(), + port=self.app_config.port(), + timeout_graceful_shutdown=1, + log_level=self.app_config.log_level().lower(), + ) + server = uvicorn.Server(config) -def add_tools(mcp: FastMCP) -> None: - """ - Registers the tools to the MCP Server. + # Override the default behavior + server.force_exit = True # This makes Ctrl+C force exit + try: + asyncio.run(server.serve()) + except KeyboardInterrupt: + self.log.info("Keyboard interrupt received, forcing immediate exit") + os._exit(0) + except Exception as e: + self.log.error(f"Exception received, forcing immediate exit: {str(e)}") + os._exit(1) - Args: - mcp (FastMCP): The FastMCP server instance. - """ - # Register the events feed tools - events_feed_tools = EventsFeedTools(app_config) - log.info("Adding Events Feed Tools...") - mcp.add_tool( - events_feed_tools.tool_get_event_info, - name="get_event_info", - description="Retrieve detailed information for a specific security event by its ID", - ) - mcp.add_tool( - events_feed_tools.tool_list_runtime_events, - name="list_runtime_events", - description="List runtime security events from the last given hours, optionally filtered by severity level.", - ) - mcp.add_prompt( - events_feed_tools.investigate_event_prompt, - name="investigate_event", - description="Prompt to investigate a security event based on its severity and time range.", - tags={"analysis", "secure_feeds"}, - ) - mcp.add_tool( - events_feed_tools.tool_get_event_process_tree, - name="get_event_process_tree", - description=( - """ - Retrieve the process tree for a specific security event by its ID. Not every event has a process tree, - so this may return an empty tree. + def add_tools(self) -> None: """ - ), - ) - - # Register the Sysdig Inventory tools - log.info("Adding Sysdig Inventory Tools...") - inventory_tools = InventoryTools(app_config) - mcp.add_tool( - inventory_tools.tool_list_resources, - name="list_resources", - description=( - """ - List inventory resources based on Sysdig Filter Query Language expression with optional pagination.' - """ - ), - ) - mcp.add_tool( - inventory_tools.tool_get_resource, - name="get_resource", - description="Retrieve a single inventory resource by its unique hash identifier.", - ) + Registers the tools to the MCP Server. - # Register the Sysdig Vulnerability Management tools - log.info("Adding Sysdig Vulnerability Management Tools...") - vulnerability_tools = VulnerabilityManagementTools(app_config) - mcp.add_tool( - vulnerability_tools.tool_list_runtime_vulnerabilities, - name="list_runtime_vulnerabilities", - description=( - """ - List runtime vulnerability assets scan results from Sysdig Vulnerability Management API - (Supports pagination using cursor). - """ - ), - ) - mcp.add_tool( - vulnerability_tools.tool_list_accepted_risks, - name="list_accepted_risks", - description="List all accepted risks. Supports filtering and pagination.", - ) - mcp.add_tool( - vulnerability_tools.tool_get_accepted_risk, - name="get_accepted_risk", - description="Retrieve a specific accepted risk by its ID.", - ) - mcp.add_tool( - vulnerability_tools.tool_list_registry_scan_results, - name="list_registry_scan_results", - description="List registry scan results. Supports filtering and pagination.", - ) - mcp.add_tool( - vulnerability_tools.tool_get_vulnerability_policy, - name="get_vulnerability_policy_by_id", - description="Retrieve a specific vulnerability policy by its ID.", - ) - mcp.add_tool( - vulnerability_tools.tool_list_vulnerability_policies, - name="list_vulnerability_policies", - description="List all vulnerability policies. Supports filtering, pagination, and sorting.", - ) - mcp.add_tool( - vulnerability_tools.tool_list_pipeline_scan_results, - name="list_pipeline_scan_results", - description="List pipeline scan results (e.g., built images). Supports pagination and filtering.", - ) - mcp.add_tool( - vulnerability_tools.tool_get_scan_result, - name="get_scan_result", - description="Retrieve a specific scan result (registry/runtime/pipeline).", - ) - mcp.add_prompt( - vulnerability_tools.explore_vulnerabilities_prompt, - name="explore_vulnerabilities", - description="Prompt to explore vulnerabilities based on filters", - tags={"vulnerability", "exploration"}, - ) + Args: + mcp (FastMCP): The FastMCP server instance. + """ + # Register the events feed tools + events_feed_tools = EventsFeedTools(self.app_config) + self.log.info("Adding Events Feed Tools...") + self.mcp_instance.add_tool( + events_feed_tools.tool_get_event_info, + name="get_event_info", + description="Retrieve detailed information for a specific security event by its ID", + ) + self.mcp_instance.add_tool( + events_feed_tools.tool_list_runtime_events, + name="list_runtime_events", + description="List runtime security events from the last given hours, optionally filtered by severity level.", + ) - # Register the Sysdig Sage tools - log.info("Adding Sysdig Sage Tools...") - sysdig_sage_tools = SageTools(app_config) - mcp.add_tool( - sysdig_sage_tools.tool_sage_to_sysql, - name="sysdig_sysql_sage_query", - description=( - """ - Query Sysdig Sage to generate a SysQL query based on a natural language question, - execute it against the Sysdig API, and return the results. + self.mcp_instance.add_prompt( + events_feed_tools.investigate_event_prompt, + name="investigate_event", + description="Prompt to investigate a security event based on its severity and time range.", + tags={"analysis", "secure_feeds"}, + ) + self.mcp_instance.add_tool( + events_feed_tools.tool_get_event_process_tree, + name="get_event_process_tree", + description=( + """ + Retrieve the process tree for a specific security event by its ID. Not every event has a process tree, + so this may return an empty tree. """ - ), - ) + ), + ) - if app_config.transport() == "stdio": - # Register the tools for STDIO transport - cli_scanner_tool = CLIScannerTool(app_config) - log.info("Adding Sysdig CLI Scanner Tool...") - mcp.add_tool( - cli_scanner_tool.run_sysdig_cli_scanner, - name="run_sysdig_cli_scanner", + # Register the Sysdig Inventory tools + self.log.info("Adding Sysdig Inventory Tools...") + inventory_tools = InventoryTools(self.app_config) + self.mcp_instance.add_tool( + inventory_tools.tool_list_resources, + name="list_resources", description=( """ - Run the Sysdig CLI Scanner to analyze a container image or IaC files for vulnerabilities - and posture and misconfigurations. + List inventory resources based on Sysdig Filter Query Language expression with optional pagination.' """ ), ) + self.mcp_instance.add_tool( + inventory_tools.tool_get_resource, + name="get_resource", + description="Retrieve a single inventory resource by its unique hash identifier.", + ) + # Register the Sysdig Vulnerability Management tools + self.log.info("Adding Sysdig Vulnerability Management Tools...") + vulnerability_tools = VulnerabilityManagementTools(self.app_config) + self.mcp_instance.add_tool( + vulnerability_tools.tool_list_runtime_vulnerabilities, + name="list_runtime_vulnerabilities", + description=( + """ + List runtime vulnerability assets scan results from Sysdig Vulnerability Management API + (Supports pagination using cursor). + """ + ), + ) + self.mcp_instance.add_tool( + vulnerability_tools.tool_list_accepted_risks, + name="list_accepted_risks", + description="List all accepted risks. Supports filtering and pagination.", + ) + self.mcp_instance.add_tool( + vulnerability_tools.tool_get_accepted_risk, + name="get_accepted_risk", + description="Retrieve a specific accepted risk by its ID.", + ) + self.mcp_instance.add_tool( + vulnerability_tools.tool_list_registry_scan_results, + name="list_registry_scan_results", + description="List registry scan results. Supports filtering and pagination.", + ) + self.mcp_instance.add_tool( + vulnerability_tools.tool_get_vulnerability_policy, + name="get_vulnerability_policy_by_id", + description="Retrieve a specific vulnerability policy by its ID.", + ) + self.mcp_instance.add_tool( + vulnerability_tools.tool_list_vulnerability_policies, + name="list_vulnerability_policies", + description="List all vulnerability policies. Supports filtering, pagination, and sorting.", + ) + self.mcp_instance.add_tool( + vulnerability_tools.tool_list_pipeline_scan_results, + name="list_pipeline_scan_results", + description="List pipeline scan results (e.g., built images). Supports pagination and filtering.", + ) + self.mcp_instance.add_tool( + vulnerability_tools.tool_get_scan_result, + name="get_scan_result", + description="Retrieve a specific scan result (registry/runtime/pipeline).", + ) + self.mcp_instance.add_prompt( + vulnerability_tools.explore_vulnerabilities_prompt, + name="explore_vulnerabilities", + description="Prompt to explore vulnerabilities based on filters", + tags={"vulnerability", "exploration"}, + ) -def add_resources(mcp: FastMCP) -> None: - """ - Add resources to the MCP server. - Args: - mcp (FastMCP): The FastMCP server instance. - """ - vm_docs = HttpResource( - name="Sysdig Secure Vulnerability Management Overview", - description="Sysdig Secure Vulnerability Management documentation.", - uri="resource://sysdig-secure-vulnerability-management", - url="https://docs.sysdig.com/en/sysdig-secure/vulnerability-management/", - tags=["documentation"], - ) - filter_query_language = TextResource( - name="Sysdig Filter Query Language", - description=( - "Sysdig Filter Query Language documentation. " - "Learn how to filter resources in Sysdig using the Filter Query Language for the API calls." - ), - uri="resource://filter-query-language", - text=( - """ - Query language expressions for filtering results. - The query language allows you to filter resources based on their attributes. - You can use the following operators and functions to build your queries: - - Operators: - - `and` and `not` logical operators - - `=`, `!=` - - `in` - - `contains` and `startsWith` to check partial values of attributes - - `exists` to check if a field exists and not empty + # Register the Sysdig Sage tools + self.log.info("Adding Sysdig Sage Tools...") + sysdig_sage_tools = SageTools(self.app_config) + self.mcp_instance.add_tool( + sysdig_sage_tools.tool_sage_to_sysql, + name="sysdig_sysql_sage_query", + description=( + """ + Query Sysdig Sage to generate a SysQL query based on a natural language question, + execute it against the Sysdig API, and return the results. + """ + ), + ) - Note: - The supported fields are going to depend on the API endpoint you are querying. - Check the description of each tool for the supported fields. - - Examples: - - in ("example") and = "example2" - - >= "3" - """ - ), - tags=["query-language", "documentation"], - ) - mcp.add_resource(vm_docs) - mcp.add_resource(filter_query_language) + if self.app_config.transport() == "stdio": + # Register the tools for STDIO transport + cli_scanner_tool = CLIScannerTool(self.app_config) + self.log.info("Adding Sysdig CLI Scanner Tool...") + self.mcp_instance.add_tool( + cli_scanner_tool.run_sysdig_cli_scanner, + name="run_sysdig_cli_scanner", + description=( + """ + Run the Sysdig CLI Scanner to analyze a container image or IaC files for vulnerabilities + and posture and misconfigurations. + """ + ), + ) + + + def add_resources(self) -> None: + """ + Add resources to the MCP server. + Args: + mcp (FastMCP): The FastMCP server instance. + """ + vm_docs = HttpResource( + name="Sysdig Secure Vulnerability Management Overview", + description="Sysdig Secure Vulnerability Management documentation.", + uri="resource://sysdig-secure-vulnerability-management", + url="https://docs.sysdig.com/en/sysdig-secure/vulnerability-management/", + tags=["documentation"], + ) + filter_query_language = TextResource( + name="Sysdig Filter Query Language", + description=( + "Sysdig Filter Query Language documentation. " + "Learn how to filter resources in Sysdig using the Filter Query Language for the API calls." + ), + uri="resource://filter-query-language", + text=( + """ + Query language expressions for filtering results. + The query language allows you to filter resources based on their attributes. + You can use the following operators and functions to build your queries: + + Operators: + - `and` and `not` logical operators + - `=`, `!=` + - `in` + - `contains` and `startsWith` to check partial values of attributes + - `exists` to check if a field exists and not empty + + Note: + The supported fields are going to depend on the API endpoint you are querying. + Check the description of each tool for the supported fields. + + Examples: + - in ("example") and = "example2" + - >= "3" + """ + ), + tags=["query-language", "documentation"], + ) + self.mcp_instance.add_resource(vm_docs) + self.mcp_instance.add_resource(filter_query_language) From 0ca3eba3fd0f6fca56cf50c3af63b143e47a1b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Tue, 26 Aug 2025 17:40:38 +0200 Subject: [PATCH 06/11] feat: Upgrade dependencies and add OAuth Middleware --- README.md | 4 +- pyproject.toml | 8 +- tests/events_feed_test.py | 20 +- tools/events_feed/tool.py | 75 +-- tools/inventory/tool.py | 42 +- tools/sysdig_sage/tool.py | 45 +- tools/vulnerability_management/tool.py | 74 ++- utils/app_config.py | 61 ++- utils/auth/__init__.py | 0 utils/auth/auth_config.py | 32 ++ utils/auth/middleware/auth.py | 157 +++++++ utils/mcp_server.py | 126 ++--- utils/middleware/__init__.py | 1 - utils/middleware/auth.py | 88 ---- utils/query_helpers.py | 4 +- utils/sysdig/api.py | 51 -- utils/sysdig/client_config.py | 41 +- utils/sysdig/helpers.py | 12 + ...old_sysdig_api.py => legacy_sysdig_api.py} | 17 +- uv.lock | 439 ++++++++++++++++-- 20 files changed, 880 insertions(+), 417 deletions(-) create mode 100644 utils/auth/__init__.py create mode 100644 utils/auth/auth_config.py create mode 100644 utils/auth/middleware/auth.py delete mode 100644 utils/middleware/__init__.py delete mode 100644 utils/middleware/auth.py delete mode 100644 utils/sysdig/api.py create mode 100644 utils/sysdig/helpers.py rename utils/sysdig/{old_sysdig_api.py => legacy_sysdig_api.py} (83%) diff --git a/README.md b/README.md index 5d69334..7fbbebd 100644 --- a/README.md +++ b/README.md @@ -172,8 +172,8 @@ The following environment variables are **required** for configuring the Sysdig You can also set the following variables to override the default configuration: -- `SYSDIG_MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`. -- `SYSDIG_MCP_MOUNT_PATH`: The URL prefix for the Streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server` +- `MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`. +- `MCP_MOUNT_PATH`: The URL prefix for the Streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server` - `LOGLEVEL`: Log Level of the application (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Defaults to: `INFO` - `SYSDIG_MCP_LISTENING_PORT`: The port for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `8080` - `SYSDIG_MCP_LISTENING_HOST`: The host for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `localhost` diff --git a/pyproject.toml b/pyproject.toml index 9c650cf..a073e31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,20 @@ [project] name = "sysdig-mcp-server" -version = "0.1.5" +version = "0.2.0" description = "Sysdig MCP Server" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "mcp[cli]==1.10.0", + "mcp[cli]==1.12.4", "python-dotenv>=1.1.0", "pyyaml==6.0.2", "sqlalchemy==2.0.36", "sqlmodel==0.0.22", - "sysdig-sdk @ git+https://github.com/sysdiglabs/sysdig-sdk-python@e9b0d336c2f617f3bbd752416860f84eed160c41", + "sysdig-sdk-python @ git+https://github.com/sysdiglabs/sysdig-sdk-python@597285143188019cd0e86fde43f94b1139f5441d", "dask==2025.4.1", "oauthlib==3.2.2", "fastapi==0.116.1", - "fastmcp==2.5.1", + "fastmcp==2.11.3", "requests", ] diff --git a/tests/events_feed_test.py b/tests/events_feed_test.py index 1b738ff..0d299dc 100644 --- a/tests/events_feed_test.py +++ b/tests/events_feed_test.py @@ -7,7 +7,10 @@ from utils.app_config import AppConfig from .conftest import util_load_json from unittest.mock import MagicMock, AsyncMock, create_autospec +from sysdig_client.api import SecureEventsApi import os +from fastmcp.server.context import Context +from fastmcp.server import FastMCP # Get the absolute path of the current module file module_path = os.path.abspath(__file__) @@ -40,8 +43,22 @@ def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds tools_client = EventsFeedTools(app_config=mock_app_config()) + ctx = Context(FastMCP()) + + # Seed FastMCP Context state with mocked API instances expected by the tools + secure_events_api = MagicMock(spec=SecureEventsApi) + # The tool returns whatever the SDK method returns; make it be our mocked HTTP response + secure_events_api.get_event_v1_without_preload_content.return_value = mock_success_response.return_value + + api_instances = { + "secure_events": secure_events_api, + # Not used by this test, but present in real runtime; keep as empty mock to avoid KeyErrors elsewhere + "legacy_sysdig_api": MagicMock(), + } + ctx.set_state("api_instances", api_instances) + # Pass the mocked Context object - result: dict = tools_client.tool_get_event_info("12345") + result: dict = tools_client.tool_get_event_info(ctx=ctx, event_id="12345") results: dict = result["results"] assert result.get("status_code") == HTTPStatus.OK @@ -49,3 +66,4 @@ def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds assert results.get("results").get("content", {}).get("ruleName") == "Fileless execution via memfd_create" assert results.get("results").get("id") == "123456789012" assert results.get("results").get("content", {}).get("type") == "workloadRuntimeDetection" + print("Event info retrieved successfully.") diff --git a/tools/events_feed/tool.py b/tools/events_feed/tool.py index 77aafaf..929fcd4 100644 --- a/tools/events_feed/tool.py +++ b/tools/events_feed/tool.py @@ -8,20 +8,17 @@ import logging import os import time -from datetime import datetime -from typing import Optional, Annotated, Any, Dict +import datetime +from typing import Optional, Annotated from pydantic import Field -from sysdig_client import ApiException from fastmcp.prompts.prompt import PromptMessage, TextContent from fastmcp.exceptions import ToolError -from starlette.requests import Request +from fastmcp.server.context import Context +from sysdig_client import ApiException from sysdig_client.api import SecureEventsApi -from utils.sysdig.old_sysdig_api import OldSysdigApi -from fastmcp.server.dependencies import get_http_request +from utils.sysdig.legacy_sysdig_api import LegacySysdigApi from utils.query_helpers import create_standard_response -from utils.sysdig.client_config import get_configuration from utils.app_config import AppConfig -from utils.sysdig.api import initialize_api_client class EventsFeedTools: @@ -32,55 +29,23 @@ class EventsFeedTools: def __init__(self, app_config: AppConfig): self.app_config = app_config - logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) self.log = logging.getLogger(__name__) - def init_client(self, old_api: bool = False) -> SecureEventsApi | OldSysdigApi: - """ - Initializes the SecureEventsApi client from the request state. - If the request does not have the API client initialized, it will create a new instance - using the Sysdig Secure token and host from the environment variables. - Args: - old_api (bool): If True, initializes the OldSysdigApi client instead of SecureEventsApi. - Returns: - SecureEventsApi | OldSysdigApi: An instance of the SecureEventsApi or OldSysdigApi client. - """ - secure_events_api: SecureEventsApi = None - old_sysdig_api: OldSysdigApi = None - transport = self.app_config.transport() - if transport in ["streamable-http", "sse"]: - # Try to get the HTTP request - self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") - request: Request = get_http_request() - secure_events_api = request.state.api_instances["secure_events"] - old_sysdig_api = request.state.api_instances["old_sysdig_api"] - else: - # If running in STDIO mode, we need to initialize the API client from environment variables - self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") - cfg = get_configuration() - api_client = initialize_api_client(cfg) - secure_events_api = SecureEventsApi(api_client) - # Initialize the old Sysdig API client for process tree requests - old_cfg = get_configuration(old_api=True) - old_sysdig_api = initialize_api_client(old_cfg) - old_sysdig_api = OldSysdigApi(old_sysdig_api) - - if old_api: - return old_sysdig_api - return secure_events_api - - def tool_get_event_info(self, event_id: str) -> dict: + def tool_get_event_info(self, ctx: Context, event_id: str) -> dict: """ Retrieves detailed information for a specific security event. Args: + ctx (Context): Context to use. event_id (str): The unique identifier of the security event. Returns: Event: The Event object containing detailed information about the specified event. """ # Init of the sysdig client - secure_events_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + secure_events_api: SecureEventsApi = api_instances.get("secure_events") + try: # Get the HTTP request start_time = time.time() @@ -98,6 +63,7 @@ def tool_get_event_info(self, event_id: str) -> dict: def tool_list_runtime_events( self, + ctx: Context, cursor: Optional[str] = None, scope_hours: int = 1, limit: int = 50, @@ -136,6 +102,7 @@ def tool_list_runtime_events( cluster name, or an optional filter expression. Args: + ctx (Context): Context to use. cursor (Optional[str]): Cursor for pagination. scope_hours (int): Number of hours back from now to include events. Defaults to 1. limit (int): Maximum number of events to return. Defaults to 50. @@ -144,7 +111,9 @@ def tool_list_runtime_events( Returns: dict: A dictionary containing the results of the runtime events query, including pagination information. """ - secure_events_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + secure_events_api: SecureEventsApi = api_instances.get("secure_events") + start_time = time.time() # Compute time window now_ns = time.time_ns() @@ -176,7 +145,7 @@ def tool_list_runtime_events( # A tool to retrieve all the process-tree information for a specific event.Add commentMore actions - def tool_get_event_process_tree(self, event_id: str) -> dict: + def tool_get_event_process_tree(self, ctx: Context, event_id: str) -> dict: """ Retrieves the process tree for a specific security event. Not every event has a process tree, so this may return an empty tree. @@ -191,12 +160,14 @@ def tool_get_event_process_tree(self, event_id: str) -> dict: ToolError: If there is an error constructing or processing the response. """ try: + api_instances: dict = ctx.get_state("api_instances") + legacy_api_client: LegacySysdigApi = api_instances.get("legacy_sysdig_api") + start_time = time.time() # Get process tree branches - old_api_client = self.init_client(old_api=True) - branches = old_api_client.request_process_tree_branches(event_id) + branches = legacy_api_client.request_process_tree_branches(event_id) # Get process tree - tree = old_api_client.request_process_tree_trees(event_id) + tree = legacy_api_client.request_process_tree_trees(event_id) # Parse the response (tolerates empty bodies) branches_std = create_standard_response(results=branches, execution_time_ms=(time.time() - start_time) * 1000) @@ -209,7 +180,7 @@ def tool_get_event_process_tree(self, event_id: str) -> dict: "tree": tree_std.get("results", {}), "metadata": { "execution_time_ms": execution_time, - "timestamp": datetime.utcnow().isoformat() + "Z", + "timestamp": datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z"), }, } @@ -222,7 +193,7 @@ def tool_get_event_process_tree(self, event_id: str) -> dict: "tree": {}, "metadata": { "execution_time_ms": (time.time() - start_time) * 1000, - "timestamp": datetime.utcnow().isoformat() + "Z", + "timestamp": datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z"), "note": "Process tree not available for this event" }, } diff --git a/tools/inventory/tool.py b/tools/inventory/tool.py index d96f1c5..2cc217b 100644 --- a/tools/inventory/tool.py +++ b/tools/inventory/tool.py @@ -5,14 +5,12 @@ import logging import time from typing import Annotated + +from fastmcp import Context from pydantic import Field -from fastmcp.server.dependencies import get_http_request from fastmcp.exceptions import ToolError -from starlette.requests import Request from sysdig_client.api import InventoryApi -from utils.sysdig.client_config import get_configuration from utils.app_config import AppConfig -from utils.sysdig.api import initialize_api_client from utils.query_helpers import create_standard_response @@ -24,34 +22,11 @@ class InventoryTools: def __init__(self, app_config: AppConfig): self.app_config = app_config # Configure logging - logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) self.log = logging.getLogger(__name__) - def init_client(self) -> InventoryApi: - """ - Initializes the InventoryApi client from the request state. - If the request does not have the API client initialized, it will create a new instance - using the Sysdig Secure token and host from the environment variables. - Returns: - InventoryApi: An instance of the InventoryApi client. - """ - inventory_api: InventoryApi = None - transport = self.app_config.transport() - if transport in ["streamable-http", "sse"]: - # Try to get the HTTP request - self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") - request: Request = get_http_request() - inventory_api = request.state.api_instances["inventory"] - else: - # If running in STDIO mode, we need to initialize the API client from environment variables - self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") - cfg = get_configuration() - api_client = initialize_api_client(cfg) - inventory_api = InventoryApi(api_client) - return inventory_api - def tool_list_resources( self, + ctx: Context, filter_exp: Annotated[ str, Field( @@ -141,6 +116,7 @@ def tool_list_resources( List inventory items based on a filter expression, with optional pagination. Args: + ctx (Context): A context object containing configuration information. filter_exp (str): Sysdig query filter expression to filter inventory resources. Use the resource://filter-query-language to get the expected filter expression format. Supports operators: =, !=, in, exists, contains, startsWith. @@ -162,7 +138,9 @@ def tool_list_resources( Or a dict containing an error message if the call fails. """ try: - inventory_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + inventory_api: InventoryApi = api_instances.get("inventory") + start_time = time.time() api_response = inventory_api.get_resources_without_preload_content( @@ -180,19 +158,23 @@ def tool_list_resources( def tool_get_resource( self, + ctx: Context, resource_hash: Annotated[str, Field(description="The unique hash of the inventory resource to retrieve.")], ) -> dict: """ Fetch a specific inventory resource by hash. Args: + ctx (Context): A context object containing configuration information. resource_hash (str): The hash identifier of the resource. Returns: dict: A dictionary containing the details of the requested inventory resource. """ try: - inventory_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + inventory_api: InventoryApi = api_instances.get("inventory") + start_time = time.time() api_response = inventory_api.get_resource_without_preload_content(hash=resource_hash) diff --git a/tools/sysdig_sage/tool.py b/tools/sysdig_sage/tool.py index 5c567a5..dc6c062 100644 --- a/tools/sysdig_sage/tool.py +++ b/tools/sysdig_sage/tool.py @@ -5,16 +5,12 @@ """ import logging -import os import time -from typing import Any, Dict + +from fastmcp.server.context import Context from fastmcp.exceptions import ToolError -from utils.sysdig.old_sysdig_api import OldSysdigApi -from starlette.requests import Request -from fastmcp.server.dependencies import get_http_request -from utils.sysdig.client_config import get_configuration +from utils.sysdig.legacy_sysdig_api import LegacySysdigApi from utils.app_config import AppConfig -from utils.sysdig.api import initialize_api_client from utils.query_helpers import create_standard_response @@ -26,38 +22,15 @@ class SageTools: """ def __init__(self, app_config: AppConfig): self.app_config = app_config - logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) self.log = logging.getLogger(__name__) - def init_client(self) -> OldSysdigApi: - """ - Initializes the OldSysdigApi client from the request state. - If the request does not have the API client initialized, it will create a new instance - using the Sysdig Secure token and host from the environment variables. - Returns: - OldSysdigApi: An instance of the OldSysdigApi client. - """ - old_sysdig_api: OldSysdigApi = None - transport = self.app_config.transport() - if transport in ["streamable-http", "sse"]: - # Try to get the HTTP request - self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") - request: Request = get_http_request() - old_sysdig_api = request.state.api_instances["old_sysdig_api"] - else: - # If running in STDIO mode, we need to initialize the API client from environment variables - self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") - cfg = get_configuration(old_api=True) - api_client = initialize_api_client(cfg) - old_sysdig_api = OldSysdigApi(api_client) - return old_sysdig_api - - async def tool_sage_to_sysql(self, question: str) -> dict: + async def tool_sage_to_sysql(self, ctx: Context, question: str) -> dict: """ Queries Sysdig Sage with a natural language question, retrieves a SysQL query, executes it against the Sysdig API, and returns the results. Args: + ctx (Context): A context object containing configuration information. question (str): A natural language question to send to Sage. Returns: @@ -74,8 +47,10 @@ async def tool_sage_to_sysql(self, question: str) -> dict: # 1) Generate SysQL query try: start_time = time.time() - old_sysdig_api = self.init_client() - sysql_response = await old_sysdig_api.generate_sysql_query(question) + api_instances: dict = ctx.get_state("api_instances") + legacy_api_client: LegacySysdigApi = api_instances.get("legacy_sysdig_api") + + sysql_response = await legacy_api_client.generate_sysql_query(question) if sysql_response.status > 299: raise ToolError(f"Sysdig Sage returned an error: {sysql_response.status} - {sysql_response.data}") except ToolError as e: @@ -89,7 +64,7 @@ async def tool_sage_to_sysql(self, question: str) -> dict: # 2) Execute generated SysQL query try: self.log.debug(f"Executing SysQL query: {sysql_query}") - results = old_sysdig_api.execute_sysql_query(sysql_query) + results = legacy_api_client.execute_sysql_query(sysql_query) execution_time = (time.time() - start_time) * 1000 self.log.debug(f"SysQL query executed in {execution_time} ms") response = create_standard_response( diff --git a/tools/vulnerability_management/tool.py b/tools/vulnerability_management/tool.py index 21bad08..c5747ab 100644 --- a/tools/vulnerability_management/tool.py +++ b/tools/vulnerability_management/tool.py @@ -7,16 +7,13 @@ import time from typing import List, Optional, Literal, Annotated from pydantic import Field +from sysdig_client import VulnerabilityManagementApi from sysdig_client.models.scan_result_response import ScanResultResponse from sysdig_client.models.get_policy_response import GetPolicyResponse from fastmcp.prompts.prompt import PromptMessage, TextContent from fastmcp.exceptions import ToolError -from starlette.requests import Request -from sysdig_client.api import VulnerabilityManagementApi -from fastmcp.server.dependencies import get_http_request -from utils.sysdig.client_config import get_configuration +from fastmcp.server.context import Context from utils.app_config import AppConfig -from utils.sysdig.api import initialize_api_client from utils.query_helpers import create_standard_response @@ -31,34 +28,12 @@ class VulnerabilityManagementTools: def __init__(self, app_config: AppConfig): self.app_config = app_config # Configure logging - logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) self.log = logging.getLogger(__name__) - def init_client(self) -> VulnerabilityManagementApi: - """ - Initializes the VulnerabilityManagementApi client from the request state. - If the request does not have the API client initialized, it will create a new instance - using the Sysdig Secure token and host from the environment variables. - Returns: - VulnerabilityManagementApi: An instance of the VulnerabilityManagementApi client. - """ - vulnerability_management_api: VulnerabilityManagementApi = None - transport = self.app_config.transport() - if transport in ["streamable-http", "sse"]: - # Try to get the HTTP request - self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.") - request: Request = get_http_request() - vulnerability_management_api = request.state.api_instances["vulnerability_management"] - else: - # If running in STDIO mode, we need to initialize the API client from environment variables - self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.") - cfg = get_configuration() - api_client = initialize_api_client(cfg) - vulnerability_management_api = VulnerabilityManagementApi(api_client) - return vulnerability_management_api def tool_list_runtime_vulnerabilities( self, + ctx: Context, cursor: Annotated[Optional[str], Field(description="Cursor for pagination. If None, returns the first page.")] = None, filter: Annotated[ Optional[str], @@ -124,6 +99,7 @@ def tool_list_runtime_vulnerabilities( Retrieves a list of assets with runtime vulnerabilities scan results. Args: + ctx (Context): A context object containing configuration information. cursor (Optional[str]): Cursor for pagination. If None, returns the first page. filter (Optional[str]): Sysdig Secure query filter expression to filter runtime vulnerability scan results. Use the resource://filter-query-language to get the expected filter expression format. @@ -146,7 +122,8 @@ def tool_list_runtime_vulnerabilities( - execution_time_ms (float): Execution duration in milliseconds. """ try: - vulnerability_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") # Record start time for execution duration start_time = time.time() api_response = vulnerability_api.scanner_api_service_list_runtime_results_without_preload_content( @@ -167,6 +144,7 @@ def tool_list_runtime_vulnerabilities( def tool_list_accepted_risks( self, + ctx : Context, filter: Optional[str] = None, limit: int = 50, cursor: Optional[str] = None, @@ -177,6 +155,7 @@ def tool_list_accepted_risks( Retrieve a paginated list of accepted vulnerability risks. Args: + ctx (Context): A context object containing configuration information. filter (Optional[str]): Query expression to filter accepted risks. limit (int): Maximum number of risks to return. cursor (Optional[str]): Pagination cursor. If None, returns the first page. @@ -187,7 +166,8 @@ def tool_list_accepted_risks( dict: The API response as a dictionary, or an error dict on failure. """ try: - vulnerability_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") start_time = time.time() api_response = vulnerability_api.get_accepted_risks_v1_without_preload_content( filter=filter, limit=limit, cursor=cursor, sort=sort, order=order @@ -205,18 +185,21 @@ def tool_list_accepted_risks( self.log.error(f"Exception when calling VulnerabilityManagementApi->get_accepted_risks_v1: {e}") raise e - def tool_get_accepted_risk(self, accepted_risk_id: str) -> dict: + def tool_get_accepted_risk(self, ctx: Context, accepted_risk_id: str) -> dict: """ Retrieve details of a specific accepted risk by its ID. Args: + ctx (Context): A context object containing configuration information. accepted_risk_id (str): The ID of the accepted risk to retrieve. Returns: dict: The accepted risk details as a dictionary, or an error dict on failure. """ try: - vulnerability_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + response = vulnerability_api.get_accepted_risk_v1(accepted_risk_id) return response.model_dump_json() if hasattr(response, "dict") else response except ToolError as e: @@ -225,6 +208,7 @@ def tool_get_accepted_risk(self, accepted_risk_id: str) -> dict: def tool_list_registry_scan_results( self, + ctx: Context, filter: Annotated[ Optional[str], Field( @@ -269,7 +253,9 @@ def tool_list_registry_scan_results( dict: The registry scan results as a dictionary, or an error dict on failure. """ try: - vulnerability_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + start_time = time.time() api_response = vulnerability_api.scanner_api_service_list_registry_results_without_preload_content( filter=filter, limit=limit, cursor=cursor @@ -287,7 +273,7 @@ def tool_list_registry_scan_results( raise e def tool_get_vulnerability_policy( - self, policy_id: Annotated[int, Field(description="The unique ID of the vulnerability policy to retrieve.")] + self, ctx: Context, policy_id: Annotated[int, Field(description="The unique ID of the vulnerability policy to retrieve.")] ) -> GetPolicyResponse | dict: """ Retrieve a specific vulnerability policy by its ID. @@ -300,7 +286,9 @@ def tool_get_vulnerability_policy( dict: An error dict on failure. """ try: - vulnerability_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + response: GetPolicyResponse = vulnerability_api.secure_vulnerability_v1_policies_policy_id_get(policy_id) return response.model_dump_json() if hasattr(response, "dict") else response except ToolError as e: @@ -311,6 +299,7 @@ def tool_get_vulnerability_policy( def tool_list_vulnerability_policies( self, + ctx: Context, cursor: Optional[str] = None, limit: int = 50, name: Optional[str] = None, @@ -330,7 +319,8 @@ def tool_list_vulnerability_policies( """ start_time = time.time() try: - vulnerability_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") api_response = vulnerability_api.secure_vulnerability_v1_policies_get_without_preload_content( cursor=cursor, limit=limit, name=name, stages=stages ) @@ -352,6 +342,7 @@ def tool_list_vulnerability_policies( def tool_list_pipeline_scan_results( self, + ctx: Context, cursor: Annotated[Optional[str], Field(description="Cursor for pagination. If None, returns the first page.")] = None, filter: Annotated[ Optional[str], @@ -378,6 +369,7 @@ def tool_list_pipeline_scan_results( Retrieve a paginated list of pipeline vulnerability scan results. Args: + ctx (Context): A context object containing configuration information. cursor (Optional[str]): Cursor for pagination. If None, returns the first page. filter (Optional[str]): Sysdig Secure query filter expression to filter vulnerability. Use the resource://filter-query-language to get the expected filter expression format. @@ -400,7 +392,9 @@ def tool_list_pipeline_scan_results( """ start_time = time.time() try: - vulnerability_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + api_response = vulnerability_api.secure_vulnerability_v1_pipeline_results_get_without_preload_content( cursor=cursor, filter=filter, limit=limit ) @@ -418,18 +412,20 @@ def tool_list_pipeline_scan_results( ) raise e - def tool_get_scan_result(self, scan_id: str) -> dict: + def tool_get_scan_result(self, ctx: Context, scan_id: str) -> dict: """ Retrieve the result of a specific scan. Args: + ctx (Context): A context object containing configuration information. scan_id (str): The ID of the scan. Returns: dict: ScanResultResponse as dict, or {"error": ...}. """ try: - vulnerability_api = self.init_client() + api_instances: dict = ctx.get_state("api_instances") + vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") resp: ScanResultResponse = vulnerability_api.secure_vulnerability_v1_results_result_id_get(scan_id) return resp.model_dump_json() if hasattr(resp, "dict") else resp except ToolError as e: diff --git a/utils/app_config.py b/utils/app_config.py index 0404047..0b81e4a 100644 --- a/utils/app_config.py +++ b/utils/app_config.py @@ -1,10 +1,10 @@ """ -Utility functions to load and manage the application configuration from a YAML file. -It will load a singleton configuration object that can be accessed throughout the application. +Utility functions to load and manage the application configuration. +It will load a single configuration class object that can be accessed throughout the application. """ import os -from typing import Optional +from typing import Optional, List # app_config singleton _app_config: Optional[dict] = None @@ -54,7 +54,7 @@ def transport(self) -> str: Returns: str: The transport protocol (e.g., "stdio", "streamable-http", or "sse"). """ - transport = os.environ.get("SYSDIG_TRANSPORT", "stdio").lower() + transport = os.environ.get("MCP_TRANSPORT", "stdio").lower() if transport not in ("stdio", "streamable-http", "sse"): raise ValueError("Invalid transport protocol. Valid values are: stdio, streamable-http, sse.") @@ -78,7 +78,7 @@ def port(self) -> int: Returns: int: The MCP server port. """ - return os.environ.get("SYSDIG_MCP_LISTENING_PORT", "8080") + return int(os.environ.get("SYSDIG_MCP_LISTENING_PORT", "8080")) # def host(self) -> str: @@ -100,6 +100,57 @@ def mcp_mount_path(self) -> str: """ return os.environ.get("MCP_MOUNT_PATH", "/sysdig-mcp-server") + def oauth_jwks_uri(self) -> str: + """ + Get the string value for the remote OAuth JWKS URI. + Returns: + str: The OAuth JWKS URI. + """ + return os.environ.get("OAUTH_JWKS_URI", "") + + def oauth_issuer(self) -> str: + """ + Get the string value for the remote OAuth Issuer. + Returns: + str: The OAuth Issuer. + """ + return os.environ.get("OAUTH_ISSUER", "") + + def oauth_audience(self) -> str: + """ + Get the string value for the remote OAuth Audience. + Returns: + str: The OAuth Audience. + """ + return os.environ.get("OAUTH_AUDIENCE", "") + + def oauth_required_scopes(self) -> List[str]: + """ + Get the list of required scopes for the remote OAuth. + Returns: + List[str]: The list of scopes. + """ + raw = os.environ.get("OAUTH_REQUIRED_SCOPES", "") + if not raw: + return [] + # Support comma-separated scopes in env var + return [s.strip() for s in raw.split(",") if s.strip()] + + def oauth_enabled(self) -> bool: + """ + Check to enable the remote OAuth. + Returns: + bool: Whether the remote OAuth should be enabled or not. + """ + return os.environ.get("OAUTH_ENABLED", "false").lower() == "true" + + def oauth_resource_server_uri(self) -> str: + """ + Get the string value for the remote OAuth Server Resource URI. + Returns: + str: The OAuth Resource URI. + """ + return os.environ.get("OAUTH_RESOURCE_SERVER_URI", "[]") def get_app_config() -> AppConfig: """ diff --git a/utils/auth/__init__.py b/utils/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/auth/auth_config.py b/utils/auth/auth_config.py new file mode 100644 index 0000000..f1daa6c --- /dev/null +++ b/utils/auth/auth_config.py @@ -0,0 +1,32 @@ +""" +Auth configuration for the MCP server. +""" +from typing import Optional + +from fastmcp.server.auth import RemoteAuthProvider +from fastmcp.server.auth.providers.jwt import JWTVerifier +from pydantic import AnyHttpUrl +from utils.app_config import AppConfig + + +def obtain_remote_auth_provider(app_config: AppConfig) -> RemoteAuthProvider: + # Configure token validation for your identity provider + + # Create the remote auth provider + remote_auth_provider: Optional[RemoteAuthProvider] = None + + if app_config.oauth_enabled(): + token_verifier = JWTVerifier( + jwks_uri=app_config.oauth_jwks_uri(), + issuer=app_config.oauth_issuer(), + audience=app_config.oauth_audience(), + required_scopes=app_config.oauth_required_scopes() + ) + + remote_auth_provider = RemoteAuthProvider( + token_verifier=token_verifier, + authorization_servers=[AnyHttpUrl(app_config.oauth_issuer())], + resource_server_url=AnyHttpUrl(app_config.oauth_resource_server_uri()), + ) + + return remote_auth_provider \ No newline at end of file diff --git a/utils/auth/middleware/auth.py b/utils/auth/middleware/auth.py new file mode 100644 index 0000000..e051f47 --- /dev/null +++ b/utils/auth/middleware/auth.py @@ -0,0 +1,157 @@ +""" +Custom middleware for access control and initialization of Sysdig API clients. +""" + +import logging +import os +from starlette.requests import Request +from fastmcp.server.middleware import Middleware, MiddlewareContext, CallNext +from utils.sysdig.helpers import TOOL_PERMISSIONS +from fastmcp.tools import Tool +from fastmcp.server.dependencies import get_http_request +from utils.sysdig.client_config import initialize_api_client, get_sysdig_api_instances +from utils.sysdig.client_config import get_configuration +from utils.sysdig.legacy_sysdig_api import LegacySysdigApi +from utils.app_config import AppConfig + +# Set up logging +log = logging.getLogger(__name__) + +# TODO: Define the correct message notifications +INIT_NOTIFICATIONS = ["notifications/initialized", "tools/list", "tools/call"] + + +def _get_permissions(context: MiddlewareContext) -> None: + """ + Get the permissions for the current user/team based on the Bearer token and set them in the context. + Args: + context (MiddlewareContext): The middleware context. + Raises: + Exception: If fetching permissions fails. + """ + try: + api_instances: dict = context.fastmcp_context.get_state("api_instances") + legacy_api_client: LegacySysdigApi = api_instances.get("legacy_sysdig_api") + response = legacy_api_client.get_me_permissions() + if response.status != 200: + log.error(f"Error fetching permissions: Status {response.status} {legacy_api_client.api_client.configuration.host}") + raise Exception("Failed to fetch user permissions. Check your current Token and permissions.") + context.fastmcp_context.set_state("permissions", response.json().get("permissions", [])) + except Exception as e: + log.error(f"Error fetching permissions: {e}") + raise + + +async def allowed_tool(context: MiddlewareContext, tool: Tool) -> bool: + """ + Check if the user has permission to access a specific tool. + Args: + context (MiddlewareContext): The middleware context. + tool (str): The tool to check permissions for. + Returns: + bool: True if the user has permission to access the tool, False otherwise. + """ + permissions = context.fastmcp_context.get_state("permissions") + if permissions is None: + # Try to fetch permissions once + _get_permissions(context) + permissions = context.fastmcp_context.get_state("permissions") + for tag in tool.tags: + if tag in TOOL_PERMISSIONS: + tool_permissions = TOOL_PERMISSIONS[tag] + if all(permission in permissions for permission in tool_permissions): + return True + log.warning(f"User does not have permission to access tool: {tool.name}") + return False + + +async def _save_api_instances(context: MiddlewareContext, app_config: AppConfig) -> None: + """ + This method initializes the Sysdig API client and saves the instances to the FastMCP context per request. + Based on the transport method, it extracts the Bearer token from the Authorization + header or from the environment variables. + Raises: + Exception: If the Authorization header or required env vars are missing. + """ + cfg = None + legacy_cfg = None + + if context.fastmcp_context.get_state("transport_method") in ["streamable-http", "sse"]: + request: Request = get_http_request() + # Check for the Authorization header + auth_header = request.headers.get("Authorization") + if not auth_header or not auth_header.startswith("Bearer "): + raise Exception("Missing or invalid Authorization header") + + # Extract relevant information from the request headers + token = auth_header.removeprefix("Bearer ").strip() + base_url = request.headers.get("X-Sysdig-Host", app_config.sysdig_endpoint()) or str(request.base_url) + log.info(f"Using Sysdig API base URL: {base_url}") + + cfg = get_configuration(app_config, token, base_url) + legacy_cfg = get_configuration(app_config, token, base_url, old_api=True) + else: + # If running in STDIO mode, we initialize the API client from environment variables + cfg = get_configuration(app_config) + legacy_cfg = get_configuration(app_config, old_api=True) + + api_client = initialize_api_client(cfg) + legacy_sysdig_api = initialize_api_client(legacy_cfg) + api_instances = get_sysdig_api_instances(api_client) + # api_instances have a dictionary of all the Sysdig API instances needed to be accessed in every request + _legacy_sysdig_api = LegacySysdigApi(legacy_sysdig_api) + api_instances["legacy_sysdig_api"] = _legacy_sysdig_api + # Save the API instances to the context + log.debug("Saving API instances to the context.") + context.fastmcp_context.set_state("api_instances", api_instances) + + +class CustomMiddleware(Middleware): + """ + Custom middleware for filtering tool listings and performing authentication. + """ + + def __init__(self, app_config: AppConfig): + self.app_config = app_config + + # TODO: Evaluate if init the clients and perform auth only on the `notifications/initialized` event + async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> CallNext: + """ + Handle incoming messages and initialize the Sysdig API client if needed. + Returns: + CallNext: The next middleware or route handler to call. + Raises: + Exception: If a problem occurs while initializing the API clients. + """ + # Save transport method in context + if not context.fastmcp_context.get_state("transport_method"): + transport_method = os.environ.get("MCP_TRANSPORT", self.app_config.transport()).lower() + context.fastmcp_context.set_state("transport_method", transport_method) + try: + # TODO: Currently not able to get the notifications/initialized only that should be the method that initializes + # the API instances for the whole session, we need to check if its possible + if context.method in INIT_NOTIFICATIONS: + await _save_api_instances(context, self.app_config) + + return await call_next(context) + except Exception as error: + raise Exception(f"Error in {context.method}: {type(error).__name__}: {error}") + + async def on_list_tools(self, context: MiddlewareContext, call_next: CallNext) -> list[Tool]: + """ + Handle listing of tools by checking permissions for the current user. + Returns: + list[Tool]: A list of tools that the user is allowed to access. + Raises: + Exception: If a problem occurs while checking tool permissions. + """ + result = await call_next(context) + try: + filtered_tools = [tool for tool in result if await allowed_tool(context, tool)] + if not filtered_tools: + raise Exception("No allowed tools found for the user.") + except Exception as e: + log.error(f"Error filtering tools: {e}") + raise + # Return modified list + return filtered_tools \ No newline at end of file diff --git a/utils/mcp_server.py b/utils/mcp_server.py index 3e7eae1..501cea2 100644 --- a/utils/mcp_server.py +++ b/utils/mcp_server.py @@ -8,13 +8,16 @@ import asyncio from typing import Optional import uvicorn +from fastmcp.prompts import Prompt from starlette.requests import Request from starlette.responses import JSONResponse, Response from typing_extensions import Literal from fastapi import FastAPI -from fastmcp import FastMCP +from fastmcp import FastMCP, Settings from fastmcp.resources import HttpResource, TextResource -from utils.middleware.auth import create_auth_middleware + +from utils.auth.auth_config import obtain_remote_auth_provider +from utils.auth.middleware.auth import CustomMiddleware from starlette.middleware import Middleware from tools.events_feed.tool import EventsFeedTools from tools.inventory.tool import InventoryTools @@ -30,18 +33,16 @@ class SysdigMCPServer: def __init__(self, app_config: AppConfig): self.app_config = app_config # Set up logging - logging.basicConfig( - format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", - level=self.app_config.log_level(), - ) self.log = logging.getLogger(__name__) - self.middlewares = [Middleware(create_auth_middleware(app_config))] + + middlewares = [CustomMiddleware(app_config)] + self.mcp_instance: Optional[FastMCP] = FastMCP( name="Sysdig MCP Server", instructions="Provides Sysdig Secure tools and resources.", - host=app_config.host(), - port=app_config.port(), - tags=["sysdig", "mcp", app_config.transport()], + include_tags={"sysdig_secure"}, + middleware=middlewares, + auth=obtain_remote_auth_provider(app_config) ) # Add tools to the MCP server self.add_tools() @@ -68,11 +69,13 @@ def run_http(self): # Add tools to the MCP server transport = self.app_config.transport() + settings = Settings() + # Mount the MCP HTTP/SSE app - mcp_app = self.mcp_instance.http_app(transport=transport, middleware=self.middlewares) + mcp_app = self.mcp_instance.http_app(transport=transport) suffix_path = ( - self.mcp_instance.settings.streamable_http_path if transport == "streamable-http" - else self.mcp_instance.settings.sse_path) + settings.streamable_http_path if transport == "streamable-http" + else settings.sse_path) app = FastAPI(lifespan=mcp_app.lifespan) app.mount(self.app_config.mcp_mount_path(), mcp_app) @@ -123,25 +126,29 @@ def add_tools(self) -> None: # Register the events feed tools events_feed_tools = EventsFeedTools(self.app_config) self.log.info("Adding Events Feed Tools...") - self.mcp_instance.add_tool( - events_feed_tools.tool_get_event_info, + self.mcp_instance.tool( + name_or_fn=events_feed_tools.tool_get_event_info, name="get_event_info", description="Retrieve detailed information for a specific security event by its ID", + tags={"threat-detection", "sysdig_secure"} ) - self.mcp_instance.add_tool( - events_feed_tools.tool_list_runtime_events, + self.mcp_instance.tool( + name_or_fn=events_feed_tools.tool_list_runtime_events, name="list_runtime_events", description="List runtime security events from the last given hours, optionally filtered by severity level.", + tags={"threat-detection", "sysdig_secure"} ) self.mcp_instance.add_prompt( - events_feed_tools.investigate_event_prompt, - name="investigate_event", - description="Prompt to investigate a security event based on its severity and time range.", - tags={"analysis", "secure_feeds"}, + Prompt.from_function( + fn=events_feed_tools.investigate_event_prompt, + name="investigate_event", + description="Prompt to investigate a security event based on its severity and time range.", + tags={"analysis", "sysdig_secure", "threat_detection"}, + ) ) - self.mcp_instance.add_tool( - events_feed_tools.tool_get_event_process_tree, + self.mcp_instance.tool( + name_or_fn=events_feed_tools.tool_get_event_process_tree, name="get_event_process_tree", description=( """ @@ -149,31 +156,34 @@ def add_tools(self) -> None: so this may return an empty tree. """ ), + tags={"threat-detection", "sysdig_secure"} ) # Register the Sysdig Inventory tools self.log.info("Adding Sysdig Inventory Tools...") inventory_tools = InventoryTools(self.app_config) - self.mcp_instance.add_tool( - inventory_tools.tool_list_resources, + self.mcp_instance.tool( + name_or_fn=inventory_tools.tool_list_resources, name="list_resources", description=( """ List inventory resources based on Sysdig Filter Query Language expression with optional pagination.' """ ), + tags={"inventory", "sysdig_secure"} ) - self.mcp_instance.add_tool( - inventory_tools.tool_get_resource, + self.mcp_instance.tool( + name_or_fn=inventory_tools.tool_get_resource, name="get_resource", description="Retrieve a single inventory resource by its unique hash identifier.", + tags={"inventory", "sysdig_secure"} ) # Register the Sysdig Vulnerability Management tools self.log.info("Adding Sysdig Vulnerability Management Tools...") vulnerability_tools = VulnerabilityManagementTools(self.app_config) - self.mcp_instance.add_tool( - vulnerability_tools.tool_list_runtime_vulnerabilities, + self.mcp_instance.tool( + name_or_fn=vulnerability_tools.tool_list_runtime_vulnerabilities, name="list_runtime_vulnerabilities", description=( """ @@ -181,54 +191,64 @@ def add_tools(self) -> None: (Supports pagination using cursor). """ ), + tags={"vulnerability", "sysdig_secure"} ) - self.mcp_instance.add_tool( - vulnerability_tools.tool_list_accepted_risks, + self.mcp_instance.tool( + name_or_fn=vulnerability_tools.tool_list_accepted_risks, name="list_accepted_risks", description="List all accepted risks. Supports filtering and pagination.", + tags={"vulnerability", "sysdig_secure"} ) - self.mcp_instance.add_tool( - vulnerability_tools.tool_get_accepted_risk, + self.mcp_instance.tool( + name_or_fn=vulnerability_tools.tool_get_accepted_risk, name="get_accepted_risk", description="Retrieve a specific accepted risk by its ID.", + tags={"vulnerability", "sysdig_secure"} ) - self.mcp_instance.add_tool( - vulnerability_tools.tool_list_registry_scan_results, + self.mcp_instance.tool( + name_or_fn=vulnerability_tools.tool_list_registry_scan_results, name="list_registry_scan_results", description="List registry scan results. Supports filtering and pagination.", + tags={"vulnerability", "sysdig_secure"} ) - self.mcp_instance.add_tool( - vulnerability_tools.tool_get_vulnerability_policy, + self.mcp_instance.tool( + name_or_fn=vulnerability_tools.tool_get_vulnerability_policy, name="get_vulnerability_policy_by_id", description="Retrieve a specific vulnerability policy by its ID.", + tags={"vulnerability", "sysdig_secure"} ) - self.mcp_instance.add_tool( - vulnerability_tools.tool_list_vulnerability_policies, + self.mcp_instance.tool( + name_or_fn=vulnerability_tools.tool_list_vulnerability_policies, name="list_vulnerability_policies", description="List all vulnerability policies. Supports filtering, pagination, and sorting.", + tags={"vulnerability", "sysdig_secure"} ) - self.mcp_instance.add_tool( - vulnerability_tools.tool_list_pipeline_scan_results, + self.mcp_instance.tool( + name_or_fn=vulnerability_tools.tool_list_pipeline_scan_results, name="list_pipeline_scan_results", description="List pipeline scan results (e.g., built images). Supports pagination and filtering.", + tags={"vulnerability", "sysdig_secure"} ) - self.mcp_instance.add_tool( - vulnerability_tools.tool_get_scan_result, + self.mcp_instance.tool( + name_or_fn=vulnerability_tools.tool_get_scan_result, name="get_scan_result", description="Retrieve a specific scan result (registry/runtime/pipeline).", + tags={"vulnerability", "sysdig_secure"} ) self.mcp_instance.add_prompt( - vulnerability_tools.explore_vulnerabilities_prompt, - name="explore_vulnerabilities", - description="Prompt to explore vulnerabilities based on filters", - tags={"vulnerability", "exploration"}, + Prompt.from_function( + fn=vulnerability_tools.explore_vulnerabilities_prompt, + name="explore_vulnerabilities", + description="Prompt to explore vulnerabilities based on filters", + tags={"vulnerability", "exploration", "sysdig_secure"}, + ) ) # Register the Sysdig Sage tools self.log.info("Adding Sysdig Sage Tools...") sysdig_sage_tools = SageTools(self.app_config) - self.mcp_instance.add_tool( - sysdig_sage_tools.tool_sage_to_sysql, + self.mcp_instance.tool( + name_or_fn=sysdig_sage_tools.tool_sage_to_sysql, name="sysdig_sysql_sage_query", description=( """ @@ -236,14 +256,15 @@ def add_tools(self) -> None: execute it against the Sysdig API, and return the results. """ ), + tags={"sage", "sysdig_secure"} ) if self.app_config.transport() == "stdio": # Register the tools for STDIO transport cli_scanner_tool = CLIScannerTool(self.app_config) self.log.info("Adding Sysdig CLI Scanner Tool...") - self.mcp_instance.add_tool( - cli_scanner_tool.run_sysdig_cli_scanner, + self.mcp_instance.tool( + name_or_fn=cli_scanner_tool.run_sysdig_cli_scanner, name="run_sysdig_cli_scanner", description=( """ @@ -251,14 +272,13 @@ def add_tools(self) -> None: and posture and misconfigurations. """ ), + tags={"cli-scanner", "sysdig_secure"}, ) def add_resources(self) -> None: """ Add resources to the MCP server. - Args: - mcp (FastMCP): The FastMCP server instance. """ vm_docs = HttpResource( name="Sysdig Secure Vulnerability Management Overview", diff --git a/utils/middleware/__init__.py b/utils/middleware/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/utils/middleware/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/utils/middleware/auth.py b/utils/middleware/auth.py deleted file mode 100644 index 0c34702..0000000 --- a/utils/middleware/auth.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Token-based authentication middleware -""" - -import json -import logging -import os -from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint -from starlette.requests import Request -from starlette.responses import Response -from starlette.types import ASGIApp - -from utils.sysdig.api import initialize_api_client, get_sysdig_api_instances -from utils.sysdig.client_config import get_configuration -from utils.sysdig.old_sysdig_api import OldSysdigApi -from utils.app_config import AppConfig - - -class CustomAuthMiddleware(BaseHTTPMiddleware): - """ - Custom middleware for handling token-based authentication in the MCP server and initializing Sysdig API clients. - """ - - def __init__(self, app: ASGIApp, app_config: AppConfig): - super().__init__(app) - self.app_config = app_config - # Set up logging - logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level()) - self.log = logging.getLogger(__name__) - - async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: - """ - Dispatch method to handle incoming requests, validate the Authorization header, - and initialize the Sysdig API client with the provided token and base URL. - Args: - request (Request): The incoming HTTP request. - call_next (RequestResponseEndpoint): The next middleware or endpoint to call. - Returns: - Response: The response from the next middleware or endpoint, or an error response if authentication fails. - """ - - auth_header = request.headers.get("Authorization") - if not auth_header or not auth_header.startswith("Bearer "): - json_response = {"error": "Unauthorized", "message": "Missing or invalid Authorization header"} - return Response(json.dumps(json_response), status_code=401) - # set header to be used by the API client - - # Extract releavant information from the request headers - token = auth_header.removeprefix("Bearer ").strip() - session_id = request.headers.get("mcp-session-id", "") - base_url = request.headers.get("X-Sysdig-Host", self.app_config.sysdig_endpoint()) or str(request.base_url) - self.log.info(f"Received request with session ID: {session_id}") - self.log.info(f"Using Sysdig API base URL: {base_url}") - - # Initialize the API client with the token and base URL - cfg = get_configuration(token, base_url) - cfg_sage = get_configuration(token, base_url, old_api=True) - api_client = initialize_api_client(cfg) - old_sysdig_api = initialize_api_client(cfg_sage) - api_instances = get_sysdig_api_instances(api_client) - _old_sysdig_api = OldSysdigApi(old_sysdig_api) - api_instances["old_sysdig_api"] = _old_sysdig_api - # Having access to the Sysdig API instances per request to be used by the MCP tools - request.state.api_instances = api_instances - - try: - response = await call_next(request) - return response - except Exception as e: - return Response(f"Internal server error: {str(e)}", status_code=500) - - -def create_auth_middleware(app_config: AppConfig): - """ - Factory function to create the CustomAuthMiddleware with injected app_config. - - Args: - app_config (AppConfig): The application configuration object - - Returns: - A middleware class that can be instantiated by Starlette - """ - - class ConfiguredAuthMiddleware(CustomAuthMiddleware): - def __init__(self, app: ASGIApp): - super().__init__(app, app_config) - - return ConfiguredAuthMiddleware \ No newline at end of file diff --git a/utils/query_helpers.py b/utils/query_helpers.py index 82cae96..e82c2b3 100644 --- a/utils/query_helpers.py +++ b/utils/query_helpers.py @@ -2,7 +2,7 @@ Utility functions for handling API response for the MCP server responses. """ -from datetime import datetime +import datetime from sysdig_client.rest import RESTResponseType, ApiException import json import logging @@ -101,7 +101,7 @@ def create_standard_response(results: RESTResponseType, execution_time_ms: float "results": parsed, "metadata": { "execution_time_ms": execution_time_ms, - "timestamp": datetime.utcnow().isoformat() + "Z", + "timestamp": datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z"), **metadata_kwargs, }, "status_code": status, diff --git a/utils/sysdig/api.py b/utils/sysdig/api.py deleted file mode 100644 index 299796c..0000000 --- a/utils/sysdig/api.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -This module provides functions to initialize and manage Sysdig API clients. -""" - -from sysdig_client import ApiClient, SecureEventsApi, VulnerabilityManagementApi, InventoryApi -from utils.sysdig.old_sysdig_api import OldSysdigApi -from sysdig_client.configuration import Configuration - - -def get_api_client(config: Configuration) -> ApiClient: - """ - Creates a unique instance of ApiClient with the provided configuration. - - Args: - config (Configuration): The Sysdig client configuration containing the access token and host URL. - Returns: - ApiClient: An instance of ApiClient configured with the provided settings. - """ - api_client_instance = ApiClient(config) - return api_client_instance - - -def initialize_api_client(config: Configuration = None) -> ApiClient: - """ - Initializes the Sysdig API client with the provided token and host. - This function creates a new ApiClient instance and returns a dictionary of API instances - - Args: - config (Configuration): The Sysdig client configuration containing the access token and host URL. - Returns: - dict: A dictionary containing instances of multiple Sysdig API classes. - """ - api_client = get_api_client(config) - return api_client - - -def get_sysdig_api_instances(api_client: ApiClient) -> dict: - """ - Returns a dictionary of Sysdig API instances using the provided ApiClient. - - Args: - api_client (ApiClient): The ApiClient instance to use for creating API instances. - Returns: - dict: A dictionary containing instances of multiple Sysdig API classes. - """ - return { - "secure_events": SecureEventsApi(api_client), - "vulnerability_management": VulnerabilityManagementApi(api_client), - "inventory": InventoryApi(api_client), - "old_sysdig_api": OldSysdigApi(api_client), - } diff --git a/utils/sysdig/client_config.py b/utils/sysdig/client_config.py index 3203467..ec20d9c 100644 --- a/utils/sysdig/client_config.py +++ b/utils/sysdig/client_config.py @@ -3,29 +3,57 @@ """ import sysdig_client -import os import logging import re from typing import Optional # Application config loader -from utils.app_config import get_app_config - -app_config = get_app_config() +from utils.app_config import AppConfig +from sysdig_client.configuration import Configuration +from sysdig_client import ApiClient, SecureEventsApi, VulnerabilityManagementApi, InventoryApi # Set up logging -logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=app_config.log_level()) log = logging.getLogger(__name__) +def initialize_api_client(config: Configuration = None) -> ApiClient: + """ + Initializes the Sysdig API client with the provided configuration. + Args: + config (Configuration): The Sysdig client configuration containing the access token and host URL. + Returns: + ApiClient: An instance of ApiClient configured with the provided settings. + """ + api_client = ApiClient(config) + return api_client + + +def get_sysdig_api_instances(api_client: ApiClient) -> dict: + """ + Returns a dictionary of Sysdig API instances using the provided ApiClient. + Args: + api_client (ApiClient): The ApiClient instance to use for creating API instances. + Returns: + dict: A dictionary containing instances of multiple Sysdig API classes. + """ + return { + "secure_events": SecureEventsApi(api_client), + "vulnerability_management": VulnerabilityManagementApi(api_client), + "inventory": InventoryApi(api_client), + } + # Lazy-load the Sysdig client configuration def get_configuration( - token: Optional[str] = None, sysdig_host_url: Optional[str] = None, old_api: bool = False + app_config: AppConfig, + token: Optional[str] = None, + sysdig_host_url: Optional[str] = None, + old_api: bool = False ) -> sysdig_client.Configuration: """ Returns a configured Sysdig client using environment variables. Args: + app_config (SysdigClient): MCP Server configuration. token (str): The Sysdig Secure token. sysdig_host_url (str): The base URL of the Sysdig API, refer to the docs https://docs.sysdig.com/en/administration/saas-regions-and-ip-ranges/#sysdig-platform-regions. @@ -55,7 +83,6 @@ def get_configuration( "explicitly set the public API URL in the app config 'sysdig.public_api_url'." "The expected format is https://api.{region}.sysdig.com." ) - log.info(f"Using public API URL: {sysdig_host_url}") configuration = sysdig_client.Configuration( access_token=token, diff --git a/utils/sysdig/helpers.py b/utils/sysdig/helpers.py new file mode 100644 index 0000000..4334135 --- /dev/null +++ b/utils/sysdig/helpers.py @@ -0,0 +1,12 @@ +""" +Helper functions for working with Sysdig API clients. +""" + +# Sysdig permissions needed for the different set of tools +TOOL_PERMISSIONS = { + "inventory": ["explore.read"], + "vulnerability": ["scanning.read", "secure.vm.scanresults.read"], + "sage": ["sage.exec", "sage.manage.exec"], + "cli-scanner": ["secure.vm.cli-scanner.exec"], + "threat-detection": ["custom-events.read"], +} \ No newline at end of file diff --git a/utils/sysdig/old_sysdig_api.py b/utils/sysdig/legacy_sysdig_api.py similarity index 83% rename from utils/sysdig/old_sysdig_api.py rename to utils/sysdig/legacy_sysdig_api.py index a2e84b6..b5bda47 100644 --- a/utils/sysdig/old_sysdig_api.py +++ b/utils/sysdig/legacy_sysdig_api.py @@ -1,5 +1,5 @@ """ -Temporary wrapper for Old Sysdig API. +Temporary wrapper for Legacy Sysdig API. Will be replaced with a proper implementation in the future """ @@ -8,9 +8,10 @@ from sysdig_client import ApiClient -class OldSysdigApi: +class LegacySysdigApi: """ - Wrapper for Old non-public Sysdig API. + [Deprecated] + Wrapper for Legacy Sysdig API. """ def __init__(self, api_client: ApiClient): @@ -75,3 +76,13 @@ def request_process_tree_trees(self, process_id: str) -> RESTResponseType: url = f"{self.base}/process-tree/v1/process-trees/{process_id}" resp = self.api_client.call_api("GET", url, header_params=self.headers) return resp.response + + def get_me_permissions(self) -> RESTResponseType: + """ + Retrieves the permissions for the current user. + Returns: + RESTResponseType: The response from the Sysdig API containing the user's permissions. + """ + url = f"{self.base}/users/me/permissions" + resp = self.api_client.call_api("GET", url, header_params=self.headers) + return resp.response \ No newline at end of file diff --git a/uv.lock b/uv.lock index 173795d..afc30b2 100644 --- a/uv.lock +++ b/uv.lock @@ -34,6 +34,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "authlib" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/95/e4f4ab5ce465821fe2229e10985ab80462941fe5d96387ae76bafd36f0ba/authlib-1.6.2.tar.gz", hash = "sha256:3bde83ac0392683eeef589cd5ab97e63cbe859e552dd75dca010548e79202cb1", size = 160429, upload-time = "2025-08-23T08:34:32.665Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/00/fb65909bf4c8d7da893a12006074343402a8dc8c00d916b3cee524d97f3f/authlib-1.6.2-py2.py3-none-any.whl", hash = "sha256:2dd5571013cacf6b15f7addce03ed057ffdf629e9e81bacd9c08455a190e9b57", size = 239601, upload-time = "2025-08-23T08:34:31.4Z" }, +] + [[package]] name = "certifi" version = "2025.6.15" @@ -43,6 +55,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, ] +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.2" @@ -150,6 +195,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, ] +[[package]] +name = "cryptography" +version = "45.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" }, + { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" }, + { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" }, + { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" }, + { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" }, +] + +[[package]] +name = "cyclopts" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser", marker = "python_full_version < '4.0'" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/8b/87b309117bb0892233d279dc402603c1d44c13912baf54d0a9eb8d6205ce/cyclopts-3.23.0.tar.gz", hash = "sha256:e8e28386b600c12a6db8916f2191bfdb4518260fbbceb16177f0fecae7ac7996", size = 75161, upload-time = "2025-08-25T13:14:27.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/12/d5131ad8675207e51caac3fd6ce16359f170f09ee6dc1725f82f8b9ea860/cyclopts-3.23.0-py3-none-any.whl", hash = "sha256:544ae741596bd7ce5dfc0aecf7e4ce9aa452c69dfcec0c66d20a63798dd51fb1", size = 85211, upload-time = "2025-08-25T13:14:26.679Z" }, +] + [[package]] name = "dask" version = "2025.4.1" @@ -168,6 +263,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/12/f9effea5fe2bebfdd8b0d9c857f798382afacd57dc1cd0e9ce21e66c1bc2/dask-2025.4.1-py3-none-any.whl", hash = "sha256:aacbb0a9667856fe58385015efd64aca22f0c0b2c5e1b5e633531060303bb4be", size = 1471761, upload-time = "2025-04-25T20:39:20.725Z" }, ] +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "docutils" +version = "0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984, upload-time = "2025-07-29T15:20:31.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -196,21 +331,24 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.5.1" +version = "2.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, { name = "exceptiongroup" }, { name = "httpx" }, { name = "mcp" }, + { name = "openapi-core" }, { name = "openapi-pydantic" }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, { name = "python-dotenv" }, { name = "rich" }, - { name = "typer" }, - { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/cc/37ff3a96338234a697df31d2c70b50a1d0f5e20f045d9b7cbba052be36af/fastmcp-2.5.1.tar.gz", hash = "sha256:0d10ec65a362ae4f78bdf3b639faf35b36cc0a1c8f5461a54fac906fe821b84d", size = 1035613, upload-time = "2025-05-24T11:48:27.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/80/13aec687ec21727b0fe6d26c6fe2febb33ae24e24c980929a706db3a8bc2/fastmcp-2.11.3.tar.gz", hash = "sha256:e8e3834a3e0b513712b8e63a6f0d4cbe19093459a1da3f7fbf8ef2810cfd34e3", size = 2692092, upload-time = "2025-08-11T21:38:46.493Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/4f/e7ec7b63eadcd5b10978dbc472fc3c36de3fc8c91f60ad7642192ed78836/fastmcp-2.5.1-py3-none-any.whl", hash = "sha256:a6fe50693954a6aed89fc6e43f227dcd66e112e3d3a1d633ee22b4f435ee8aed", size = 105789, upload-time = "2025-05-24T11:48:26.371Z" }, + { url = "https://files.pythonhosted.org/packages/61/05/63f63ad5b6789a730d94b8cb3910679c5da1ed5b4e38c957140ac9edcf0e/fastmcp-2.11.3-py3-none-any.whl", hash = "sha256:28f22126c90fd36e5de9cc68b9c271b6d832dcf322256f23d220b68afb3352cc", size = 260231, upload-time = "2025-08-11T21:38:44.746Z" }, ] [[package]] @@ -319,6 +457,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" @@ -334,6 +481,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.4.1" @@ -346,6 +508,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] +[[package]] +name = "lazy-object-proxy" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, + { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, + { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, +] + [[package]] name = "locket" version = "1.0.0" @@ -367,9 +561,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + [[package]] name = "mcp" -version = "1.10.0" +version = "1.12.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -379,13 +611,14 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/1a/d90e42be23a7e6dd35c03e35c7c63fe1036f082d3bb88114b66bd0f2467e/mcp-1.10.0.tar.gz", hash = "sha256:91fb1623c3faf14577623d14755d3213db837c5da5dae85069e1b59124cbe0e9", size = 392961, upload-time = "2025-06-26T13:51:19.025Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/88/f6cb7e7c260cd4b4ce375f2b1614b33ce401f63af0f49f7141a2e9bf0a45/mcp-1.12.4.tar.gz", hash = "sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5", size = 431148, upload-time = "2025-08-07T20:31:18.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/52/e1c43c4b5153465fd5d3b4b41bf2d4c7731475e9f668f38d68f848c25c9a/mcp-1.10.0-py3-none-any.whl", hash = "sha256:925c45482d75b1b6f11febddf9736d55edf7739c7ea39b583309f6651cbc9e5c", size = 150894, upload-time = "2025-06-26T13:51:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/ad/68/316cbc54b7163fa22571dcf42c9cc46562aae0a021b974e0a8141e897200/mcp-1.12.4-py3-none-any.whl", hash = "sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789", size = 160145, upload-time = "2025-08-07T20:31:15.69Z" }, ] [package.optional-dependencies] @@ -403,6 +636,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, +] + [[package]] name = "oauthlib" version = "3.2.2" @@ -412,6 +654,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688, upload-time = "2022-10-17T20:04:24.037Z" }, ] +[[package]] +name = "openapi-core" +version = "0.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "more-itertools" }, + { name = "openapi-schema-validator" }, + { name = "openapi-spec-validator" }, + { name = "parse" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" }, +] + [[package]] name = "openapi-pydantic" version = "0.5.1" @@ -424,6 +686,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] +[[package]] +name = "openapi-schema-validator" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-specifications" }, + { name = "rfc3339-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, +] + +[[package]] +name = "openapi-spec-validator" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "lazy-object-proxy" }, + { name = "openapi-schema-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -433,6 +724,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, +] + [[package]] name = "partd" version = "1.4.2" @@ -446,6 +746,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, ] +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -455,6 +764,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -470,6 +788,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.33.2" @@ -535,6 +858,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyperclip" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961, upload-time = "2024-06-18T20:38:48.401Z" } + [[package]] name = "pytest" version = "8.4.1" @@ -595,6 +924,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -650,6 +995,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + [[package]] name = "rich" version = "14.0.0" @@ -663,6 +1020,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] +[[package]] +name = "rich-rst" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839, upload-time = "2024-04-30T04:40:38.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621, upload-time = "2024-04-30T04:40:32.619Z" }, +] + [[package]] name = "rpds-py" version = "0.27.0" @@ -864,7 +1234,7 @@ wheels = [ [[package]] name = "sysdig-mcp-server" -version = "0.1.5" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "dask" }, @@ -877,7 +1247,7 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, { name = "sqlmodel" }, - { name = "sysdig-sdk" }, + { name = "sysdig-sdk-python" }, ] [package.dev-dependencies] @@ -891,15 +1261,15 @@ dev = [ requires-dist = [ { name = "dask", specifier = "==2025.4.1" }, { name = "fastapi", specifier = "==0.116.1" }, - { name = "fastmcp", specifier = "==2.5.1" }, - { name = "mcp", extras = ["cli"], specifier = "==1.10.0" }, + { name = "fastmcp", specifier = "==2.11.3" }, + { name = "mcp", extras = ["cli"], specifier = "==1.12.4" }, { name = "oauthlib", specifier = "==3.2.2" }, { name = "python-dotenv", specifier = ">=1.1.0" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "requests" }, { name = "sqlalchemy", specifier = "==2.0.36" }, { name = "sqlmodel", specifier = "==0.0.22" }, - { name = "sysdig-sdk", git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=e9b0d336c2f617f3bbd752416860f84eed160c41" }, + { name = "sysdig-sdk-python", git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=597285143188019cd0e86fde43f94b1139f5441d" }, ] [package.metadata.requires-dev] @@ -910,9 +1280,9 @@ dev = [ ] [[package]] -name = "sysdig-sdk" -version = "1.0.0" -source = { git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=e9b0d336c2f617f3bbd752416860f84eed160c41#e9b0d336c2f617f3bbd752416860f84eed160c41" } +name = "sysdig-sdk-python" +version = "0.19.1" +source = { git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=597285143188019cd0e86fde43f94b1139f5441d#597285143188019cd0e86fde43f94b1139f5441d" } dependencies = [ { name = "pydantic" }, { name = "python-dateutil" }, @@ -988,32 +1358,13 @@ wheels = [ ] [[package]] -name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +name = "werkzeug" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, ] From 4668cc7f123985fd32be6bdbec3990b657b6e86b Mon Sep 17 00:00:00 2001 From: S3B4SZ17 Date: Tue, 26 Aug 2025 18:52:02 -0600 Subject: [PATCH 07/11] feat Added prefix to env vars and fixed convention of return outputs Signed-off-by: S3B4SZ17 --- .github/workflows/test.yaml | 54 ------------ main.py | 2 - pyproject.toml | 2 +- tests/conftest.py | 49 ++++++++++- tests/events_feed_test.py | 36 ++------ tools/cli_scanner/tool.py | 10 ++- tools/events_feed/tool.py | 6 +- tools/inventory/tool.py | 9 +- tools/sysdig_sage/tool.py | 1 + tools/vulnerability_management/tool.py | 51 +++++++----- utils/app_config.py | 111 +++++++++++++++++++------ utils/auth/auth_config.py | 26 ++++-- utils/auth/middleware/auth.py | 18 ++-- utils/mcp_server.py | 49 +++++------ utils/sysdig/client_config.py | 6 +- utils/sysdig/helpers.py | 2 +- utils/sysdig/legacy_sysdig_api.py | 2 +- uv.lock | 6 +- 18 files changed, 240 insertions(+), 200 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d229702..b7fdc83 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -45,57 +45,3 @@ jobs: - name: Run Unit Tests run: make test - - check_version: - name: Check Version - runs-on: ubuntu-latest - needs: test - permissions: - contents: write # required for creating a tag - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: ${{ github.sha }} # required for better experience using pre-releases - fetch-depth: '0' # Required due to the way Git works, without it this action won't be able to find any or the correct tags - - - name: Extract current version - id: pyproject_version - run: | - TAG=v$(grep 'version =' pyproject.toml | sed -e 's/version = "\(.*\)"/\1/') - echo "TAG=$TAG" >> "$GITHUB_OUTPUT" - - - name: Get branch ref name - id: branch_ref - run: | - BRANCH_NAME=${{ github.base_ref || github.ref_name }} - echo "$BRANCH_NAME" - echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_OUTPUT" - - - name: Get tag version - id: semantic_release - uses: anothrNick/github-tag-action@1.71.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEFAULT_BUMP: "patch" - TAG_CONTEXT: 'repo' - WITH_V: true - DRY_RUN: true - - - name: Compare versions - run: | - echo "Current version: ${{ steps.pyproject_version.outputs.TAG }}" - echo "New version: ${{ steps.semantic_release.outputs.tag }}" - if [ "${{ steps.pyproject_version.outputs.TAG }}" != "${{ steps.semantic_release.outputs.tag }}" ]; then - echo "### Version mismatch detected! :warning: - Current pyproject version: ${{ steps.pyproject_version.outputs.TAG }} - New Tag version: **${{ steps.semantic_release.outputs.tag }}** - Current Tag: ${{ steps.semantic_release.outputs.old_tag }} - Please update the version in pyproject.toml." >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "### Version match confirmed! :rocket: - Current pyproject version: ${{ steps.pyproject_version.outputs.TAG }} - New Tag version: **${{ steps.semantic_release.outputs.tag }}** - The version is up-to-date." >> $GITHUB_STEP_SUMMARY - fi diff --git a/main.py b/main.py index f6d5134..e7b6b80 100644 --- a/main.py +++ b/main.py @@ -27,8 +27,6 @@ log = logging.getLogger(__name__) - - def handle_signals(): def signal_handler(sig, frame): log.info(f"Received signal {sig}, shutting down...") diff --git a/pyproject.toml b/pyproject.toml index a073e31..da47b23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "pyyaml==6.0.2", "sqlalchemy==2.0.36", "sqlmodel==0.0.22", - "sysdig-sdk-python @ git+https://github.com/sysdiglabs/sysdig-sdk-python@597285143188019cd0e86fde43f94b1139f5441d", + "sysdig-sdk-python @ git+https://github.com/sysdiglabs/sysdig-sdk-python@852ee2ccad12a8b445dd4732e7f3bd44d78a37f7", "dask==2025.4.1", "oauthlib==3.2.2", "fastapi==0.116.1", diff --git a/tests/conftest.py b/tests/conftest.py index 577d703..bd0599d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,12 @@ import json import pytest import os -from unittest.mock import patch +from utils.app_config import AppConfig +from unittest.mock import MagicMock, create_autospec, patch +from fastmcp.server.context import Context +from sysdig_client import SecureEventsApi, ApiClient, InventoryApi, VulnerabilityManagementApi +from utils.sysdig.legacy_sysdig_api import LegacySysdigApi +from fastmcp.server import FastMCP def util_load_json(path): @@ -26,7 +31,7 @@ def mock_success_response(): Fixture to mock the urllib3.PoolManager.request method Yields: - MagicMock: A mocked request object that simulates a successful HTTP response. + MagicMock: A mocked request object that simulates a successful HTTP response. """ with patch("urllib3.PoolManager.request") as mock_request: mock_resp = patch("urllib3.response.HTTPResponse").start() @@ -42,5 +47,41 @@ def mock_creds(): """ Fixture to set up mocked credentials. """ - os.environ["SYSDIG_SECURE_TOKEN"] = "mocked_token" - os.environ["SYSDIG_HOST"] = "https://us2.app.sysdig.com" + os.environ["SYSDIG_MCP_SECURE_TOKEN"] = "mocked_token" + os.environ["SYSDIG_MCP_HOST"] = "https://us2.app.sysdig.com" + + +def mock_app_config() -> AppConfig: + """ + Utility function to create a mocked AppConfig instance. + Returns: + AppConfig: A mocked AppConfig instance. + """ + mock_cfg = create_autospec(AppConfig, instance=True) + + mock_cfg.sysdig_endpoint.return_value = "https://us2.app.sysdig.com" + mock_cfg.transport.return_value = "stdio" + mock_cfg.log_level.return_value = "DEBUG" + mock_cfg.port.return_value = 8080 + + return mock_cfg + + +@pytest.fixture +def mock_context() -> Context: + """ + Utility function to create a mocked FastMCP context. + Returns: + Context: A mocked FastMCP context. + """ + + ctx = Context(MagicMock(spec=FastMCP)) + + api_instances = { + "secure_events": SecureEventsApi(ApiClient()), + "vulnerability_management": VulnerabilityManagementApi(ApiClient()), + "inventory": InventoryApi(ApiClient()), + "legacy_sysdig_api": LegacySysdigApi(ApiClient()), + } + ctx.set_state("api_instances", api_instances) + return ctx diff --git a/tests/events_feed_test.py b/tests/events_feed_test.py index 0d299dc..4638860 100644 --- a/tests/events_feed_test.py +++ b/tests/events_feed_test.py @@ -2,15 +2,13 @@ Events Feed Test Module """ +import os from http import HTTPStatus from tools.events_feed.tool import EventsFeedTools -from utils.app_config import AppConfig -from .conftest import util_load_json -from unittest.mock import MagicMock, AsyncMock, create_autospec -from sysdig_client.api import SecureEventsApi -import os +from unittest.mock import MagicMock, AsyncMock from fastmcp.server.context import Context from fastmcp.server import FastMCP +from .conftest import util_load_json, mock_app_config # Get the absolute path of the current module file module_path = os.path.abspath(__file__) @@ -20,18 +18,10 @@ EVENT_INFO_RESPONSE = util_load_json(f"{module_directory}/test_data/events_feed/event_info_response.json") +ctx = Context(MagicMock(spec=FastMCP)) -def mock_app_config() -> AppConfig: - mock_cfg = create_autospec(AppConfig, instance=True) - - mock_cfg.sysdig_endpoint.return_value = "https://us2.app.sysdig.com" - mock_cfg.transport.return_value = "stdio" - mock_cfg.log_level.return_value = "DEBUG" - mock_cfg.port.return_value = 8080 - - return mock_cfg -def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds) -> None: +def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds, mock_context: Context) -> None: """Test the get_event_info tool method. Args: mock_success_response (MagicMock | AsyncMock): Mocked response object. @@ -43,22 +33,8 @@ def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds tools_client = EventsFeedTools(app_config=mock_app_config()) - ctx = Context(FastMCP()) - - # Seed FastMCP Context state with mocked API instances expected by the tools - secure_events_api = MagicMock(spec=SecureEventsApi) - # The tool returns whatever the SDK method returns; make it be our mocked HTTP response - secure_events_api.get_event_v1_without_preload_content.return_value = mock_success_response.return_value - - api_instances = { - "secure_events": secure_events_api, - # Not used by this test, but present in real runtime; keep as empty mock to avoid KeyErrors elsewhere - "legacy_sysdig_api": MagicMock(), - } - ctx.set_state("api_instances", api_instances) - # Pass the mocked Context object - result: dict = tools_client.tool_get_event_info(ctx=ctx, event_id="12345") + result: dict = tools_client.tool_get_event_info(ctx=mock_context, event_id="12345") results: dict = result["results"] assert result.get("status_code") == HTTPStatus.OK diff --git a/tools/cli_scanner/tool.py b/tools/cli_scanner/tool.py index 55472f6..06d1d3b 100644 --- a/tools/cli_scanner/tool.py +++ b/tools/cli_scanner/tool.py @@ -11,6 +11,9 @@ from utils.app_config import AppConfig +TMP_OUTPUT_FILE = "/tmp/sysdig_cli_scanner_output.json" + + class CLIScannerTool: """ A class to encapsulate the tools for interacting with the Sysdig CLI Scanner. @@ -18,7 +21,6 @@ class CLIScannerTool: def __init__(self, app_config: AppConfig): self.app_config = app_config - logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=app_config.log_level()) self.log = logging.getLogger(__name__) self.cmd: str = "sysdig-cli-scanner" self.default_args: list = [ @@ -146,7 +148,7 @@ def run_sysdig_cli_scanner( try: # Run the command - with open("sysdig_cli_scanner_output.json", "w") as output_file: + with open(TMP_OUTPUT_FILE, "w") as output_file: result = subprocess.run(cmd, text=True, check=True, stdout=output_file, stderr=subprocess.PIPE) output_result = output_file.read() output_file.close() @@ -168,7 +170,7 @@ def run_sysdig_cli_scanner( } raise Exception(result) else: - with open("sysdig_cli_scanner_output.json", "r") as output_file: + with open(TMP_OUTPUT_FILE, "r") as output_file: output_result = output_file.read() result: dict = { "exit_code": e.returncode, @@ -176,7 +178,7 @@ def run_sysdig_cli_scanner( "output": output_result, "exit_codes_explained": self.exit_code_explained, } - os.remove("sysdig_cli_scanner_output.json") + os.remove(TMP_OUTPUT_FILE) return result # Handle any other exceptions that may occur and exit codes 2 and 3 except Exception as e: diff --git a/tools/events_feed/tool.py b/tools/events_feed/tool.py index 929fcd4..2f31c40 100644 --- a/tools/events_feed/tool.py +++ b/tools/events_feed/tool.py @@ -111,10 +111,10 @@ def tool_list_runtime_events( Returns: dict: A dictionary containing the results of the runtime events query, including pagination information. """ + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") secure_events_api: SecureEventsApi = api_instances.get("secure_events") - start_time = time.time() # Compute time window now_ns = time.time_ns() from_ts = now_ns - scope_hours * 3600 * 1_000_000_000 @@ -160,10 +160,10 @@ def tool_get_event_process_tree(self, ctx: Context, event_id: str) -> dict: ToolError: If there is an error constructing or processing the response. """ try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") legacy_api_client: LegacySysdigApi = api_instances.get("legacy_sysdig_api") - start_time = time.time() # Get process tree branches branches = legacy_api_client.request_process_tree_branches(event_id) # Get process tree @@ -194,7 +194,7 @@ def tool_get_event_process_tree(self, ctx: Context, event_id: str) -> dict: "metadata": { "execution_time_ms": (time.time() - start_time) * 1000, "timestamp": datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z"), - "note": "Process tree not available for this event" + "note": "Process tree not available for this event", }, } else: diff --git a/tools/inventory/tool.py b/tools/inventory/tool.py index 2cc217b..182f19d 100644 --- a/tools/inventory/tool.py +++ b/tools/inventory/tool.py @@ -19,6 +19,7 @@ class InventoryTools: A class to encapsulate the tools for interacting with the Sysdig Secure Inventory API. This class provides methods to list resources and retrieve a single resource by its hash. """ + def __init__(self, app_config: AppConfig): self.app_config = app_config # Configure logging @@ -35,7 +36,7 @@ def tool_list_resources( Sysdig Secure query filter expression to filter inventory resources. Use the resource://filter-query-language to get the expected filter expression format. - + List of supported fields: - accountName - accountId @@ -138,11 +139,10 @@ def tool_list_resources( Or a dict containing an error message if the call fails. """ try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") inventory_api: InventoryApi = api_instances.get("inventory") - start_time = time.time() - api_response = inventory_api.get_resources_without_preload_content( filter=filter_exp, page_number=page_number, page_size=page_size, with_enriched_containers=with_enrich_containers ) @@ -172,11 +172,10 @@ def tool_get_resource( dict: A dictionary containing the details of the requested inventory resource. """ try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") inventory_api: InventoryApi = api_instances.get("inventory") - start_time = time.time() - api_response = inventory_api.get_resource_without_preload_content(hash=resource_hash) execution_time = (time.time() - start_time) * 1000 diff --git a/tools/sysdig_sage/tool.py b/tools/sysdig_sage/tool.py index dc6c062..f802262 100644 --- a/tools/sysdig_sage/tool.py +++ b/tools/sysdig_sage/tool.py @@ -20,6 +20,7 @@ class SageTools: This class provides methods to generate SysQL queries based on natural language questions and execute them against the Sysdig API. """ + def __init__(self, app_config: AppConfig): self.app_config = app_config self.log = logging.getLogger(__name__) diff --git a/tools/vulnerability_management/tool.py b/tools/vulnerability_management/tool.py index c5747ab..bca0a0e 100644 --- a/tools/vulnerability_management/tool.py +++ b/tools/vulnerability_management/tool.py @@ -3,7 +3,6 @@ """ import logging -import os import time from typing import List, Optional, Literal, Annotated from pydantic import Field @@ -17,7 +16,6 @@ from utils.query_helpers import create_standard_response - class VulnerabilityManagementTools: """ A class to encapsulate the tools for interacting with the Sysdig Secure Vulnerability Management API. @@ -30,7 +28,6 @@ def __init__(self, app_config: AppConfig): # Configure logging self.log = logging.getLogger(__name__) - def tool_list_runtime_vulnerabilities( self, ctx: Context, @@ -122,10 +119,10 @@ def tool_list_runtime_vulnerabilities( - execution_time_ms (float): Execution duration in milliseconds. """ try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") # Record start time for execution duration - start_time = time.time() api_response = vulnerability_api.scanner_api_service_list_runtime_results_without_preload_content( cursor=cursor, filter=filter, sort=sort, order=order, limit=limit ) @@ -144,7 +141,7 @@ def tool_list_runtime_vulnerabilities( def tool_list_accepted_risks( self, - ctx : Context, + ctx: Context, filter: Optional[str] = None, limit: int = 50, cursor: Optional[str] = None, @@ -166,9 +163,9 @@ def tool_list_accepted_risks( dict: The API response as a dictionary, or an error dict on failure. """ try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") - start_time = time.time() api_response = vulnerability_api.get_accepted_risks_v1_without_preload_content( filter=filter, limit=limit, cursor=cursor, sort=sort, order=order ) @@ -197,11 +194,17 @@ def tool_get_accepted_risk(self, ctx: Context, accepted_risk_id: str) -> dict: dict: The accepted risk details as a dictionary, or an error dict on failure. """ try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") - response = vulnerability_api.get_accepted_risk_v1(accepted_risk_id) - return response.model_dump_json() if hasattr(response, "dict") else response + response = vulnerability_api.get_accepted_risk_v1_without_preload_content(accepted_risk_id) + duration_ms = (time.time() - start_time) * 1000 + response = create_standard_response( + results=response, + execution_time_ms=duration_ms, + ) + return response except ToolError as e: self.log.error(f"Exception when calling VulnerabilityManagementApi->get_accepted_risk_v1: {e}") raise e @@ -286,11 +289,17 @@ def tool_get_vulnerability_policy( dict: An error dict on failure. """ try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") - response: GetPolicyResponse = vulnerability_api.secure_vulnerability_v1_policies_policy_id_get(policy_id) - return response.model_dump_json() if hasattr(response, "dict") else response + duration_ms = (time.time() - start_time) * 1000 + + response = create_standard_response( + results=response, + execution_time_ms=duration_ms, + ) + return response except ToolError as e: self.log.error( f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_policy_id_get: {e}" @@ -317,8 +326,8 @@ def tool_list_vulnerability_policies( Returns: dict: The list of policies as a dictionary, or an error dict on failure. """ - start_time = time.time() try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") api_response = vulnerability_api.secure_vulnerability_v1_policies_get_without_preload_content( @@ -335,9 +344,7 @@ def tool_list_vulnerability_policies( ) return response except ToolError as e: - self.log.error( - f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}" - ) + self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}") raise e def tool_list_pipeline_scan_results( @@ -390,8 +397,8 @@ def tool_list_pipeline_scan_results( "execution_time_ms": float } """ - start_time = time.time() try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") @@ -407,9 +414,7 @@ def tool_list_pipeline_scan_results( ) return response except ToolError as e: - self.log.error( - f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}" - ) + self.log.error(f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_policies_get: {e}") raise e def tool_get_scan_result(self, ctx: Context, scan_id: str) -> dict: @@ -424,10 +429,18 @@ def tool_get_scan_result(self, ctx: Context, scan_id: str) -> dict: dict: ScanResultResponse as dict, or {"error": ...}. """ try: + start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") resp: ScanResultResponse = vulnerability_api.secure_vulnerability_v1_results_result_id_get(scan_id) - return resp.model_dump_json() if hasattr(resp, "dict") else resp + duration_ms = (time.time() - start_time) * 1000 + self.log.debug(f"Execution time: {duration_ms:.2f} ms") + + response = create_standard_response( + results=resp, + execution_time_ms=duration_ms, + ) + return response except ToolError as e: self.log.error( f"Exception when calling VulnerabilityManagementApi->secure_vulnerability_v1_results_result_id_get: {e}" diff --git a/utils/app_config.py b/utils/app_config.py index 0b81e4a..9149411 100644 --- a/utils/app_config.py +++ b/utils/app_config.py @@ -9,6 +9,10 @@ # app_config singleton _app_config: Optional[dict] = None +# Use ENV_PREFIX to avoid conflicts with other environment variables +ENV_PREFIX = "SYSDIG_MCP_" + + class AppConfig: """ A class to encapsulate the application configuration. @@ -19,28 +23,28 @@ def sysdig_endpoint(self) -> str: Get the Sysdig endpoint. Raises: - RuntimeError: If no SYSDIG_HOST environment variable is set. + RuntimeError: If no SYSDIG_MCP_HOST environment variable is set. Returns: str: The Sysdig API host (e.g., "https://us2.app.sysdig.com"). """ - if "SYSDIG_HOST" not in os.environ: - raise RuntimeError("Variable `SYSDIG_HOST` must be defined.") + if f"{ENV_PREFIX}HOST" not in os.environ: + raise RuntimeError(f"Variable `{ENV_PREFIX}HOST` must be defined.") - return os.environ.get("SYSDIG_HOST") + return os.environ.get(f"{ENV_PREFIX}HOST") def sysdig_secure_token(self) -> str: """ Get the Sysdig secure token. Raises: - RuntimeError: If no SYSDIG_SECURE_TOKEN environment variable is set. + RuntimeError: If no SYSDIG_MCP_SECURE_TOKEN environment variable is set. Returns: str: The Sysdig secure token. """ - if "SYSDIG_SECURE_TOKEN" not in os.environ: - raise RuntimeError("Variable `SYSDIG_SECURE_TOKEN` must be defined.") + if f"{ENV_PREFIX}SECURE_TOKEN" not in os.environ: + raise RuntimeError(f"Variable `{ENV_PREFIX}SECURE_TOKEN` must be defined.") - return os.environ.get("SYSDIG_SECURE_TOKEN") + return os.environ.get(f"{ENV_PREFIX}SECURE_TOKEN") # MCP Config Vars def transport(self) -> str: @@ -54,10 +58,12 @@ def transport(self) -> str: Returns: str: The transport protocol (e.g., "stdio", "streamable-http", or "sse"). """ - transport = os.environ.get("MCP_TRANSPORT", "stdio").lower() + transport = os.environ.get(f"{ENV_PREFIX}TRANSPORT", "stdio").lower() if transport not in ("stdio", "streamable-http", "sse"): - raise ValueError("Invalid transport protocol. Valid values are: stdio, streamable-http, sse.") + raise ValueError( + "Invalid transport protocol. Valid values are: stdio, streamable-http, sse. (sse will be deprecated)" + ) return transport @@ -68,7 +74,7 @@ def log_level(self) -> str: Returns: str: The log level string (e.g., "DEBUG", "INFO", "WARNING", "ERROR"). """ - return os.environ.get("LOGLEVEL", "INFO") + return os.environ.get(f"{ENV_PREFIX}LOGLEVEL", "ERROR") def port(self) -> int: """ @@ -78,7 +84,7 @@ def port(self) -> int: Returns: int: The MCP server port. """ - return int(os.environ.get("SYSDIG_MCP_LISTENING_PORT", "8080")) + return int(os.environ.get(f"{ENV_PREFIX}LISTENING_PORT", "8080")) # def host(self) -> str: @@ -89,7 +95,7 @@ def host(self) -> str: Returns: str: The host string (e.g., "localhost"). """ - return os.environ.get("SYSDIG_MCP_LISTENING_HOST", "localhost") + return os.environ.get(f"{ENV_PREFIX}LISTENING_HOST", "localhost") def mcp_mount_path(self) -> str: """ @@ -98,7 +104,7 @@ def mcp_mount_path(self) -> str: Returns: str: The MCP mount path. """ - return os.environ.get("MCP_MOUNT_PATH", "/sysdig-mcp-server") + return os.environ.get(f"{ENV_PREFIX}MOUNT_PATH", "/sysdig-mcp-server") def oauth_jwks_uri(self) -> str: """ @@ -106,23 +112,23 @@ def oauth_jwks_uri(self) -> str: Returns: str: The OAuth JWKS URI. """ - return os.environ.get("OAUTH_JWKS_URI", "") + return os.environ.get(f"{ENV_PREFIX}OAUTH_JWKS_URI", "") - def oauth_issuer(self) -> str: + def oauth_auth_endpoint(self) -> str: """ - Get the string value for the remote OAuth Issuer. + Get the string value for the remote OAuth Auth Endpoint. Returns: - str: The OAuth Issuer. + str: The OAuth Auth Endpoint. """ - return os.environ.get("OAUTH_ISSUER", "") + return os.environ.get(f"{ENV_PREFIX}OAUTH_AUTH_ENDPOINT", "") - def oauth_audience(self) -> str: + def oauth_token_endpoint(self) -> str: """ - Get the string value for the remote OAuth Audience. + Get the string value for the remote OAuth Token Endpoint. Returns: - str: The OAuth Audience. + str: The OAuth Token Endpoint. """ - return os.environ.get("OAUTH_AUDIENCE", "") + return os.environ.get(f"{ENV_PREFIX}OAUTH_TOKEN_ENDPOINT", "") def oauth_required_scopes(self) -> List[str]: """ @@ -130,19 +136,71 @@ def oauth_required_scopes(self) -> List[str]: Returns: List[str]: The list of scopes. """ - raw = os.environ.get("OAUTH_REQUIRED_SCOPES", "") + raw = os.environ.get(f"{ENV_PREFIX}OAUTH_REQUIRED_SCOPES", "") if not raw: return [] # Support comma-separated scopes in env var return [s.strip() for s in raw.split(",") if s.strip()] + def oauth_audience(self) -> str: + """ + Get the string value for the remote OAuth Audience. + Returns: + str: The OAuth Audience. + """ + return os.environ.get(f"{ENV_PREFIX}OAUTH_AUDIENCE", "") + + def oauth_client_id(self) -> str: + """ + Get the string value for the remote OAuth Client ID. + Returns: + str: The OAuth Client ID. + """ + return os.environ.get(f"{ENV_PREFIX}OAUTH_CLIENT_ID", "") + + def oauth_client_secret(self) -> str: + """ + Get the string value for the remote OAuth Client Secret. + Returns: + str: The OAuth Client Secret. + """ + return os.environ.get(f"{ENV_PREFIX}OAUTH_CLIENT_SECRET", "") + + def mcp_base_url(self) -> str: + """ + Get the string value for the remote MCP Base URL. + Returns: + str: The MCP Base URL. + """ + return os.environ.get(f"{ENV_PREFIX}BASE_URL", "http://localhost:8080") + + def oauth_redirect_path(self) -> str: + """ + Get the string value for the remote OAuth Redirect Path. + Returns: + str: The OAuth Redirect Path. + """ + return os.environ.get(f"{ENV_PREFIX}OAUTH_REDIRECT_PATH", "/auth/callback") + + def oauth_allowed_client_redirect_uris(self) -> List[str]: + """ + Get the list of allowed client redirect URIs for the remote OAuth. + Returns: + List[str]: The list of allowed client redirect URIs. + """ + raw = os.environ.get(f"{ENV_PREFIX}OAUTH_ALLOWED_CLIENT_REDIRECT_URIS", "http://localhost:8080") + if not raw: + return [] + # Support comma-separated URIs in env var + return [s.strip() for s in raw.split(",") if s.strip()] + def oauth_enabled(self) -> bool: """ Check to enable the remote OAuth. Returns: bool: Whether the remote OAuth should be enabled or not. """ - return os.environ.get("OAUTH_ENABLED", "false").lower() == "true" + return os.environ.get(f"{ENV_PREFIX}OAUTH_ENABLED", "false").lower() == "true" def oauth_resource_server_uri(self) -> str: """ @@ -150,7 +208,8 @@ def oauth_resource_server_uri(self) -> str: Returns: str: The OAuth Resource URI. """ - return os.environ.get("OAUTH_RESOURCE_SERVER_URI", "[]") + return os.environ.get(f"{ENV_PREFIX}OAUTH_RESOURCE_SERVER_URI", "[]") + def get_app_config() -> AppConfig: """ diff --git a/utils/auth/auth_config.py b/utils/auth/auth_config.py index f1daa6c..38bfcf9 100644 --- a/utils/auth/auth_config.py +++ b/utils/auth/auth_config.py @@ -1,32 +1,40 @@ """ Auth configuration for the MCP server. """ + from typing import Optional -from fastmcp.server.auth import RemoteAuthProvider +from fastmcp.server.auth import OAuthProvider from fastmcp.server.auth.providers.jwt import JWTVerifier from pydantic import AnyHttpUrl from utils.app_config import AppConfig -def obtain_remote_auth_provider(app_config: AppConfig) -> RemoteAuthProvider: +# FIXME: Need to implement OAuthProxy in v 2.12.0 not yet available in Pip repo +# https://gofastmcp.com/servers/auth/oauth-proxy +def obtain_remote_auth_provider(app_config: AppConfig) -> OAuthProvider: # Configure token validation for your identity provider # Create the remote auth provider - remote_auth_provider: Optional[RemoteAuthProvider] = None + remote_auth_provider: Optional[OAuthProvider] = None if app_config.oauth_enabled(): token_verifier = JWTVerifier( jwks_uri=app_config.oauth_jwks_uri(), - issuer=app_config.oauth_issuer(), + issuer=app_config.oauth_auth_endpoint(), audience=app_config.oauth_audience(), - required_scopes=app_config.oauth_required_scopes() + required_scopes=app_config.oauth_required_scopes(), ) - remote_auth_provider = RemoteAuthProvider( + remote_auth_provider = OAuthProvider( token_verifier=token_verifier, - authorization_servers=[AnyHttpUrl(app_config.oauth_issuer())], - resource_server_url=AnyHttpUrl(app_config.oauth_resource_server_uri()), + upstream_authorization_endpoint=AnyHttpUrl(app_config.oauth_auth_endpoint()), + upstream_token_endpoint=AnyHttpUrl(app_config.oauth_token_endpoint()), + upstream_client_id=app_config.oauth_client_id(), + upstream_client_secret=app_config.oauth_client_secret(), + base_url=app_config.mcp_base_url(), + redirect_path=app_config.oauth_redirect_path(), + allowed_client_redirect_uris=app_config.oauth_allowed_client_redirect_uris(), ) - return remote_auth_provider \ No newline at end of file + return remote_auth_provider diff --git a/utils/auth/middleware/auth.py b/utils/auth/middleware/auth.py index e051f47..003ef06 100644 --- a/utils/auth/middleware/auth.py +++ b/utils/auth/middleware/auth.py @@ -13,8 +13,13 @@ from utils.sysdig.client_config import get_configuration from utils.sysdig.legacy_sysdig_api import LegacySysdigApi from utils.app_config import AppConfig +from utils.app_config import get_app_config # Set up logging +logging.basicConfig( + format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", + level=get_app_config().log_level(), +) log = logging.getLogger(__name__) # TODO: Define the correct message notifications @@ -78,10 +83,12 @@ async def _save_api_instances(context: MiddlewareContext, app_config: AppConfig) if context.fastmcp_context.get_state("transport_method") in ["streamable-http", "sse"]: request: Request = get_http_request() - # Check for the Authorization header - auth_header = request.headers.get("Authorization") + # TODO: Check for the custom Authorization header or use the default. Will be relevant with the Oauth provider config. + auth_header = request.headers.get("X-Sysdig-Token", request.headers.get("Authorization")) if not auth_header or not auth_header.startswith("Bearer "): - raise Exception("Missing or invalid Authorization header") + err = "Missing or invalid Authorization header" + log.error(err) + raise Exception(err) # Extract relevant information from the request headers token = auth_header.removeprefix("Bearer ").strip() @@ -111,7 +118,7 @@ class CustomMiddleware(Middleware): Custom middleware for filtering tool listings and performing authentication. """ - def __init__(self, app_config: AppConfig): + def __init__(self, app_config: AppConfig): self.app_config = app_config # TODO: Evaluate if init the clients and perform auth only on the `notifications/initialized` event @@ -153,5 +160,4 @@ async def on_list_tools(self, context: MiddlewareContext, call_next: CallNext) - except Exception as e: log.error(f"Error filtering tools: {e}") raise - # Return modified list - return filtered_tools \ No newline at end of file + return filtered_tools diff --git a/utils/mcp_server.py b/utils/mcp_server.py index 501cea2..0461274 100644 --- a/utils/mcp_server.py +++ b/utils/mcp_server.py @@ -11,14 +11,12 @@ from fastmcp.prompts import Prompt from starlette.requests import Request from starlette.responses import JSONResponse, Response -from typing_extensions import Literal from fastapi import FastAPI from fastmcp import FastMCP, Settings from fastmcp.resources import HttpResource, TextResource from utils.auth.auth_config import obtain_remote_auth_provider from utils.auth.middleware.auth import CustomMiddleware -from starlette.middleware import Middleware from tools.events_feed.tool import EventsFeedTools from tools.inventory.tool import InventoryTools from tools.vulnerability_management.tool import VulnerabilityManagementTools @@ -28,8 +26,8 @@ # Application config loader from utils.app_config import AppConfig -class SysdigMCPServer: +class SysdigMCPServer: def __init__(self, app_config: AppConfig): self.app_config = app_config # Set up logging @@ -42,7 +40,7 @@ def __init__(self, app_config: AppConfig): instructions="Provides Sysdig Secure tools and resources.", include_tags={"sysdig_secure"}, middleware=middlewares, - auth=obtain_remote_auth_provider(app_config) + auth=obtain_remote_auth_provider(app_config), ) # Add tools to the MCP server self.add_tools() @@ -62,7 +60,6 @@ def run_stdio(self): self.log.error(f"Exception received, forcing immediate exit: {str(e)}") os._exit(1) - def run_http(self): """Run the MCP server over HTTP/SSE transport via Uvicorn.""" @@ -73,9 +70,7 @@ def run_http(self): # Mount the MCP HTTP/SSE app mcp_app = self.mcp_instance.http_app(transport=transport) - suffix_path = ( - settings.streamable_http_path if transport == "streamable-http" - else settings.sse_path) + suffix_path = settings.streamable_http_path if transport == "streamable-http" else settings.sse_path app = FastAPI(lifespan=mcp_app.lifespan) app.mount(self.app_config.mcp_mount_path(), mcp_app) @@ -97,7 +92,7 @@ async def health_check(request: Request) -> Response: # Use Uvicorn's Config and Server classes for more control config = uvicorn.Config( app, - host=self.app_config.sysdig_endpoint(), + host=self.app_config.host(), port=self.app_config.port(), timeout_graceful_shutdown=1, log_level=self.app_config.log_level().lower(), @@ -115,7 +110,6 @@ async def health_check(request: Request) -> Response: self.log.error(f"Exception received, forcing immediate exit: {str(e)}") os._exit(1) - def add_tools(self) -> None: """ Registers the tools to the MCP Server. @@ -130,13 +124,13 @@ def add_tools(self) -> None: name_or_fn=events_feed_tools.tool_get_event_info, name="get_event_info", description="Retrieve detailed information for a specific security event by its ID", - tags={"threat-detection", "sysdig_secure"} + tags={"threat-detection", "sysdig_secure"}, ) self.mcp_instance.tool( name_or_fn=events_feed_tools.tool_list_runtime_events, name="list_runtime_events", description="List runtime security events from the last given hours, optionally filtered by severity level.", - tags={"threat-detection", "sysdig_secure"} + tags={"threat-detection", "sysdig_secure"}, ) self.mcp_instance.add_prompt( @@ -156,7 +150,7 @@ def add_tools(self) -> None: so this may return an empty tree. """ ), - tags={"threat-detection", "sysdig_secure"} + tags={"threat-detection", "sysdig_secure"}, ) # Register the Sysdig Inventory tools @@ -170,13 +164,13 @@ def add_tools(self) -> None: List inventory resources based on Sysdig Filter Query Language expression with optional pagination.' """ ), - tags={"inventory", "sysdig_secure"} + tags={"inventory", "sysdig_secure"}, ) self.mcp_instance.tool( name_or_fn=inventory_tools.tool_get_resource, name="get_resource", description="Retrieve a single inventory resource by its unique hash identifier.", - tags={"inventory", "sysdig_secure"} + tags={"inventory", "sysdig_secure"}, ) # Register the Sysdig Vulnerability Management tools @@ -191,49 +185,49 @@ def add_tools(self) -> None: (Supports pagination using cursor). """ ), - tags={"vulnerability", "sysdig_secure"} + tags={"vulnerability", "sysdig_secure"}, ) self.mcp_instance.tool( name_or_fn=vulnerability_tools.tool_list_accepted_risks, name="list_accepted_risks", description="List all accepted risks. Supports filtering and pagination.", - tags={"vulnerability", "sysdig_secure"} + tags={"vulnerability", "sysdig_secure"}, ) self.mcp_instance.tool( name_or_fn=vulnerability_tools.tool_get_accepted_risk, name="get_accepted_risk", description="Retrieve a specific accepted risk by its ID.", - tags={"vulnerability", "sysdig_secure"} + tags={"vulnerability", "sysdig_secure"}, ) self.mcp_instance.tool( name_or_fn=vulnerability_tools.tool_list_registry_scan_results, name="list_registry_scan_results", description="List registry scan results. Supports filtering and pagination.", - tags={"vulnerability", "sysdig_secure"} + tags={"vulnerability", "sysdig_secure"}, ) self.mcp_instance.tool( name_or_fn=vulnerability_tools.tool_get_vulnerability_policy, name="get_vulnerability_policy_by_id", description="Retrieve a specific vulnerability policy by its ID.", - tags={"vulnerability", "sysdig_secure"} + tags={"vulnerability", "sysdig_secure"}, ) self.mcp_instance.tool( name_or_fn=vulnerability_tools.tool_list_vulnerability_policies, name="list_vulnerability_policies", description="List all vulnerability policies. Supports filtering, pagination, and sorting.", - tags={"vulnerability", "sysdig_secure"} + tags={"vulnerability", "sysdig_secure"}, ) self.mcp_instance.tool( name_or_fn=vulnerability_tools.tool_list_pipeline_scan_results, name="list_pipeline_scan_results", description="List pipeline scan results (e.g., built images). Supports pagination and filtering.", - tags={"vulnerability", "sysdig_secure"} + tags={"vulnerability", "sysdig_secure"}, ) self.mcp_instance.tool( name_or_fn=vulnerability_tools.tool_get_scan_result, name="get_scan_result", description="Retrieve a specific scan result (registry/runtime/pipeline).", - tags={"vulnerability", "sysdig_secure"} + tags={"vulnerability", "sysdig_secure"}, ) self.mcp_instance.add_prompt( Prompt.from_function( @@ -256,7 +250,7 @@ def add_tools(self) -> None: execute it against the Sysdig API, and return the results. """ ), - tags={"sage", "sysdig_secure"} + tags={"sage", "sysdig_secure"}, ) if self.app_config.transport() == "stdio": @@ -275,7 +269,6 @@ def add_tools(self) -> None: tags={"cli-scanner", "sysdig_secure"}, ) - def add_resources(self) -> None: """ Add resources to the MCP server. @@ -299,18 +292,18 @@ def add_resources(self) -> None: Query language expressions for filtering results. The query language allows you to filter resources based on their attributes. You can use the following operators and functions to build your queries: - + Operators: - `and` and `not` logical operators - `=`, `!=` - `in` - `contains` and `startsWith` to check partial values of attributes - `exists` to check if a field exists and not empty - + Note: The supported fields are going to depend on the API endpoint you are querying. Check the description of each tool for the supported fields. - + Examples: - in ("example") and = "example2" - >= "3" diff --git a/utils/sysdig/client_config.py b/utils/sysdig/client_config.py index ec20d9c..889af7c 100644 --- a/utils/sysdig/client_config.py +++ b/utils/sysdig/client_config.py @@ -42,12 +42,10 @@ def get_sysdig_api_instances(api_client: ApiClient) -> dict: "inventory": InventoryApi(api_client), } + # Lazy-load the Sysdig client configuration def get_configuration( - app_config: AppConfig, - token: Optional[str] = None, - sysdig_host_url: Optional[str] = None, - old_api: bool = False + app_config: AppConfig, token: Optional[str] = None, sysdig_host_url: Optional[str] = None, old_api: bool = False ) -> sysdig_client.Configuration: """ Returns a configured Sysdig client using environment variables. diff --git a/utils/sysdig/helpers.py b/utils/sysdig/helpers.py index 4334135..db687db 100644 --- a/utils/sysdig/helpers.py +++ b/utils/sysdig/helpers.py @@ -9,4 +9,4 @@ "sage": ["sage.exec", "sage.manage.exec"], "cli-scanner": ["secure.vm.cli-scanner.exec"], "threat-detection": ["custom-events.read"], -} \ No newline at end of file +} diff --git a/utils/sysdig/legacy_sysdig_api.py b/utils/sysdig/legacy_sysdig_api.py index b5bda47..16e2f64 100644 --- a/utils/sysdig/legacy_sysdig_api.py +++ b/utils/sysdig/legacy_sysdig_api.py @@ -85,4 +85,4 @@ def get_me_permissions(self) -> RESTResponseType: """ url = f"{self.base}/users/me/permissions" resp = self.api_client.call_api("GET", url, header_params=self.headers) - return resp.response \ No newline at end of file + return resp.response diff --git a/uv.lock b/uv.lock index afc30b2..f27d5a1 100644 --- a/uv.lock +++ b/uv.lock @@ -236,7 +236,7 @@ version = "3.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "docstring-parser", marker = "python_full_version < '4.0'" }, + { name = "docstring-parser", marker = "python_full_version < '4'" }, { name = "rich" }, { name = "rich-rst" }, ] @@ -1269,7 +1269,7 @@ requires-dist = [ { name = "requests" }, { name = "sqlalchemy", specifier = "==2.0.36" }, { name = "sqlmodel", specifier = "==0.0.22" }, - { name = "sysdig-sdk-python", git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=597285143188019cd0e86fde43f94b1139f5441d" }, + { name = "sysdig-sdk-python", git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=852ee2ccad12a8b445dd4732e7f3bd44d78a37f7" }, ] [package.metadata.requires-dev] @@ -1282,7 +1282,7 @@ dev = [ [[package]] name = "sysdig-sdk-python" version = "0.19.1" -source = { git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=597285143188019cd0e86fde43f94b1139f5441d#597285143188019cd0e86fde43f94b1139f5441d" } +source = { git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=852ee2ccad12a8b445dd4732e7f3bd44d78a37f7#852ee2ccad12a8b445dd4732e7f3bd44d78a37f7" } dependencies = [ { name = "pydantic" }, { name = "python-dateutil" }, From 1b3f9d0e11b4674784fa447c113a65dd2cc5b886 Mon Sep 17 00:00:00 2001 From: S3B4SZ17 Date: Tue, 26 Aug 2025 19:51:12 -0600 Subject: [PATCH 08/11] feat: Updating docs and k8s related resources Signed-off-by: S3B4SZ17 --- .github/workflows/helm_test.yaml | 4 +- Dockerfile | 1 - README.md | 79 +++++++++++++------- charts/sysdig-mcp/Chart.yaml | 4 +- charts/sysdig-mcp/templates/configmap.yaml | 9 --- charts/sysdig-mcp/templates/deployment.yaml | 19 ++--- charts/sysdig-mcp/templates/secrets.yaml | 2 +- charts/sysdig-mcp/values.schema.json | 21 ------ charts/sysdig-mcp/values.yaml | 27 +------ docs/assets/goose_results.png | Bin 0 -> 186544 bytes tests/conftest.py | 4 +- tools/cli_scanner/tool.py | 12 +-- tools/events_feed/tool.py | 16 +++- tools/inventory/tool.py | 12 ++- tools/sysdig_sage/tool.py | 3 + tools/vulnerability_management/tool.py | 46 +++++++++++- utils/app_config.py | 16 ++-- utils/auth/middleware/auth.py | 9 ++- utils/sysdig/legacy_sysdig_api.py | 10 +++ 19 files changed, 172 insertions(+), 122 deletions(-) delete mode 100644 charts/sysdig-mcp/templates/configmap.yaml create mode 100644 docs/assets/goose_results.png diff --git a/.github/workflows/helm_test.yaml b/.github/workflows/helm_test.yaml index b0a4d23..d47f4a2 100644 --- a/.github/workflows/helm_test.yaml +++ b/.github/workflows/helm_test.yaml @@ -57,10 +57,10 @@ jobs: run: ct lint --target-branch ${{ github.event.repository.default_branch }} --chart-dirs charts - name: Create kind cluster - if: steps.list-changed.outputs.changed == 'true' + if: steps.list-changed.outputs.changed == 'true' && github.event_name != 'pull_request' uses: helm/kind-action@v1.12.0 - name: Run chart-testing (install) - if: steps.list-changed.outputs.changed == 'true' + if: steps.list-changed.outputs.changed == 'true' && github.event_name != 'pull_request' run: | ct install --target-branch ${{ github.event.repository.default_branch }} --chart-dirs charts diff --git a/Dockerfile b/Dockerfile index 472a84f..2644672 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,6 @@ WORKDIR /app RUN apt update && apt install -y git # Copy the application from the builder COPY --from=builder --chown=app:app /tmp/sysdig_mcp_server.tar.gz /app -COPY --from=builder --chown=app:app /app/app_config.yaml /app RUN pip install /app/sysdig_mcp_server.tar.gz diff --git a/README.md b/README.md index 7fbbebd..a393fb5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ - [Requirements](#requirements) - [UV Setup](#uv-setup) - [Configuration](#configuration) - - [Environment Variables](#environment-variables) - [Running the Server](#running-the-server) - [Docker](#docker) - [K8s Deployment](#k8s-deployment) @@ -27,6 +26,7 @@ - [URL](#url) - [Claude Desktop App](#claude-desktop-app) - [MCP Inspector](#mcp-inspector) + - [Goose Agent](#goose-agent) ## Description @@ -65,17 +65,17 @@ Get up and running with the Sysdig MCP Server quickly using our pre-built Docker "-i", "--rm", "-e", - "SYSDIG_HOST", + "SYSDIG_MCP_API_HOST", "-e", - "MCP_TRANSPORT", + "SYSDIG_MCP_TRANSPORT", "-e", - "SYSDIG_SECURE_TOKEN", + "SYSDIG_MCP_API_SECURE_TOKEN", "ghcr.io/sysdiglabs/sysdig-mcp-server:latest" ], "env": { - "SYSDIG_HOST": "", - "SYSDIG_SECURE_TOKEN": "", - "MCP_TRANSPORT": "stdio" + "SYSDIG_MCP_API_HOST": "", + "SYSDIG_MCP_API_SECURE_TOKEN": "", + "SYSDIG_MCP_TRANSPORT": "stdio" } } } @@ -167,14 +167,14 @@ This will create a virtual environment using `uv` and install the required depen The following environment variables are **required** for configuring the Sysdig SDK: -- `SYSDIG_HOST`: The URL of your Sysdig Secure instance (e.g., `https://us2.app.sysdig.com`). -- `SYSDIG_SECURE_TOKEN`: Your Sysdig Secure API token. +- `SYSDIG_MCP_API_HOST`: The URL of your Sysdig Secure instance (e.g., `https://us2.app.sysdig.com`). +- `SYSDIG_MCP_API_SECURE_TOKEN`: Your Sysdig Secure API token. You can also set the following variables to override the default configuration: -- `MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`. -- `MCP_MOUNT_PATH`: The URL prefix for the Streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server` -- `LOGLEVEL`: Log Level of the application (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Defaults to: `INFO` +- `SYSDIG_MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`. +- `SYSDIG_MCP_MOUNT_PATH`: The URL prefix for the Streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server` +- `SYSDIG_MCP_LOGLEVEL`: Log Level of the application (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Defaults to: `INFO` - `SYSDIG_MCP_LISTENING_PORT`: The port for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `8080` - `SYSDIG_MCP_LISTENING_HOST`: The host for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `localhost` @@ -203,7 +203,7 @@ Then, you can run the container, making sure to pass the required environment va docker run -e SYSDIG_HOST= -e SYSDIG_SECURE_TOKEN= -p 8080:8080 sysdig-mcp-server ``` -By default, the server will run using the `stdio` transport. To use the `streamable-http` or `sse` transports, set the `MCP_TRANSPORT` environment variable to `streamable-http` or `sse`: +By default, the server will run using the `stdio` transport. To use the `streamable-http` or `sse` transports, set the `SYSDIG_MCP_TRANSPORT` environment variable to `streamable-http` or `sse`: ```bash docker run -e MCP_TRANSPORT=streamable-http -e SYSDIG_HOST= -e SYSDIG_SECURE_TOKEN= -p 8080:8080 sysdig-mcp-server @@ -267,7 +267,7 @@ To run the server using `uv`, first set up the environment as described in the [ uv run main.py ``` -By default, the server will run using the `stdio` transport. To use the `streamable-http` or `sse` transports, set the `MCP_TRANSPORT` environment variable to `streamable-http` or `sse`: +By default, the server will run using the `stdio` transport. To use the `streamable-http` or `sse` transports, set the `SYSDIG_MCP_TRANSPORT` environment variable to `streamable-http` or `sse`: ```bash MCP_TRANSPORT=streamable-http uv run main.py @@ -279,9 +279,9 @@ To use the MCP server with a client like Claude or Cursor, you need to provide t ### Authentication -When using the `sse` or `streamable-http` transport, the server requires a Bearer token for authentication. The token is passed in the `Authorization` header of the HTTP request. +When using the `sse` or `streamable-http` transport, the server requires a Bearer token for authentication. The token is passed in the `X-Sysdig-Token` or default to `Authorization` header of the HTTP request (i.e `Bearer SYSDIG_SECURE_API_TOKEN`). -Additionally, you can specify the Sysdig Secure host by providing the `X-Sysdig-Host` header. If this header is not present, the server will use the value from the env variable. +Additionally, you can specify the Sysdig Secure host by providing the `X-Sysdig-Host` header. If this header is not present, the server will use the value from the env variable `SYSDIG_MCP_API_HOST`. Example headers: @@ -319,9 +319,9 @@ For the Claude Desktop app, you can manually configure the MCP server by editing "main.py" ], "env": { - "SYSDIG_HOST": "", - "SYSDIG_SECURE_TOKEN": "", - "MCP_TRANSPORT": "stdio" + "SYSDIG_MCP_API_HOST": "", + "SYSDIG_MCP_API_SECURE_TOKEN": "", + "SYSDIG_MCP_TRANSPORT": "stdio" } } } @@ -340,17 +340,17 @@ For the Claude Desktop app, you can manually configure the MCP server by editing "-i", "--rm", "-e", - "SYSDIG_HOST", + "SYSDIG_MCP_API_HOST", "-e", - "MCP_TRANSPORT", + "SYSDIG_MCP_TRANSPORT", "-e", - "SYSDIG_SECURE_TOKEN", + "SYSDIG_MCP_API_SECURE_TOKEN", "ghcr.io/sysdiglabs/sysdig-mcp-server" ], "env": { - "SYSDIG_HOST": "", - "SYSDIG_SECURE_TOKEN": "", - "MCP_TRANSPORT": "stdio" + "SYSDIG_MCP_API_HOST": "", + "SYSDIG_MCP_API_SECURE_TOKEN": "", + "SYSDIG_MCP_TRANSPORT": "stdio" } } } @@ -371,3 +371,32 @@ For the Claude Desktop app, you can manually configure the MCP server by editing 3. Pass the Authorization header if using "streamable-http" or the SYSDIG_SECURE_API_TOKEN env var if using "stdio" ![mcp-inspector](./docs/assets/mcp-inspector.png) + + +### Goose Agent + +1. In your terminal run `goose configure` and follow the steps to add the extension (more info on the [goose docs](https://block.github.io/goose/docs/getting-started/using-extensions/)), again could be using docker or uv as shown in the above examples. +2. Your `~/.config/goose/config.yaml` config file should have one config like this one, check out the env vars + + ```yaml + extensions: + ... + sysdig-mcp-server: + args: [] + bundled: null + cmd: sysdig-mcp-server + description: Sysdig MCP server + enabled: true + env_keys: + - SYSDIG_MCP_TRANSPORT + - SYSDIG_MCP_API_HOST + - SYSDIG_MCP_API_SECURE_TOKEN + envs: + SYSDIG_MCP_TRANSPORT: stdio + name: sysdig-mcp-server + timeout: 300 + type: stdio + ``` +3. Have fun + +![goose_results](./docs/assets/goose_results.png) diff --git a/charts/sysdig-mcp/Chart.yaml b/charts/sysdig-mcp/Chart.yaml index 224d8b0..9044449 100644 --- a/charts/sysdig-mcp/Chart.yaml +++ b/charts/sysdig-mcp/Chart.yaml @@ -20,10 +20,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.3 +version: 0.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.1.3" +appVersion: "v0.2.0" diff --git a/charts/sysdig-mcp/templates/configmap.yaml b/charts/sysdig-mcp/templates/configmap.yaml deleted file mode 100644 index b75f033..0000000 --- a/charts/sysdig-mcp/templates/configmap.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{- if .Values.configMap.enabled -}} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "sysdig-mcp.fullname" . }}-config -data: - app_config.yaml: | -{{- (tpl .Values.configMap.app_config $) | nindent 4 }} -{{- end }} diff --git a/charts/sysdig-mcp/templates/deployment.yaml b/charts/sysdig-mcp/templates/deployment.yaml index 2c25d74..0e0ea59 100644 --- a/charts/sysdig-mcp/templates/deployment.yaml +++ b/charts/sysdig-mcp/templates/deployment.yaml @@ -37,28 +37,28 @@ spec: image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} env: - - name: SYSDIG_HOST + - name: SYSDIG_MCP_API_HOST value: {{ .Values.sysdig.host | quote }} {{- if .Values.sysdig.secrets.create }} - - name: SYSDIG_SECURE_API_TOKEN + - name: SYSDIG_MCP_API_SECURE_TOKEN valueFrom: secretKeyRef: name: "{{ include "sysdig-mcp.fullname" . }}-sysdig-secrets" - key: SYSDIG_SECURE_API_TOKEN + key: SYSDIG_MCP_API_SECURE_TOKEN {{- end }} {{- if .Values.oauth.secrets.create }} - - name: MCP_OAUTH_OAUTH_CLIENT_ID + - name: SYSDIG_MCP_OAUTH_CLIENT_ID valueFrom: secretKeyRef: name: "{{ include "sysdig-mcp.fullname" . }}-oauth-secrets" key: clientId - - name: MCP_OAUTH_OAUTH_CLIENT_SECRET + - name: SYSDIG_MCP_OAUTH_CLIENT_SECRET valueFrom: secretKeyRef: name: "{{ include "sysdig-mcp.fullname" . }}-oauth-secrets" key: clientSecret {{- end }} - - name: MCP_TRANSPORT + - name: SYSDIG_MCP_TRANSPORT value: {{ .Values.sysdig.mcp.transport | quote }} ports: - name: http @@ -77,17 +77,10 @@ spec: resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: - - name: config - mountPath: "/app/app_config.yaml" - subPath: "app_config.yaml" {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} volumes: - - name: config - configMap: - # Provide the name of the ConfigMap you want to mount. - name: {{ include "sysdig-mcp.fullname" . }}-config {{- with .Values.volumes }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/sysdig-mcp/templates/secrets.yaml b/charts/sysdig-mcp/templates/secrets.yaml index 0976e63..7420c1e 100644 --- a/charts/sysdig-mcp/templates/secrets.yaml +++ b/charts/sysdig-mcp/templates/secrets.yaml @@ -8,7 +8,7 @@ metadata: release: {{ .Release.Name }} type: Opaque data: - SYSDIG_SECURE_API_TOKEN: {{ .Values.sysdig.secrets.secureAPIToken | b64enc }} + SYSDIG_MCP_API_SECURE_TOKEN: {{ .Values.sysdig.secrets.secureAPIToken | b64enc }} {{- end }} --- {{- if .Values.oauth.secrets.create -}} diff --git a/charts/sysdig-mcp/values.schema.json b/charts/sysdig-mcp/values.schema.json index 33cb7bf..76c69e3 100644 --- a/charts/sysdig-mcp/values.schema.json +++ b/charts/sysdig-mcp/values.schema.json @@ -11,7 +11,6 @@ } }, "required": [ - "configMap", "sysdig" ], "$defs": { @@ -116,26 +115,6 @@ "secrets" ], "additionalProperties": false - }, - "AppConfig": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "description": "Whether to create the application configuration" - }, - "app_config": { - "type": [ - "string", - "null" - ], - "description": "The application configuration in YAML format" - } - }, - "required": [ - "secrets" - ], - "additionalProperties": false } } } diff --git a/charts/sysdig-mcp/values.yaml b/charts/sysdig-mcp/values.yaml index e84f93a..c478a8c 100644 --- a/charts/sysdig-mcp/values.yaml +++ b/charts/sysdig-mcp/values.yaml @@ -8,7 +8,7 @@ image: repository: ghcr.io/sysdiglabs/sysdig-mcp-server pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "v0.1.3" + tag: "v0.2.0" imagePullSecrets: [] nameOverride: "" @@ -107,28 +107,3 @@ nodeSelector: {} tolerations: [] affinity: {} - -configMap: - enabled: true - app_config: | - # Sysdig MCP Server Configuration - # This file is used to configure the Sysdig MCP server. - # You can add your custom configuration here. - app: - host: "0.0.0.0" - port: 8080 - log_level: "ERROR" - - sysdig: - host: "https://us2.app.sysdig.com" - - mcp: - transport: streamable-http - host: "0.0.0.0" - port: 8080 - allowed_tools: - - "events-feed" - - "sysdig-cli-scanner" # You need the sysdig-cli-scanner binary installed in your server to use this tool - - "vulnerability-management" - - "inventory" - - "sysdig-sage" diff --git a/docs/assets/goose_results.png b/docs/assets/goose_results.png new file mode 100644 index 0000000000000000000000000000000000000000..56161426c4c76853cc06e47203f79b8ca80c84b5 GIT binary patch literal 186544 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 z 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 bD_5Fn>T<2V*a5GuTv1j~moJeu5BmQAG85b^ literal 0 HcmV?d00001 diff --git a/tests/conftest.py b/tests/conftest.py index bd0599d..c96cec4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,8 +47,8 @@ def mock_creds(): """ Fixture to set up mocked credentials. """ - os.environ["SYSDIG_MCP_SECURE_TOKEN"] = "mocked_token" - os.environ["SYSDIG_MCP_HOST"] = "https://us2.app.sysdig.com" + os.environ["SYSDIG_MCP_API_SECURE_TOKEN"] = "mocked_token" + os.environ["SYSDIG_MCP_API_HOST"] = "https://us2.app.sysdig.com" def mock_app_config() -> AppConfig: diff --git a/tools/cli_scanner/tool.py b/tools/cli_scanner/tool.py index 06d1d3b..9d7ee0c 100644 --- a/tools/cli_scanner/tool.py +++ b/tools/cli_scanner/tool.py @@ -8,11 +8,10 @@ import os import subprocess from typing import Literal, Optional +from tempfile import NamedTemporaryFile from utils.app_config import AppConfig -TMP_OUTPUT_FILE = "/tmp/sysdig_cli_scanner_output.json" - class CLIScannerTool: """ @@ -120,6 +119,7 @@ def run_sysdig_cli_scanner( self.check_sysdig_cli_installed() self.check_env_credentials() + tmp_result_file = NamedTemporaryFile(suffix=".json", prefix="sysdig_cli_scanner_", delete_on_close=False) # Prepare the command based on the mode if mode == "iac": self.log.info("Running Sysdig CLI Scanner in IaC mode.") @@ -148,7 +148,7 @@ def run_sysdig_cli_scanner( try: # Run the command - with open(TMP_OUTPUT_FILE, "w") as output_file: + with open(tmp_result_file.name, "w") as output_file: result = subprocess.run(cmd, text=True, check=True, stdout=output_file, stderr=subprocess.PIPE) output_result = output_file.read() output_file.close() @@ -170,7 +170,7 @@ def run_sysdig_cli_scanner( } raise Exception(result) else: - with open(TMP_OUTPUT_FILE, "r") as output_file: + with open(tmp_result_file.name, "r") as output_file: output_result = output_file.read() result: dict = { "exit_code": e.returncode, @@ -178,8 +178,10 @@ def run_sysdig_cli_scanner( "output": output_result, "exit_codes_explained": self.exit_code_explained, } - os.remove(TMP_OUTPUT_FILE) return result # Handle any other exceptions that may occur and exit codes 2 and 3 except Exception as e: raise e + finally: + if os.path.exists(tmp_result_file.name): + os.remove(tmp_result_file.name) diff --git a/tools/events_feed/tool.py b/tools/events_feed/tool.py index 2f31c40..b183e5f 100644 --- a/tools/events_feed/tool.py +++ b/tools/events_feed/tool.py @@ -6,7 +6,6 @@ """ import logging -import os import time import datetime from typing import Optional, Annotated @@ -41,10 +40,15 @@ def tool_get_event_info(self, ctx: Context, event_id: str) -> dict: Returns: Event: The Event object containing detailed information about the specified event. + Raises: + ToolError: If the API call fails or the response is invalid. """ # Init of the sysdig client api_instances: dict = ctx.get_state("api_instances") secure_events_api: SecureEventsApi = api_instances.get("secure_events") + if not secure_events_api: + self.log("SecureEventsApi instance not found") + raise ToolError("SecureEventsApi instance not found") try: # Get the HTTP request @@ -58,7 +62,7 @@ def tool_get_event_info(self, ctx: Context, event_id: str) -> dict: return response except ToolError as e: - logging.error("Exception when calling SecureEventsApi->get_event_v1: %s\n" % e) + self.log("Exception when calling SecureEventsApi->get_event_v1: %s\n" % e) raise e def tool_list_runtime_events( @@ -110,10 +114,15 @@ def tool_list_runtime_events( Returns: dict: A dictionary containing the results of the runtime events query, including pagination information. + Raises: + ToolError: If the API call fails or the response is invalid. """ start_time = time.time() api_instances: dict = ctx.get_state("api_instances") secure_events_api: SecureEventsApi = api_instances.get("secure_events") + if not secure_events_api: + self.log("SecureEventsApi instance not found") + raise ToolError("SecureEventsApi instance not found") # Compute time window now_ns = time.time_ns() @@ -163,6 +172,9 @@ def tool_get_event_process_tree(self, ctx: Context, event_id: str) -> dict: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") legacy_api_client: LegacySysdigApi = api_instances.get("legacy_sysdig_api") + if not legacy_api_client: + self.log("LegacySysdigApi instance not found") + raise ToolError("LegacySysdigApi instance not found") # Get process tree branches branches = legacy_api_client.request_process_tree_branches(event_id) diff --git a/tools/inventory/tool.py b/tools/inventory/tool.py index 182f19d..6f3d258 100644 --- a/tools/inventory/tool.py +++ b/tools/inventory/tool.py @@ -137,11 +137,16 @@ def tool_list_resources( Returns: dict: A dictionary containing the results of the inventory query, including pagination information. Or a dict containing an error message if the call fails. + Raises: + ToolError: If the API call fails or the response is invalid. """ try: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") inventory_api: InventoryApi = api_instances.get("inventory") + if not inventory_api: + self.log("InventoryApi instance not found") + raise ToolError("InventoryApi instance not found") api_response = inventory_api.get_resources_without_preload_content( filter=filter_exp, page_number=page_number, page_size=page_size, with_enriched_containers=with_enrich_containers @@ -153,7 +158,7 @@ def tool_list_resources( return response except ToolError as e: - logging.error("Exception when calling InventoryApi->get_resources: %s\n" % e) + self.log.error("Exception when calling InventoryApi->get_resources: %s\n" % e) raise e def tool_get_resource( @@ -170,11 +175,16 @@ def tool_get_resource( Returns: dict: A dictionary containing the details of the requested inventory resource. + Raises: + ToolError: If the API call fails or the response is invalid. """ try: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") inventory_api: InventoryApi = api_instances.get("inventory") + if not inventory_api: + self.log("InventoryApi instance not found") + raise ToolError("InventoryApi instance not found") api_response = inventory_api.get_resource_without_preload_content(hash=resource_hash) execution_time = (time.time() - start_time) * 1000 diff --git a/tools/sysdig_sage/tool.py b/tools/sysdig_sage/tool.py index f802262..c88771a 100644 --- a/tools/sysdig_sage/tool.py +++ b/tools/sysdig_sage/tool.py @@ -50,6 +50,9 @@ async def tool_sage_to_sysql(self, ctx: Context, question: str) -> dict: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") legacy_api_client: LegacySysdigApi = api_instances.get("legacy_sysdig_api") + if not legacy_api_client: + self.log.error("LegacySysdigApi instance not found") + raise ToolError("LegacySysdigApi instance not found") sysql_response = await legacy_api_client.generate_sysql_query(question) if sysql_response.status > 299: diff --git a/tools/vulnerability_management/tool.py b/tools/vulnerability_management/tool.py index bca0a0e..6e7b65a 100644 --- a/tools/vulnerability_management/tool.py +++ b/tools/vulnerability_management/tool.py @@ -117,12 +117,17 @@ def tool_list_runtime_vulnerabilities( - results: Serializable dict of runtime vulnerability scan results. - cursor (Optional[str]): Next page cursor, or None if no further pages. - execution_time_ms (float): Execution duration in milliseconds. + Raises: + ToolError: If the API call fails or the response is invalid. """ try: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") - # Record start time for execution duration + if not vulnerability_api: + self.log.error("VulnerabilityManagementApi instance not found") + raise ToolError("VulnerabilityManagementApi instance not found") + api_response = vulnerability_api.scanner_api_service_list_runtime_results_without_preload_content( cursor=cursor, filter=filter, sort=sort, order=order, limit=limit ) @@ -161,11 +166,17 @@ def tool_list_accepted_risks( Returns: dict: The API response as a dictionary, or an error dict on failure. + Raises: + ToolError: If the API call fails or the response is invalid. """ try: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + if not vulnerability_api: + self.log.error("VulnerabilityManagementApi instance not found") + raise ToolError("VulnerabilityManagementApi instance not found") + api_response = vulnerability_api.get_accepted_risks_v1_without_preload_content( filter=filter, limit=limit, cursor=cursor, sort=sort, order=order ) @@ -192,11 +203,16 @@ def tool_get_accepted_risk(self, ctx: Context, accepted_risk_id: str) -> dict: Returns: dict: The accepted risk details as a dictionary, or an error dict on failure. + Raises: + ToolError: If the API call fails or the response is invalid. """ try: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + if not vulnerability_api: + self.log.error("VulnerabilityManagementApi instance not found") + raise ToolError("VulnerabilityManagementApi instance not found") response = vulnerability_api.get_accepted_risk_v1_without_preload_content(accepted_risk_id) duration_ms = (time.time() - start_time) * 1000 @@ -254,10 +270,15 @@ def tool_list_registry_scan_results( Returns: dict: The registry scan results as a dictionary, or an error dict on failure. + Raises: + ToolError: If the API call fails or the response is invalid. """ try: api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + if not vulnerability_api: + self.log.error("VulnerabilityManagementApi instance not found") + raise ToolError("VulnerabilityManagementApi instance not found") start_time = time.time() api_response = vulnerability_api.scanner_api_service_list_registry_results_without_preload_content( @@ -287,11 +308,17 @@ def tool_get_vulnerability_policy( Returns: GetPolicyResponse: The policy details model. dict: An error dict on failure. + Raises: + ToolError: If the API call fails or the response is invalid. """ try: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + if not vulnerability_api: + self.log.error("VulnerabilityManagementApi instance not found") + raise ToolError("VulnerabilityManagementApi instance not found") + response: GetPolicyResponse = vulnerability_api.secure_vulnerability_v1_policies_policy_id_get(policy_id) duration_ms = (time.time() - start_time) * 1000 @@ -325,11 +352,17 @@ def tool_list_vulnerability_policies( Returns: dict: The list of policies as a dictionary, or an error dict on failure. + Raises: + ToolError: If the API call fails or the response is invalid. """ try: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + if not vulnerability_api: + self.log.error("VulnerabilityManagementApi instance not found") + raise ToolError("VulnerabilityManagementApi instance not found") + api_response = vulnerability_api.secure_vulnerability_v1_policies_get_without_preload_content( cursor=cursor, limit=limit, name=name, stages=stages ) @@ -396,11 +429,16 @@ def tool_list_pipeline_scan_results( "next_cursor": Optional[str], "execution_time_ms": float } + Raises: + ToolError: If the API call fails or the response is invalid. """ try: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + if not vulnerability_api: + self.log.error("VulnerabilityManagementApi instance not found") + raise ToolError("VulnerabilityManagementApi instance not found") api_response = vulnerability_api.secure_vulnerability_v1_pipeline_results_get_without_preload_content( cursor=cursor, filter=filter, limit=limit @@ -427,11 +465,17 @@ def tool_get_scan_result(self, ctx: Context, scan_id: str) -> dict: Returns: dict: ScanResultResponse as dict, or {"error": ...}. + Raises: + ToolError: If the API call fails or the response is invalid. """ try: start_time = time.time() api_instances: dict = ctx.get_state("api_instances") vulnerability_api: VulnerabilityManagementApi = api_instances.get("vulnerability_management") + if not vulnerability_api: + self.log.error("VulnerabilityManagementApi instance not found") + raise ToolError("VulnerabilityManagementApi instance not found") + resp: ScanResultResponse = vulnerability_api.secure_vulnerability_v1_results_result_id_get(scan_id) duration_ms = (time.time() - start_time) * 1000 self.log.debug(f"Execution time: {duration_ms:.2f} ms") diff --git a/utils/app_config.py b/utils/app_config.py index 9149411..996e17e 100644 --- a/utils/app_config.py +++ b/utils/app_config.py @@ -23,28 +23,28 @@ def sysdig_endpoint(self) -> str: Get the Sysdig endpoint. Raises: - RuntimeError: If no SYSDIG_MCP_HOST environment variable is set. + RuntimeError: If no SYSDIG_MCP_API_HOST environment variable is set. Returns: str: The Sysdig API host (e.g., "https://us2.app.sysdig.com"). """ - if f"{ENV_PREFIX}HOST" not in os.environ: - raise RuntimeError(f"Variable `{ENV_PREFIX}HOST` must be defined.") + if f"{ENV_PREFIX}API_HOST" not in os.environ: + raise RuntimeError(f"Variable `{ENV_PREFIX}API_HOST` must be defined.") - return os.environ.get(f"{ENV_PREFIX}HOST") + return os.environ[f"{ENV_PREFIX}API_HOST"] def sysdig_secure_token(self) -> str: """ Get the Sysdig secure token. Raises: - RuntimeError: If no SYSDIG_MCP_SECURE_TOKEN environment variable is set. + RuntimeError: If no SYSDIG_MCP_API_SECURE_TOKEN environment variable is set. Returns: str: The Sysdig secure token. """ - if f"{ENV_PREFIX}SECURE_TOKEN" not in os.environ: - raise RuntimeError(f"Variable `{ENV_PREFIX}SECURE_TOKEN` must be defined.") + if f"{ENV_PREFIX}API_SECURE_TOKEN" not in os.environ: + raise RuntimeError(f"Variable `{ENV_PREFIX}API_SECURE_TOKEN` must be defined.") - return os.environ.get(f"{ENV_PREFIX}SECURE_TOKEN") + return os.environ[f"{ENV_PREFIX}API_SECURE_TOKEN"] # MCP Config Vars def transport(self) -> str: diff --git a/utils/auth/middleware/auth.py b/utils/auth/middleware/auth.py index 003ef06..fb80b9d 100644 --- a/utils/auth/middleware/auth.py +++ b/utils/auth/middleware/auth.py @@ -4,6 +4,7 @@ import logging import os +from http import HTTPStatus from starlette.requests import Request from fastmcp.server.middleware import Middleware, MiddlewareContext, CallNext from utils.sysdig.helpers import TOOL_PERMISSIONS @@ -15,10 +16,12 @@ from utils.app_config import AppConfig from utils.app_config import get_app_config +app_config = get_app_config() + # Set up logging logging.basicConfig( format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", - level=get_app_config().log_level(), + level=app_config.log_level(), ) log = logging.getLogger(__name__) @@ -38,8 +41,8 @@ def _get_permissions(context: MiddlewareContext) -> None: api_instances: dict = context.fastmcp_context.get_state("api_instances") legacy_api_client: LegacySysdigApi = api_instances.get("legacy_sysdig_api") response = legacy_api_client.get_me_permissions() - if response.status != 200: - log.error(f"Error fetching permissions: Status {response.status} {legacy_api_client.api_client.configuration.host}") + if response.status != HTTPStatus.OK: + log.error(f"Error fetching permissions: Status {response.status}") raise Exception("Failed to fetch user permissions. Check your current Token and permissions.") context.fastmcp_context.set_state("permissions", response.json().get("permissions", [])) except Exception as e: diff --git a/utils/sysdig/legacy_sysdig_api.py b/utils/sysdig/legacy_sysdig_api.py index 16e2f64..f649cdc 100644 --- a/utils/sysdig/legacy_sysdig_api.py +++ b/utils/sysdig/legacy_sysdig_api.py @@ -82,6 +82,16 @@ def get_me_permissions(self) -> RESTResponseType: Retrieves the permissions for the current user. Returns: RESTResponseType: The response from the Sysdig API containing the user's permissions. + The response is typically a JSON object with a structure similar to: + { + "permissions": [ + "explore.read", + "scanning.read", + ... + ] + } + Each permission is a string representing an action the user is allowed to perform. + For a full list of possible permissions, refer to the Sysdig API documentation. """ url = f"{self.base}/users/me/permissions" resp = self.api_client.call_api("GET", url, header_params=self.headers) From 6a37c8fe58149e6dd1c47aab40a474ed7846b5a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Thu, 28 Aug 2025 11:22:49 +0200 Subject: [PATCH 09/11] chore: Remove helm charts --- .github/workflows/helm_test.yaml | 66 ---------- .github/workflows/publish.yaml | 11 +- README.md | 50 -------- charts/sysdig-mcp/.helmignore | 23 ---- charts/sysdig-mcp/Chart.yaml | 29 ----- charts/sysdig-mcp/templates/NOTES.txt | 22 ---- charts/sysdig-mcp/templates/_helpers.tpl | 62 --------- charts/sysdig-mcp/templates/deployment.yaml | 98 -------------- charts/sysdig-mcp/templates/hpa.yaml | 32 ----- charts/sysdig-mcp/templates/ingress.yaml | 61 --------- charts/sysdig-mcp/templates/secrets.yaml | 26 ---- charts/sysdig-mcp/templates/service.yaml | 15 --- .../sysdig-mcp/templates/serviceaccount.yaml | 13 -- .../templates/tests/test-connection.yaml | 15 --- charts/sysdig-mcp/values.schema.json | 120 ------------------ charts/sysdig-mcp/values.yaml | 109 ---------------- 16 files changed, 1 insertion(+), 751 deletions(-) delete mode 100644 .github/workflows/helm_test.yaml delete mode 100644 charts/sysdig-mcp/.helmignore delete mode 100644 charts/sysdig-mcp/Chart.yaml delete mode 100644 charts/sysdig-mcp/templates/NOTES.txt delete mode 100644 charts/sysdig-mcp/templates/_helpers.tpl delete mode 100644 charts/sysdig-mcp/templates/deployment.yaml delete mode 100644 charts/sysdig-mcp/templates/hpa.yaml delete mode 100644 charts/sysdig-mcp/templates/ingress.yaml delete mode 100644 charts/sysdig-mcp/templates/secrets.yaml delete mode 100644 charts/sysdig-mcp/templates/service.yaml delete mode 100644 charts/sysdig-mcp/templates/serviceaccount.yaml delete mode 100644 charts/sysdig-mcp/templates/tests/test-connection.yaml delete mode 100644 charts/sysdig-mcp/values.schema.json delete mode 100644 charts/sysdig-mcp/values.yaml diff --git a/.github/workflows/helm_test.yaml b/.github/workflows/helm_test.yaml deleted file mode 100644 index d47f4a2..0000000 --- a/.github/workflows/helm_test.yaml +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: Lint & Test helm chart - -on: - pull_request: - branches: - - main - paths: - - 'charts/**' - push: - branches: - - main - paths: - - 'charts/**' - workflow_call: - workflow_dispatch: - -concurrency: - group: 'helm-test-${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' - cancel-in-progress: true - -jobs: - lint-charts: - name: Lint new helm charts - runs-on: [ubuntu-latest] - steps: - - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Helm - uses: azure/setup-helm@v4 - with: - version: v3.13.3 - - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - check-latest: true - - - name: Set up chart-testing - uses: helm/chart-testing-action@v2.7.0 - with: - version: v3.13.0 - - - name: Run chart-testing (list-changed) - id: list-changed - run: | - changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }} --chart-dirs charts) - if [[ -n "$changed" ]]; then - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Run chart-testing (lint) - if: steps.list-changed.outputs.changed == 'true' - run: ct lint --target-branch ${{ github.event.repository.default_branch }} --chart-dirs charts - - - name: Create kind cluster - if: steps.list-changed.outputs.changed == 'true' && github.event_name != 'pull_request' - uses: helm/kind-action@v1.12.0 - - - name: Run chart-testing (install) - if: steps.list-changed.outputs.changed == 'true' && github.event_name != 'pull_request' - run: | - ct install --target-branch ${{ github.event.repository.default_branch }} --chart-dirs charts diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 3a1fb35..7560e22 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -103,13 +103,4 @@ jobs: run: | echo "## Release Summary - Tag: ${{ steps.semantic_release.outputs.tag }} - - Docker Image: ghcr.io/sysdiglabs/sysdig-mcp-server:v${{ needs.push_to_registry.outputs.version }}" >> $GITHUB_STEP_SUMMARY - - test_helm_chart: - name: Test Helm Chart - needs: push_to_registry - permissions: - contents: read # required for actions/checkout - pull-requests: write # required for creating a PR with the chart changes - uses: ./.github/workflows/helm_test.yaml - secrets: inherit + - Docker Image: ghcr.io/sysdiglabs/sysdig-mcp-server:v${{ needs.push_to_registry.outputs.version }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/README.md b/README.md index a393fb5..b053c8c 100644 --- a/README.md +++ b/README.md @@ -209,56 +209,6 @@ By default, the server will run using the `stdio` transport. To use the `streama docker run -e MCP_TRANSPORT=streamable-http -e SYSDIG_HOST= -e SYSDIG_SECURE_TOKEN= -p 8080:8080 sysdig-mcp-server ``` -### K8s Deployment - -If you want to run the Sysdig MCP server in a K8s cluster you can use the helm chart provided in the `charts/sysdig-mcp` path - -Modify the `values.yaml` - -```yaml -# Example values.yaml ---- -sysdig: - secrets: - create: true - # If enabled, the secrets will be mounted as environment variables - secureAPIToken: "" - mcp: - transport: "streamable-http" - host: "https://us2.app.sysdig.com" # "https://eu1.app.sysdig.com" - -configMap: - enabled: true - app_config: | - # Sysdig MCP Server Configuration - # This file is used to configure the Sysdig MCP server. - # You can add your custom configuration here. - app: - host: "0.0.0.0" - port: 8080 - log_level: "error" - - sysdig: - host: "https://us2.app.sysdig.com" # "https://eu1.app.sysdig.com" - - mcp: - transport: streamable-http - host: "0.0.0.0" - port: 8080 - allowed_tools: - - "events-feed" - - "inventory" - - "vulnerability-management" - - "sysdig-sage" - - "sysdig-cli-scanner" # You need the sysdig-cli-scanner binary installed in your server to use this tool -``` - -Install the chart - -```bash,copy -helm upgrade --install sysdig-mcp ./charts/sysdig-mcp/ -n sysdig-mcp -f charts/sysdig-mcp/values.yaml -``` - ### UV To run the server using `uv`, first set up the environment as described in the [UV Setup](#uv-setup) section. Then, run the `main.py` script: diff --git a/charts/sysdig-mcp/.helmignore b/charts/sysdig-mcp/.helmignore deleted file mode 100644 index 0e8a0eb..0000000 --- a/charts/sysdig-mcp/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/charts/sysdig-mcp/Chart.yaml b/charts/sysdig-mcp/Chart.yaml deleted file mode 100644 index 9044449..0000000 --- a/charts/sysdig-mcp/Chart.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: v2 -name: sysdig-mcp -description: A Helm chart to deploy the Sysdig MCP server -maintainers: - - name: S3B4SZ17 - email: sebastian.zumbado@sysdig.com - - name: alecron - email: alejandro.magallon@sysdig.com - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "v0.2.0" diff --git a/charts/sysdig-mcp/templates/NOTES.txt b/charts/sysdig-mcp/templates/NOTES.txt deleted file mode 100644 index fe2d134..0000000 --- a/charts/sysdig-mcp/templates/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sysdig-mcp.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sysdig-mcp.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sysdig-mcp.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sysdig-mcp.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/charts/sysdig-mcp/templates/_helpers.tpl b/charts/sysdig-mcp/templates/_helpers.tpl deleted file mode 100644 index 921c181..0000000 --- a/charts/sysdig-mcp/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "sysdig-mcp.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "sysdig-mcp.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "sysdig-mcp.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "sysdig-mcp.labels" -}} -helm.sh/chart: {{ include "sysdig-mcp.chart" . }} -{{ include "sysdig-mcp.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "sysdig-mcp.selectorLabels" -}} -app.kubernetes.io/name: {{ include "sysdig-mcp.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "sysdig-mcp.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "sysdig-mcp.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/charts/sysdig-mcp/templates/deployment.yaml b/charts/sysdig-mcp/templates/deployment.yaml deleted file mode 100644 index 0e0ea59..0000000 --- a/charts/sysdig-mcp/templates/deployment.yaml +++ /dev/null @@ -1,98 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "sysdig-mcp.fullname" . }} - labels: - {{- include "sysdig-mcp.labels" . | nindent 4 }} -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "sysdig-mcp.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "sysdig-mcp.labels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "sysdig-mcp.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - - name: SYSDIG_MCP_API_HOST - value: {{ .Values.sysdig.host | quote }} - {{- if .Values.sysdig.secrets.create }} - - name: SYSDIG_MCP_API_SECURE_TOKEN - valueFrom: - secretKeyRef: - name: "{{ include "sysdig-mcp.fullname" . }}-sysdig-secrets" - key: SYSDIG_MCP_API_SECURE_TOKEN - {{- end }} - {{- if .Values.oauth.secrets.create }} - - name: SYSDIG_MCP_OAUTH_CLIENT_ID - valueFrom: - secretKeyRef: - name: "{{ include "sysdig-mcp.fullname" . }}-oauth-secrets" - key: clientId - - name: SYSDIG_MCP_OAUTH_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: "{{ include "sysdig-mcp.fullname" . }}-oauth-secrets" - key: clientSecret - {{- end }} - - name: SYSDIG_MCP_TRANSPORT - value: {{ .Values.sysdig.mcp.transport | quote }} - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - livenessProbe: - httpGet: - path: /healthz - port: http - periodSeconds: 60 - readinessProbe: - httpGet: - path: /healthz - port: http - periodSeconds: 60 - resources: - {{- toYaml .Values.resources | nindent 12 }} - volumeMounts: - {{- with .Values.volumeMounts }} - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - {{- with .Values.volumes }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/charts/sysdig-mcp/templates/hpa.yaml b/charts/sysdig-mcp/templates/hpa.yaml deleted file mode 100644 index d28cd73..0000000 --- a/charts/sysdig-mcp/templates/hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "sysdig-mcp.fullname" . }} - labels: - {{- include "sysdig-mcp.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "sysdig-mcp.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/charts/sysdig-mcp/templates/ingress.yaml b/charts/sysdig-mcp/templates/ingress.yaml deleted file mode 100644 index 887baac..0000000 --- a/charts/sysdig-mcp/templates/ingress.yaml +++ /dev/null @@ -1,61 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "sysdig-mcp.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} - {{- end }} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "sysdig-mcp.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} - pathType: {{ .pathType }} - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} - service: - name: {{ $fullName }} - port: - number: {{ $svcPort }} - {{- else }} - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} diff --git a/charts/sysdig-mcp/templates/secrets.yaml b/charts/sysdig-mcp/templates/secrets.yaml deleted file mode 100644 index 7420c1e..0000000 --- a/charts/sysdig-mcp/templates/secrets.yaml +++ /dev/null @@ -1,26 +0,0 @@ -{{- if .Values.sysdig.secrets.create -}} -apiVersion: v1 -kind: Secret -metadata: - name: "{{ include "sysdig-mcp.fullname" . }}-sysdig-secrets" - labels: - {{- include "sysdig-mcp.labels" . | nindent 4 }} - release: {{ .Release.Name }} -type: Opaque -data: - SYSDIG_MCP_API_SECURE_TOKEN: {{ .Values.sysdig.secrets.secureAPIToken | b64enc }} -{{- end }} ---- -{{- if .Values.oauth.secrets.create -}} -apiVersion: v1 -kind: Secret -metadata: - name: "{{ include "sysdig-mcp.fullname" . }}-oauth-secrets" - labels: - {{- include "sysdig-mcp.labels" . | nindent 4 }} - release: {{ .Release.Name }} -type: Opaque -data: - clientId: {{ .Values.oauth.secrets.clientId | b64enc }} - clientSecret: {{ .Values.oauth.secrets.clientSecret | b64enc }} -{{- end }} diff --git a/charts/sysdig-mcp/templates/service.yaml b/charts/sysdig-mcp/templates/service.yaml deleted file mode 100644 index 16f4c7a..0000000 --- a/charts/sysdig-mcp/templates/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "sysdig-mcp.fullname" . }} - labels: - {{- include "sysdig-mcp.labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "sysdig-mcp.selectorLabels" . | nindent 4 }} diff --git a/charts/sysdig-mcp/templates/serviceaccount.yaml b/charts/sysdig-mcp/templates/serviceaccount.yaml deleted file mode 100644 index ec77e92..0000000 --- a/charts/sysdig-mcp/templates/serviceaccount.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "sysdig-mcp.serviceAccountName" . }} - labels: - {{- include "sysdig-mcp.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automount }} -{{- end }} diff --git a/charts/sysdig-mcp/templates/tests/test-connection.yaml b/charts/sysdig-mcp/templates/tests/test-connection.yaml deleted file mode 100644 index ef513d0..0000000 --- a/charts/sysdig-mcp/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "sysdig-mcp.fullname" . }}-test-connection" - labels: - {{- include "sysdig-mcp.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "sysdig-mcp.fullname" . }}:{{ .Values.service.port }}/healthz'] - restartPolicy: Never diff --git a/charts/sysdig-mcp/values.schema.json b/charts/sysdig-mcp/values.schema.json deleted file mode 100644 index 76c69e3..0000000 --- a/charts/sysdig-mcp/values.schema.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft-07/schema#", - "title": "Values", - "type": "object", - "properties": { - "sysdig": { - "$ref": "#/$defs/SysdigConfig" - }, - "oauth": { - "$ref": "#/$defs/OauthConfig" - } - }, - "required": [ - "sysdig" - ], - "$defs": { - "SysdigConfig": { - "type": "object", - "properties": { - "host": { - "type": [ "string", "null" ], - "description": "Sysdig Tenant Host", - "examples": [ - "https://us2.app.sysdig.com", - "https://eu1.app.sysdig.com" - ] - }, - "mcp": { - "type": "object", - "properties": { - "transport": { - "type": "string", - "enum": [ - "streamable-http", - "sse", - "stdio" - ], - "description": "The transport protocol for the Sysdig MCP" - } - }, - "required": [ - "transport" - ] - }, - "secrets": { - "type": "object", - "properties": { - "create": { - "type": "boolean", - "description": "Whether to create the secret" - }, - "secureAPIToken": { - "type": [ - "string", - "null" - ], - "description": "The API Token to access Sysdig Secure", - "examples": [ - "12345678-1234-1234-1234-123456789012" - ] - } - }, - "required": [ - "create", - "secureAPIToken" - ] - } - }, - "required": [ - "host", - "mcp", - "secrets" - ], - "additionalProperties": false - }, - "OauthConfig": { - "type": "object", - "properties": { - "secrets": { - "type": "object", - "properties": { - "create": { - "type": "boolean", - "description": "Whether to create the secret" - }, - "clientId": { - "type": [ - "string", - "null" - ], - "description": "The Client ID for the OAuth application", - "examples": [ - "my-client-id" - ] - }, - "clientSecret": { - "type": [ - "string", - "null" - ], - "description": "The Client Secret for the OAuth application", - "examples": [ - "my-client-secret" - ] - } - }, - "required": [ - "create", - "clientId", - "clientSecret" - ] - } - }, - "required": [ - "secrets" - ], - "additionalProperties": false - } - } -} diff --git a/charts/sysdig-mcp/values.yaml b/charts/sysdig-mcp/values.yaml deleted file mode 100644 index c478a8c..0000000 --- a/charts/sysdig-mcp/values.yaml +++ /dev/null @@ -1,109 +0,0 @@ -# Default values for sysdig-mcp. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 1 - -image: - repository: ghcr.io/sysdiglabs/sysdig-mcp-server - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "v0.2.0" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Automatically mount a ServiceAccount's API credentials? - automount: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -sysdig: - secrets: - create: true - # If enabled, the Sysdig Secure API token will be mounted as an environment variable - secureAPIToken: "YOUR_SECURE_API_TOKEN" - mcp: - transport: "streamable-http" - host: "https://us2.app.sysdig.com" -oauth: - secrets: - create: false - # If enabled, the OAuth client ID and secret will be mounted as environment variables - clientId: "YOUR_CLIENT_ID" - clientSecret: "YOUR_CLIENT_SECRET" - -podAnnotations: {} -podLabels: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: - readOnlyRootFilesystem: false - runAsNonRoot: true - runAsUser: 1001 - runAsGroup: 1001 - -service: - type: ClusterIP - port: 8080 - -ingress: - enabled: true - className: "nginx" - annotations: - kubernetes.io/ingress.class: nginx - nginx.ingress.kubernetes.io/rewrite-target: /$2 - hosts: - - paths: - - path: /sysdig-mcp(/|$)(.*) - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -# Additional volumes on the output Deployment definition. -volumes: [] - # - name: config - # configMap: - # name: sysdig-mcp-config - -# Additional volumeMounts on the output Deployment definition. -volumeMounts: [] - # - name: config - # mountPath: /app/config - # readOnly: true - -nodeSelector: {} - -tolerations: [] - -affinity: {} From 8a443e91dad14f563c4736b818963034e6cd3b99 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Thu, 28 Aug 2025 11:34:56 +0200 Subject: [PATCH 10/11] build: pin only to compatible versions in pyproject and let the uv.lock manage pinning --- pyproject.toml | 24 +- uv.lock | 583 ++++++++++++++++++++++++++----------------------- 2 files changed, 319 insertions(+), 288 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index da47b23..2173422 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,16 +5,16 @@ description = "Sysdig MCP Server" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "mcp[cli]==1.12.4", - "python-dotenv>=1.1.0", - "pyyaml==6.0.2", - "sqlalchemy==2.0.36", - "sqlmodel==0.0.22", + "mcp[cli]~=1.12", + "python-dotenv~=1.1", + "pyyaml~=6.0", + "sqlalchemy~=2.0", + "sqlmodel~=0.0.22", "sysdig-sdk-python @ git+https://github.com/sysdiglabs/sysdig-sdk-python@852ee2ccad12a8b445dd4732e7f3bd44d78a37f7", - "dask==2025.4.1", - "oauthlib==3.2.2", - "fastapi==0.116.1", - "fastmcp==2.11.3", + "dask~=2025.4", + "oauthlib~=3.2", + "fastapi~=0.116.1", + "fastmcp~=2.11", "requests", ] @@ -23,9 +23,9 @@ sysdig-mcp-server = "main:main" [tool.uv] dev-dependencies = [ - "pytest-cov==6.2.1", - "pytest==8.4.1", - "ruff==0.12.1", + "pytest-cov~=6.2", + "pytest~=8.4", + "ruff~=0.12.1", ] [build-system] diff --git a/uv.lock b/uv.lock index f27d5a1..35ff3cb 100644 --- a/uv.lock +++ b/uv.lock @@ -13,16 +13,16 @@ wheels = [ [[package]] name = "anyio" -version = "4.9.0" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, ] [[package]] @@ -36,23 +36,23 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.2" +version = "1.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/95/e4f4ab5ce465821fe2229e10985ab80462941fe5d96387ae76bafd36f0ba/authlib-1.6.2.tar.gz", hash = "sha256:3bde83ac0392683eeef589cd5ab97e63cbe859e552dd75dca010548e79202cb1", size = 160429, upload-time = "2025-08-23T08:34:32.665Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/c6/d9a9db2e71957827e23a34322bde8091b51cb778dcc38885b84c772a1ba9/authlib-1.6.3.tar.gz", hash = "sha256:9f7a982cc395de719e4c2215c5707e7ea690ecf84f1ab126f28c053f4219e610", size = 160836, upload-time = "2025-08-26T12:13:25.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/00/fb65909bf4c8d7da893a12006074343402a8dc8c00d916b3cee524d97f3f/authlib-1.6.2-py2.py3-none-any.whl", hash = "sha256:2dd5571013cacf6b15f7addce03ed057ffdf629e9e81bacd9c08455a190e9b57", size = 239601, upload-time = "2025-08-23T08:34:31.4Z" }, + { url = "https://files.pythonhosted.org/packages/25/2f/efa9d26dbb612b774990741fd8f13c7cf4cfd085b870e4a5af5c82eaf5f1/authlib-1.6.3-py2.py3-none-any.whl", hash = "sha256:7ea0f082edd95a03b7b72edac65ec7f8f68d703017d7e37573aee4fc603f2a48", size = 240105, upload-time = "2025-08-26T12:13:23.889Z" }, ] [[package]] name = "certifi" -version = "2025.6.15" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] @@ -90,37 +90,44 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] @@ -155,44 +162,66 @@ wheels = [ [[package]] name = "coverage" -version = "7.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, - { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, - { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, - { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a7/a027970c991ca90f24e968999f7d509332daf6b8c3533d68633930aaebac/coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", size = 212358, upload-time = "2025-06-13T13:01:30.909Z" }, - { url = "https://files.pythonhosted.org/packages/f2/48/6aaed3651ae83b231556750280682528fea8ac7f1232834573472d83e459/coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", size = 212620, upload-time = "2025-06-13T13:01:32.256Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/f4b613f3b44d8b9f144847c89151992b2b6b79cbc506dee89ad0c35f209d/coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", size = 245788, upload-time = "2025-06-13T13:01:33.948Z" }, - { url = "https://files.pythonhosted.org/packages/04/d2/de4fdc03af5e4e035ef420ed26a703c6ad3d7a07aff2e959eb84e3b19ca8/coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86", size = 243001, upload-time = "2025-06-13T13:01:35.285Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e8/eed18aa5583b0423ab7f04e34659e51101135c41cd1dcb33ac1d7013a6d6/coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43", size = 244985, upload-time = "2025-06-13T13:01:36.712Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/ae9e5cce8885728c934eaa58ebfa8281d488ef2afa81c3dbc8ee9e6d80db/coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1", size = 245152, upload-time = "2025-06-13T13:01:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c8/272c01ae792bb3af9b30fac14d71d63371db227980682836ec388e2c57c0/coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751", size = 243123, upload-time = "2025-06-13T13:01:40.727Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/2819a1e3086143c094ab446e3bdf07138527a7b88cb235c488e78150ba7a/coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67", size = 244506, upload-time = "2025-06-13T13:01:42.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/4e/9f6117b89152df7b6112f65c7a4ed1f2f5ec8e60c4be8f351d91e7acc848/coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643", size = 214766, upload-time = "2025-06-13T13:01:44.482Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/4b59f7c93b52c2c4ce7387c5a4e135e49891bb3b7408dcc98fe44033bbe0/coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a", size = 215568, upload-time = "2025-06-13T13:01:45.772Z" }, - { url = "https://files.pythonhosted.org/packages/09/1e/9679826336f8c67b9c39a359352882b24a8a7aee48d4c9cad08d38d7510f/coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d", size = 213939, upload-time = "2025-06-13T13:01:47.087Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5b/5c6b4e7a407359a2e3b27bf9c8a7b658127975def62077d441b93a30dbe8/coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0", size = 213079, upload-time = "2025-06-13T13:01:48.554Z" }, - { url = "https://files.pythonhosted.org/packages/a2/22/1e2e07279fd2fd97ae26c01cc2186e2258850e9ec125ae87184225662e89/coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d", size = 213299, upload-time = "2025-06-13T13:01:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/14/c0/4c5125a4b69d66b8c85986d3321520f628756cf524af810baab0790c7647/coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f", size = 256535, upload-time = "2025-06-13T13:01:51.314Z" }, - { url = "https://files.pythonhosted.org/packages/81/8b/e36a04889dda9960be4263e95e777e7b46f1bb4fc32202612c130a20c4da/coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029", size = 252756, upload-time = "2025-06-13T13:01:54.403Z" }, - { url = "https://files.pythonhosted.org/packages/98/82/be04eff8083a09a4622ecd0e1f31a2c563dbea3ed848069e7b0445043a70/coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece", size = 254912, upload-time = "2025-06-13T13:01:56.769Z" }, - { url = "https://files.pythonhosted.org/packages/0f/25/c26610a2c7f018508a5ab958e5b3202d900422cf7cdca7670b6b8ca4e8df/coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683", size = 256144, upload-time = "2025-06-13T13:01:58.19Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8b/fb9425c4684066c79e863f1e6e7ecebb49e3a64d9f7f7860ef1688c56f4a/coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f", size = 254257, upload-time = "2025-06-13T13:01:59.645Z" }, - { url = "https://files.pythonhosted.org/packages/93/df/27b882f54157fc1131e0e215b0da3b8d608d9b8ef79a045280118a8f98fe/coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10", size = 255094, upload-time = "2025-06-13T13:02:01.37Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437, upload-time = "2025-06-13T13:02:02.905Z" }, - { url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605, upload-time = "2025-06-13T13:02:05.638Z" }, - { url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392, upload-time = "2025-06-13T13:02:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, +version = "7.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077, upload-time = "2025-08-23T14:40:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310, upload-time = "2025-08-23T14:41:00.628Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2c/fbecd8381e0a07d1547922be819b4543a901402f63930313a519b937c668/coverage-7.10.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd382410039fe062097aa0292ab6335a3f1e7af7bba2ef8d27dcda484918f20c", size = 248802, upload-time = "2025-08-23T14:41:02.012Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550, upload-time = "2025-08-23T14:41:03.438Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684, upload-time = "2025-08-23T14:41:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602, upload-time = "2025-08-23T14:41:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2d/548c8e04249cbba3aba6bd799efdd11eee3941b70253733f5d355d689559/coverage-7.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2285c04ee8676f7938b02b4936d9b9b672064daab3187c20f73a55f3d70e6b4a", size = 248724, upload-time = "2025-08-23T14:41:08.429Z" }, + { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158, upload-time = "2025-08-23T14:41:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/74d4be58c70c42ef0b352d597b022baf12dbe2b43e7cb1525f56a0fb1d4b/coverage-7.10.5-cp312-cp312-win32.whl", hash = "sha256:38a9109c4ee8135d5df5505384fc2f20287a47ccbe0b3f04c53c9a1989c2bbaf", size = 219493, upload-time = "2025-08-23T14:41:11.095Z" }, + { url = "https://files.pythonhosted.org/packages/4f/08/364e6012d1d4d09d1e27437382967efed971d7613f94bca9add25f0c1f2b/coverage-7.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:6b87f1ad60b30bc3c43c66afa7db6b22a3109902e28c5094957626a0143a001f", size = 220302, upload-time = "2025-08-23T14:41:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/7c8a365e1f7355c58af4fe5faf3f90cc8e587590f5854808d17ccb4e7077/coverage-7.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:672a6c1da5aea6c629819a0e1461e89d244f78d7b60c424ecf4f1f2556c041d8", size = 218936, upload-time = "2025-08-23T14:41:13.872Z" }, + { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106, upload-time = "2025-08-23T14:41:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353, upload-time = "2025-08-23T14:41:16.656Z" }, + { url = "https://files.pythonhosted.org/packages/74/68/21e0d254dbf8972bb8dd95e3fe7038f4be037ff04ba47d6d1b12b37510ba/coverage-7.10.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63df1fdaffa42d914d5c4d293e838937638bf75c794cf20bee12978fc8c4e3bc", size = 248350, upload-time = "2025-08-23T14:41:18.128Z" }, + { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955, upload-time = "2025-08-23T14:41:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230, upload-time = "2025-08-23T14:41:20.959Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387, upload-time = "2025-08-23T14:41:22.644Z" }, + { url = "https://files.pythonhosted.org/packages/c7/89/97a9e271188c2fbb3db82235c33980bcbc733da7da6065afbaa1d685a169/coverage-7.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f39cef43d08049e8afc1fde4a5da8510fc6be843f8dea350ee46e2a26b2f54c", size = 248280, upload-time = "2025-08-23T14:41:24.061Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894, upload-time = "2025-08-23T14:41:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/56/fb3aba936addb4c9e5ea14f5979393f1c2466b4c89d10591fd05f2d6b2aa/coverage-7.10.5-cp313-cp313-win32.whl", hash = "sha256:0d511dda38595b2b6934c2b730a1fd57a3635c6aa2a04cb74714cdfdd53846f4", size = 219536, upload-time = "2025-08-23T14:41:27.694Z" }, + { url = "https://files.pythonhosted.org/packages/fc/54/baacb8f2f74431e3b175a9a2881feaa8feb6e2f187a0e7e3046f3c7742b2/coverage-7.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:9a86281794a393513cf117177fd39c796b3f8e3759bb2764259a2abba5cce54b", size = 220330, upload-time = "2025-08-23T14:41:29.081Z" }, + { url = "https://files.pythonhosted.org/packages/64/8a/82a3788f8e31dee51d350835b23d480548ea8621f3effd7c3ba3f7e5c006/coverage-7.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:cebd8e906eb98bb09c10d1feed16096700b1198d482267f8bf0474e63a7b8d84", size = 218961, upload-time = "2025-08-23T14:41:30.511Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819, upload-time = "2025-08-23T14:41:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040, upload-time = "2025-08-23T14:41:33.472Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ca/5787fb3d7820e66273913affe8209c534ca11241eb34ee8c4fd2aaa9dd87/coverage-7.10.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0913dd1613a33b13c4f84aa6e3f4198c1a21ee28ccb4f674985c1f22109f0aae", size = 259374, upload-time = "2025-08-23T14:41:34.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551, upload-time = "2025-08-23T14:41:36.333Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776, upload-time = "2025-08-23T14:41:38.25Z" }, + { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326, upload-time = "2025-08-23T14:41:40.343Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c4/bf8d459fb4ce2201e9243ce6c015936ad283a668774430a3755f467b39d1/coverage-7.10.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:42144e8e346de44a6f1dbd0a56575dd8ab8dfa7e9007da02ea5b1c30ab33a7db", size = 259090, upload-time = "2025-08-23T14:41:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217, upload-time = "2025-08-23T14:41:43.591Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/87560f036099f46c2ddd235be6476dd5c1d6be6bb57569a9348d43eeecea/coverage-7.10.5-cp313-cp313t-win32.whl", hash = "sha256:2d1b73023854068c44b0c554578a4e1ef1b050ed07cf8b431549e624a29a66ee", size = 220194, upload-time = "2025-08-23T14:41:45.051Z" }, + { url = "https://files.pythonhosted.org/packages/36/a8/04a482594fdd83dc677d4a6c7e2d62135fff5a1573059806b8383fad9071/coverage-7.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:54a1532c8a642d8cc0bd5a9a51f5a9dcc440294fd06e9dda55e743c5ec1a8f14", size = 221258, upload-time = "2025-08-23T14:41:46.44Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ad/7da28594ab66fe2bc720f1bc9b131e62e9b4c6e39f044d9a48d18429cc21/coverage-7.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:74d5b63fe3f5f5d372253a4ef92492c11a4305f3550631beaa432fc9df16fcff", size = 219521, upload-time = "2025-08-23T14:41:47.882Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7f/c8b6e4e664b8a95254c35a6c8dd0bf4db201ec681c169aae2f1256e05c85/coverage-7.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:68c5e0bc5f44f68053369fa0d94459c84548a77660a5f2561c5e5f1e3bed7031", size = 217090, upload-time = "2025-08-23T14:41:49.327Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/3ee14ede30a6e10a94a104d1d0522d5fb909a7c7cac2643d2a79891ff3b9/coverage-7.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf33134ffae93865e32e1e37df043bef15a5e857d8caebc0099d225c579b0fa3", size = 217365, upload-time = "2025-08-23T14:41:50.796Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/06ac21bf87dfb7620d1f870dfa3c2cae1186ccbcdc50b8b36e27a0d52f50/coverage-7.10.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad8fa9d5193bafcf668231294241302b5e683a0518bf1e33a9a0dfb142ec3031", size = 248413, upload-time = "2025-08-23T14:41:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/cc5bed6e985d3a14228539631573f3863be6a2587381e8bc5fdf786377a1/coverage-7.10.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:146fa1531973d38ab4b689bc764592fe6c2f913e7e80a39e7eeafd11f0ef6db2", size = 250943, upload-time = "2025-08-23T14:41:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/43/6a9fc323c2c75cd80b18d58db4a25dc8487f86dd9070f9592e43e3967363/coverage-7.10.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6013a37b8a4854c478d3219ee8bc2392dea51602dd0803a12d6f6182a0061762", size = 252301, upload-time = "2025-08-23T14:41:56.528Z" }, + { url = "https://files.pythonhosted.org/packages/69/7c/3e791b8845f4cd515275743e3775adb86273576596dc9f02dca37357b4f2/coverage-7.10.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eb90fe20db9c3d930fa2ad7a308207ab5b86bf6a76f54ab6a40be4012d88fcae", size = 250302, upload-time = "2025-08-23T14:41:58.171Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bc/5099c1e1cb0c9ac6491b281babea6ebbf999d949bf4aa8cdf4f2b53505e8/coverage-7.10.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:384b34482272e960c438703cafe63316dfbea124ac62006a455c8410bf2a2262", size = 248237, upload-time = "2025-08-23T14:41:59.703Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/d346eb750a0b2f1e77f391498b753ea906fde69cc11e4b38dca28c10c88c/coverage-7.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:467dc74bd0a1a7de2bedf8deaf6811f43602cb532bd34d81ffd6038d6d8abe99", size = 249726, upload-time = "2025-08-23T14:42:01.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/85/eebcaa0edafe427e93286b94f56ea7e1280f2c49da0a776a6f37e04481f9/coverage-7.10.5-cp314-cp314-win32.whl", hash = "sha256:556d23d4e6393ca898b2e63a5bca91e9ac2d5fb13299ec286cd69a09a7187fde", size = 219825, upload-time = "2025-08-23T14:42:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f7/6d43e037820742603f1e855feb23463979bf40bd27d0cde1f761dcc66a3e/coverage-7.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:f4446a9547681533c8fa3e3c6cf62121eeee616e6a92bd9201c6edd91beffe13", size = 220618, upload-time = "2025-08-23T14:42:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b0/ed9432e41424c51509d1da603b0393404b828906236fb87e2c8482a93468/coverage-7.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e78bd9cf65da4c303bf663de0d73bf69f81e878bf72a94e9af67137c69b9fe9", size = 219199, upload-time = "2025-08-23T14:42:06.662Z" }, + { url = "https://files.pythonhosted.org/packages/2f/54/5a7ecfa77910f22b659c820f67c16fc1e149ed132ad7117f0364679a8fa9/coverage-7.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5661bf987d91ec756a47c7e5df4fbcb949f39e32f9334ccd3f43233bbb65e508", size = 217833, upload-time = "2025-08-23T14:42:08.262Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/25672d917cc57857d40edf38f0b867fb9627115294e4f92c8fcbbc18598d/coverage-7.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a46473129244db42a720439a26984f8c6f834762fc4573616c1f37f13994b357", size = 218048, upload-time = "2025-08-23T14:42:10.247Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7c/0b2b4f1c6f71885d4d4b2b8608dcfc79057adb7da4143eb17d6260389e42/coverage-7.10.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1f64b8d3415d60f24b058b58d859e9512624bdfa57a2d1f8aff93c1ec45c429b", size = 259549, upload-time = "2025-08-23T14:42:11.811Z" }, + { url = "https://files.pythonhosted.org/packages/94/73/abb8dab1609abec7308d83c6aec547944070526578ee6c833d2da9a0ad42/coverage-7.10.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:44d43de99a9d90b20e0163f9770542357f58860a26e24dc1d924643bd6aa7cb4", size = 261715, upload-time = "2025-08-23T14:42:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d1/abf31de21ec92731445606b8d5e6fa5144653c2788758fcf1f47adb7159a/coverage-7.10.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a931a87e5ddb6b6404e65443b742cb1c14959622777f2a4efd81fba84f5d91ba", size = 263969, upload-time = "2025-08-23T14:42:15.422Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b3/ef274927f4ebede96056173b620db649cc9cb746c61ffc467946b9d0bc67/coverage-7.10.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9559b906a100029274448f4c8b8b0a127daa4dade5661dfd821b8c188058842", size = 261408, upload-time = "2025-08-23T14:42:16.971Z" }, + { url = "https://files.pythonhosted.org/packages/20/fc/83ca2812be616d69b4cdd4e0c62a7bc526d56875e68fd0f79d47c7923584/coverage-7.10.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b08801e25e3b4526ef9ced1aa29344131a8f5213c60c03c18fe4c6170ffa2874", size = 259168, upload-time = "2025-08-23T14:42:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/e0779e5716f72d5c9962e709d09815d02b3b54724e38567308304c3fc9df/coverage-7.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed9749bb8eda35f8b636fb7632f1c62f735a236a5d4edadd8bbcc5ea0542e732", size = 260317, upload-time = "2025-08-23T14:42:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600, upload-time = "2025-08-23T14:42:22.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714, upload-time = "2025-08-23T14:42:23.616Z" }, + { url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735, upload-time = "2025-08-23T14:42:25.156Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, ] [[package]] @@ -247,7 +276,7 @@ wheels = [ [[package]] name = "dask" -version = "2025.4.1" +version = "2025.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -258,9 +287,9 @@ dependencies = [ { name = "pyyaml" }, { name = "toolz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/62/07d1dadcfa41c9f5584aa1ab10f4042d2cdb2c6655b123800d9f30185b71/dask-2025.4.1.tar.gz", hash = "sha256:3b4b5d6e29d858c48339a5b9a99c39f11cb44111d3836d77ff32da51e0f51243", size = 10963890, upload-time = "2025-04-25T20:39:32.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/00/cbb7cb07742955dfe77368aa40725d7000414a8a49f415ba40c5a4379ba9/dask-2025.7.0.tar.gz", hash = "sha256:c3a0d4e78882e85ea81dbc71e6459713e45676e2d17e776c2f3f19848039e4cf", size = 10972242, upload-time = "2025-07-14T20:03:42.701Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/12/f9effea5fe2bebfdd8b0d9c857f798382afacd57dc1cd0e9ce21e66c1bc2/dask-2025.4.1-py3-none-any.whl", hash = "sha256:aacbb0a9667856fe58385015efd64aca22f0c0b2c5e1b5e633531060303bb4be", size = 1471761, upload-time = "2025-04-25T20:39:20.725Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f9/3e04725358c17329652da8c1b2dbd88de723f3dc78bf52ca6d28d52c9279/dask-2025.7.0-py3-none-any.whl", hash = "sha256:073c29c4e99c2400a39a8a67874f35c1d15bf7af9ae3d0c30af3c7c1199f24ae", size = 1475117, upload-time = "2025-07-14T20:03:33.095Z" }, ] [[package]] @@ -292,15 +321,15 @@ wheels = [ [[package]] name = "email-validator" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] [[package]] @@ -353,44 +382,44 @@ wheels = [ [[package]] name = "fsspec" -version = "2025.5.1" +version = "2025.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033, upload-time = "2025-05-24T12:03:23.792Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/02/0835e6ab9cfc03916fe3f78c0956cfcdb6ff2669ffa6651065d5ebf7fc98/fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58", size = 304432, upload-time = "2025-07-15T16:05:21.19Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597, upload-time = "2025-07-15T16:05:19.529Z" }, ] [[package]] name = "greenlet" -version = "3.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, - { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, - { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, - { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, - { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, - { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, - { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, - { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, - { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, - { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" }, - { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" }, - { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" }, - { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" }, - { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] [[package]] @@ -551,14 +580,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] @@ -601,7 +630,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.12.4" +version = "1.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -616,9 +645,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/88/f6cb7e7c260cd4b4ce375f2b1614b33ce401f63af0f49f7141a2e9bf0a45/mcp-1.12.4.tar.gz", hash = "sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5", size = 431148, upload-time = "2025-08-07T20:31:18.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/3c/82c400c2d50afdac4fbefb5b4031fd327e2ad1f23ccef8eee13c5909aa48/mcp-1.13.1.tar.gz", hash = "sha256:165306a8fd7991dc80334edd2de07798175a56461043b7ae907b279794a834c5", size = 438198, upload-time = "2025-08-22T09:22:16.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/68/316cbc54b7163fa22571dcf42c9cc46562aae0a021b974e0a8141e897200/mcp-1.12.4-py3-none-any.whl", hash = "sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789", size = 160145, upload-time = "2025-08-07T20:31:15.69Z" }, + { url = "https://files.pythonhosted.org/packages/19/3f/d085c7f49ade6d273b185d61ec9405e672b6433f710ea64a90135a8dd445/mcp-1.13.1-py3-none-any.whl", hash = "sha256:c314e7c8bd477a23ba3ef472ee5a32880316c42d03e06dcfa31a1cc7a73b65df", size = 161494, upload-time = "2025-08-22T09:22:14.705Z" }, ] [package.optional-dependencies] @@ -647,11 +676,11 @@ wheels = [ [[package]] name = "oauthlib" -version = "3.2.2" +version = "3.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352, upload-time = "2022-10-17T20:04:27.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688, upload-time = "2022-10-17T20:04:24.037Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] [[package]] @@ -982,7 +1011,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -990,9 +1019,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -1009,15 +1038,15 @@ wheels = [ [[package]] name = "rich" -version = "14.0.0" +version = "14.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, ] [[package]] @@ -1035,108 +1064,109 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.27.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" }, - { url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" }, - { url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" }, - { url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" }, - { url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" }, - { url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" }, - { url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" }, - { url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" }, - { url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" }, - { url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" }, - { url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" }, - { url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" }, - { url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" }, - { url = "https://files.pythonhosted.org/packages/81/d2/dfdfd42565a923b9e5a29f93501664f5b984a802967d48d49200ad71be36/rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", size = 362133, upload-time = "2025-08-07T08:24:04.508Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/0a2e2460c4b66021d349ce9f6331df1d6c75d7eea90df9785d333a49df04/rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", size = 347128, upload-time = "2025-08-07T08:24:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/35/8d/7d1e4390dfe09d4213b3175a3f5a817514355cb3524593380733204f20b9/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", size = 384027, upload-time = "2025-08-07T08:24:06.841Z" }, - { url = "https://files.pythonhosted.org/packages/c1/65/78499d1a62172891c8cd45de737b2a4b84a414b6ad8315ab3ac4945a5b61/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc", size = 399973, upload-time = "2025-08-07T08:24:08.143Z" }, - { url = "https://files.pythonhosted.org/packages/10/a1/1c67c1d8cc889107b19570bb01f75cf49852068e95e6aee80d22915406fc/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe", size = 515295, upload-time = "2025-08-07T08:24:09.711Z" }, - { url = "https://files.pythonhosted.org/packages/df/27/700ec88e748436b6c7c4a2262d66e80f8c21ab585d5e98c45e02f13f21c0/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9", size = 406737, upload-time = "2025-08-07T08:24:11.182Z" }, - { url = "https://files.pythonhosted.org/packages/33/cc/6b0ee8f0ba3f2df2daac1beda17fde5cf10897a7d466f252bd184ef20162/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c", size = 385898, upload-time = "2025-08-07T08:24:12.798Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7e/c927b37d7d33c0a0ebf249cc268dc2fcec52864c1b6309ecb960497f2285/rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295", size = 405785, upload-time = "2025-08-07T08:24:14.906Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d2/8ed50746d909dcf402af3fa58b83d5a590ed43e07251d6b08fad1a535ba6/rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43", size = 419760, upload-time = "2025-08-07T08:24:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/d3/60/2b2071aee781cb3bd49f94d5d35686990b925e9b9f3e3d149235a6f5d5c1/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432", size = 561201, upload-time = "2025-08-07T08:24:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/98/1f/27b67304272521aaea02be293fecedce13fa351a4e41cdb9290576fc6d81/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b", size = 591021, upload-time = "2025-08-07T08:24:18.999Z" }, - { url = "https://files.pythonhosted.org/packages/db/9b/a2fadf823164dd085b1f894be6443b0762a54a7af6f36e98e8fcda69ee50/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d", size = 556368, upload-time = "2025-08-07T08:24:20.54Z" }, - { url = "https://files.pythonhosted.org/packages/24/f3/6d135d46a129cda2e3e6d4c5e91e2cc26ea0428c6cf152763f3f10b6dd05/rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd", size = 221236, upload-time = "2025-08-07T08:24:22.144Z" }, - { url = "https://files.pythonhosted.org/packages/c5/44/65d7494f5448ecc755b545d78b188440f81da98b50ea0447ab5ebfdf9bd6/rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2", size = 232634, upload-time = "2025-08-07T08:24:23.642Z" }, - { url = "https://files.pythonhosted.org/packages/70/d9/23852410fadab2abb611733933401de42a1964ce6600a3badae35fbd573e/rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac", size = 222783, upload-time = "2025-08-07T08:24:25.098Z" }, - { url = "https://files.pythonhosted.org/packages/15/75/03447917f78512b34463f4ef11066516067099a0c466545655503bed0c77/rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774", size = 359154, upload-time = "2025-08-07T08:24:26.249Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fc/4dac4fa756451f2122ddaf136e2c6aeb758dc6fdbe9ccc4bc95c98451d50/rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b", size = 343909, upload-time = "2025-08-07T08:24:27.405Z" }, - { url = "https://files.pythonhosted.org/packages/7b/81/723c1ed8e6f57ed9d8c0c07578747a2d3d554aaefc1ab89f4e42cfeefa07/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd", size = 379340, upload-time = "2025-08-07T08:24:28.714Z" }, - { url = "https://files.pythonhosted.org/packages/98/16/7e3740413de71818ce1997df82ba5f94bae9fff90c0a578c0e24658e6201/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb", size = 391655, upload-time = "2025-08-07T08:24:30.223Z" }, - { url = "https://files.pythonhosted.org/packages/e0/63/2a9f510e124d80660f60ecce07953f3f2d5f0b96192c1365443859b9c87f/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433", size = 513017, upload-time = "2025-08-07T08:24:31.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/4e/cf6ff311d09776c53ea1b4f2e6700b9d43bb4e99551006817ade4bbd6f78/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615", size = 402058, upload-time = "2025-08-07T08:24:32.613Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/5e36096d474cb10f2a2d68b22af60a3bc4164fd8db15078769a568d9d3ac/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8", size = 383474, upload-time = "2025-08-07T08:24:33.767Z" }, - { url = "https://files.pythonhosted.org/packages/db/a2/3dff02805b06058760b5eaa6d8cb8db3eb3e46c9e452453ad5fc5b5ad9fe/rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858", size = 400067, upload-time = "2025-08-07T08:24:35.021Z" }, - { url = "https://files.pythonhosted.org/packages/67/87/eed7369b0b265518e21ea836456a4ed4a6744c8c12422ce05bce760bb3cf/rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5", size = 412085, upload-time = "2025-08-07T08:24:36.267Z" }, - { url = "https://files.pythonhosted.org/packages/8b/48/f50b2ab2fbb422fbb389fe296e70b7a6b5ea31b263ada5c61377e710a924/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9", size = 555928, upload-time = "2025-08-07T08:24:37.573Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/b18eb51045d06887666c3560cd4bbb6819127b43d758f5adb82b5f56f7d1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79", size = 585527, upload-time = "2025-08-07T08:24:39.391Z" }, - { url = "https://files.pythonhosted.org/packages/be/03/a3dd6470fc76499959b00ae56295b76b4bdf7c6ffc60d62006b1217567e1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", size = 554211, upload-time = "2025-08-07T08:24:40.6Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d1/ee5fd1be395a07423ac4ca0bcc05280bf95db2b155d03adefeb47d5ebf7e/rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", size = 216624, upload-time = "2025-08-07T08:24:42.204Z" }, - { url = "https://files.pythonhosted.org/packages/1c/94/4814c4c858833bf46706f87349c37ca45e154da7dbbec9ff09f1abeb08cc/rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", size = 230007, upload-time = "2025-08-07T08:24:43.329Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a5/8fffe1c7dc7c055aa02df310f9fb71cfc693a4d5ccc5de2d3456ea5fb022/rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb", size = 362595, upload-time = "2025-08-07T08:24:44.478Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c7/4e4253fd2d4bb0edbc0b0b10d9f280612ca4f0f990e3c04c599000fe7d71/rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f", size = 347252, upload-time = "2025-08-07T08:24:45.678Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c8/3d1a954d30f0174dd6baf18b57c215da03cf7846a9d6e0143304e784cddc/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64", size = 384886, upload-time = "2025-08-07T08:24:46.86Z" }, - { url = "https://files.pythonhosted.org/packages/e0/52/3c5835f2df389832b28f9276dd5395b5a965cea34226e7c88c8fbec2093c/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015", size = 399716, upload-time = "2025-08-07T08:24:48.174Z" }, - { url = "https://files.pythonhosted.org/packages/40/73/176e46992461a1749686a2a441e24df51ff86b99c2d34bf39f2a5273b987/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0", size = 517030, upload-time = "2025-08-07T08:24:49.52Z" }, - { url = "https://files.pythonhosted.org/packages/79/2a/7266c75840e8c6e70effeb0d38922a45720904f2cd695e68a0150e5407e2/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89", size = 408448, upload-time = "2025-08-07T08:24:50.727Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5f/a7efc572b8e235093dc6cf39f4dbc8a7f08e65fdbcec7ff4daeb3585eef1/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d", size = 387320, upload-time = "2025-08-07T08:24:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/a2/eb/9ff6bc92efe57cf5a2cb74dee20453ba444b6fdc85275d8c99e0d27239d1/rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51", size = 407414, upload-time = "2025-08-07T08:24:53.664Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bd/3b9b19b00d5c6e1bd0f418c229ab0f8d3b110ddf7ec5d9d689ef783d0268/rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c", size = 420766, upload-time = "2025-08-07T08:24:55.917Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/521a7b1079ce16258c70805166e3ac6ec4ee2139d023fe07954dc9b2d568/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4", size = 562409, upload-time = "2025-08-07T08:24:57.17Z" }, - { url = "https://files.pythonhosted.org/packages/8b/bf/65db5bfb14ccc55e39de8419a659d05a2a9cd232f0a699a516bb0991da7b/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e", size = 590793, upload-time = "2025-08-07T08:24:58.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/b8/82d368b378325191ba7aae8f40f009b78057b598d4394d1f2cdabaf67b3f/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e", size = 558178, upload-time = "2025-08-07T08:24:59.756Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ff/f270bddbfbc3812500f8131b1ebbd97afd014cd554b604a3f73f03133a36/rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6", size = 222355, upload-time = "2025-08-07T08:25:01.027Z" }, - { url = "https://files.pythonhosted.org/packages/bf/20/fdab055b1460c02ed356a0e0b0a78c1dd32dc64e82a544f7b31c9ac643dc/rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a", size = 234007, upload-time = "2025-08-07T08:25:02.268Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a8/694c060005421797a3be4943dab8347c76c2b429a9bef68fb2c87c9e70c7/rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d", size = 223527, upload-time = "2025-08-07T08:25:03.45Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f9/77f4c90f79d2c5ca8ce6ec6a76cb4734ee247de6b3a4f337e289e1f00372/rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828", size = 359469, upload-time = "2025-08-07T08:25:04.648Z" }, - { url = "https://files.pythonhosted.org/packages/c0/22/b97878d2f1284286fef4172069e84b0b42b546ea7d053e5fb7adb9ac6494/rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669", size = 343960, upload-time = "2025-08-07T08:25:05.863Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b0/dfd55b5bb480eda0578ae94ef256d3061d20b19a0f5e18c482f03e65464f/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd", size = 380201, upload-time = "2025-08-07T08:25:07.513Z" }, - { url = "https://files.pythonhosted.org/packages/28/22/e1fa64e50d58ad2b2053077e3ec81a979147c43428de9e6de68ddf6aff4e/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec", size = 392111, upload-time = "2025-08-07T08:25:09.149Z" }, - { url = "https://files.pythonhosted.org/packages/49/f9/43ab7a43e97aedf6cea6af70fdcbe18abbbc41d4ae6cdec1bfc23bbad403/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303", size = 515863, upload-time = "2025-08-07T08:25:10.431Z" }, - { url = "https://files.pythonhosted.org/packages/38/9b/9bd59dcc636cd04d86a2d20ad967770bf348f5eb5922a8f29b547c074243/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b", size = 402398, upload-time = "2025-08-07T08:25:11.819Z" }, - { url = "https://files.pythonhosted.org/packages/71/bf/f099328c6c85667aba6b66fa5c35a8882db06dcd462ea214be72813a0dd2/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410", size = 384665, upload-time = "2025-08-07T08:25:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c5/9c1f03121ece6634818490bd3c8be2c82a70928a19de03467fb25a3ae2a8/rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156", size = 400405, upload-time = "2025-08-07T08:25:14.417Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b8/e25d54af3e63ac94f0c16d8fe143779fe71ff209445a0c00d0f6984b6b2c/rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2", size = 413179, upload-time = "2025-08-07T08:25:15.664Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d1/406b3316433fe49c3021546293a04bc33f1478e3ec7950215a7fce1a1208/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1", size = 556895, upload-time = "2025-08-07T08:25:17.061Z" }, - { url = "https://files.pythonhosted.org/packages/5f/bc/3697c0c21fcb9a54d46ae3b735eb2365eea0c2be076b8f770f98e07998de/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42", size = 585464, upload-time = "2025-08-07T08:25:18.406Z" }, - { url = "https://files.pythonhosted.org/packages/63/09/ee1bb5536f99f42c839b177d552f6114aa3142d82f49cef49261ed28dbe0/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae", size = 555090, upload-time = "2025-08-07T08:25:20.461Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2c/363eada9e89f7059199d3724135a86c47082cbf72790d6ba2f336d146ddb/rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5", size = 218001, upload-time = "2025-08-07T08:25:21.761Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3f/d6c216ed5199c9ef79e2a33955601f454ed1e7420a93b89670133bca5ace/rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", size = 230993, upload-time = "2025-08-07T08:25:23.34Z" }, +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, ] [[package]] name = "ruff" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/97/38/796a101608a90494440856ccfb52b1edae90de0b817e76bfade66b12d320/ruff-0.12.1.tar.gz", hash = "sha256:806bbc17f1104fd57451a98a58df35388ee3ab422e029e8f5cf30aa4af2c138c", size = 4413426, upload-time = "2025-06-26T20:34:14.784Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/bf/3dba52c1d12ab5e78d75bd78ad52fb85a6a1f29cc447c2423037b82bed0d/ruff-0.12.1-py3-none-linux_armv6l.whl", hash = "sha256:6013a46d865111e2edb71ad692fbb8262e6c172587a57c0669332a449384a36b", size = 10305649, upload-time = "2025-06-26T20:33:39.242Z" }, - { url = "https://files.pythonhosted.org/packages/8c/65/dab1ba90269bc8c81ce1d499a6517e28fe6f87b2119ec449257d0983cceb/ruff-0.12.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b3f75a19e03a4b0757d1412edb7f27cffb0c700365e9d6b60bc1b68d35bc89e0", size = 11120201, upload-time = "2025-06-26T20:33:42.207Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3e/2d819ffda01defe857fa2dd4cba4d19109713df4034cc36f06bbf582d62a/ruff-0.12.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a256522893cb7e92bb1e1153283927f842dea2e48619c803243dccc8437b8be", size = 10466769, upload-time = "2025-06-26T20:33:44.102Z" }, - { url = "https://files.pythonhosted.org/packages/63/37/bde4cf84dbd7821c8de56ec4ccc2816bce8125684f7b9e22fe4ad92364de/ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:069052605fe74c765a5b4272eb89880e0ff7a31e6c0dbf8767203c1fbd31c7ff", size = 10660902, upload-time = "2025-06-26T20:33:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/0e/3a/390782a9ed1358c95e78ccc745eed1a9d657a537e5c4c4812fce06c8d1a0/ruff-0.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a684f125a4fec2d5a6501a466be3841113ba6847827be4573fddf8308b83477d", size = 10167002, upload-time = "2025-06-26T20:33:47.81Z" }, - { url = "https://files.pythonhosted.org/packages/6d/05/f2d4c965009634830e97ffe733201ec59e4addc5b1c0efa035645baa9e5f/ruff-0.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdecdef753bf1e95797593007569d8e1697a54fca843d78f6862f7dc279e23bd", size = 11751522, upload-time = "2025-06-26T20:33:49.857Z" }, - { url = "https://files.pythonhosted.org/packages/35/4e/4bfc519b5fcd462233f82fc20ef8b1e5ecce476c283b355af92c0935d5d9/ruff-0.12.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70d52a058c0e7b88b602f575d23596e89bd7d8196437a4148381a3f73fcd5010", size = 12520264, upload-time = "2025-06-26T20:33:52.199Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/7756a6925da236b3a31f234b4167397c3e5f91edb861028a631546bad719/ruff-0.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d0a69d1e8d716dfeab22d8d5e7c786b73f2106429a933cee51d7b09f861d4e", size = 12133882, upload-time = "2025-06-26T20:33:54.231Z" }, - { url = "https://files.pythonhosted.org/packages/dd/00/40da9c66d4a4d51291e619be6757fa65c91b92456ff4f01101593f3a1170/ruff-0.12.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cc32e863adcf9e71690248607ccdf25252eeeab5193768e6873b901fd441fed", size = 11608941, upload-time = "2025-06-26T20:33:56.202Z" }, - { url = "https://files.pythonhosted.org/packages/91/e7/f898391cc026a77fbe68dfea5940f8213622474cb848eb30215538a2dadf/ruff-0.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd49a4619f90d5afc65cf42e07b6ae98bb454fd5029d03b306bd9e2273d44cc", size = 11602887, upload-time = "2025-06-26T20:33:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/f6/02/0891872fc6aab8678084f4cf8826f85c5d2d24aa9114092139a38123f94b/ruff-0.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ed5af6aaaea20710e77698e2055b9ff9b3494891e1b24d26c07055459bb717e9", size = 10521742, upload-time = "2025-06-26T20:34:00.465Z" }, - { url = "https://files.pythonhosted.org/packages/2a/98/d6534322c74a7d47b0f33b036b2498ccac99d8d8c40edadb552c038cecf1/ruff-0.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:801d626de15e6bf988fbe7ce59b303a914ff9c616d5866f8c79eb5012720ae13", size = 10149909, upload-time = "2025-06-26T20:34:02.603Z" }, - { url = "https://files.pythonhosted.org/packages/34/5c/9b7ba8c19a31e2b6bd5e31aa1e65b533208a30512f118805371dbbbdf6a9/ruff-0.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2be9d32a147f98a1972c1e4df9a6956d612ca5f5578536814372113d09a27a6c", size = 11136005, upload-time = "2025-06-26T20:34:04.723Z" }, - { url = "https://files.pythonhosted.org/packages/dc/34/9bbefa4d0ff2c000e4e533f591499f6b834346025e11da97f4ded21cb23e/ruff-0.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:49b7ce354eed2a322fbaea80168c902de9504e6e174fd501e9447cad0232f9e6", size = 11648579, upload-time = "2025-06-26T20:34:06.766Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1c/20cdb593783f8f411839ce749ec9ae9e4298c2b2079b40295c3e6e2089e1/ruff-0.12.1-py3-none-win32.whl", hash = "sha256:d973fa626d4c8267848755bd0414211a456e99e125dcab147f24daa9e991a245", size = 10519495, upload-time = "2025-06-26T20:34:08.718Z" }, - { url = "https://files.pythonhosted.org/packages/cf/56/7158bd8d3cf16394928f47c637d39a7d532268cd45220bdb6cd622985760/ruff-0.12.1-py3-none-win_amd64.whl", hash = "sha256:9e1123b1c033f77bd2590e4c1fe7e8ea72ef990a85d2484351d408224d603013", size = 11547485, upload-time = "2025-06-26T20:34:11.008Z" }, - { url = "https://files.pythonhosted.org/packages/91/d0/6902c0d017259439d6fd2fd9393cea1cfe30169940118b007d5e0ea7e954/ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc", size = 10691209, upload-time = "2025-06-26T20:34:12.928Z" }, +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, ] [[package]] @@ -1168,68 +1198,69 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.36" +version = "2.0.43" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "greenlet", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64')" }, + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/65/9cbc9c4c3287bed2499e05033e207473504dc4df999ce49385fb1f8b058a/sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", size = 9574485, upload-time = "2024-10-15T19:41:44.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/bf/005dc47f0e57556e14512d5542f3f183b94fde46e15ff1588ec58ca89555/SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", size = 2092378, upload-time = "2024-10-16T00:43:55.469Z" }, - { url = "https://files.pythonhosted.org/packages/94/65/f109d5720779a08e6e324ec89a744f5f92c48bd8005edc814bf72fbb24e5/SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", size = 2082778, upload-time = "2024-10-16T00:43:57.304Z" }, - { url = "https://files.pythonhosted.org/packages/60/f6/d9aa8c49c44f9b8c9b9dada1f12fa78df3d4c42aa2de437164b83ee1123c/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53", size = 3232191, upload-time = "2024-10-15T21:31:12.896Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ab/81d4514527c068670cb1d7ab62a81a185df53a7c379bd2a5636e83d09ede/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", size = 3243044, upload-time = "2024-10-15T20:16:28.954Z" }, - { url = "https://files.pythonhosted.org/packages/35/b4/f87c014ecf5167dc669199cafdb20a7358ff4b1d49ce3622cc48571f811c/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", size = 3178511, upload-time = "2024-10-15T21:31:16.792Z" }, - { url = "https://files.pythonhosted.org/packages/ea/09/badfc9293bc3ccba6ede05e5f2b44a760aa47d84da1fc5a326e963e3d4d9/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", size = 3205147, upload-time = "2024-10-15T20:16:32.718Z" }, - { url = "https://files.pythonhosted.org/packages/c8/60/70e681de02a13c4b27979b7b78da3058c49bacc9858c89ba672e030f03f2/SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", size = 2062709, upload-time = "2024-10-15T20:16:29.946Z" }, - { url = "https://files.pythonhosted.org/packages/b7/ed/f6cd9395e41bfe47dd253d74d2dfc3cab34980d4e20c8878cb1117306085/SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", size = 2088433, upload-time = "2024-10-15T20:16:33.501Z" }, - { url = "https://files.pythonhosted.org/packages/78/5c/236398ae3678b3237726819b484f15f5c038a9549da01703a771f05a00d6/SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", size = 2087651, upload-time = "2024-10-16T00:43:59.168Z" }, - { url = "https://files.pythonhosted.org/packages/a8/14/55c47420c0d23fb67a35af8be4719199b81c59f3084c28d131a7767b0b0b/SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", size = 2078132, upload-time = "2024-10-16T00:44:01.279Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/1e843b36abff8c4a7aa2e37f9bea364f90d021754c2de94d792c2d91405b/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", size = 3164559, upload-time = "2024-10-15T21:31:18.961Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c5/07f18a897b997f6d6b234fab2bf31dccf66d5d16a79fe329aefc95cd7461/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", size = 3177897, upload-time = "2024-10-15T20:16:35.048Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cd/e16f3cbefd82b5c40b33732da634ec67a5f33b587744c7ab41699789d492/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", size = 3111289, upload-time = "2024-10-15T21:31:21.11Z" }, - { url = "https://files.pythonhosted.org/packages/15/85/5b8a3b0bc29c9928aa62b5c91fcc8335f57c1de0a6343873b5f372e3672b/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", size = 3139491, upload-time = "2024-10-15T20:16:38.048Z" }, - { url = "https://files.pythonhosted.org/packages/a1/95/81babb6089938680dfe2cd3f88cd3fd39cccd1543b7cb603b21ad881bff1/SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", size = 2060439, upload-time = "2024-10-15T20:16:36.182Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ce/5f7428df55660d6879d0522adc73a3364970b5ef33ec17fa125c5dbcac1d/SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", size = 2084574, upload-time = "2024-10-15T20:16:38.686Z" }, - { url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787, upload-time = "2024-10-15T20:04:30.265Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/20c78f1081446095450bdc6ee6cc10045fce67a8e003a5876b6eaafc5cc4/sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", size = 2134891, upload-time = "2025-08-11T15:51:13.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/0a/3d89034ae62b200b4396f0f95319f7d86e9945ee64d2343dcad857150fa2/sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", size = 2123061, upload-time = "2025-08-11T15:51:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/cb/10/2711f7ff1805919221ad5bee205971254845c069ee2e7036847103ca1e4c/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", size = 3320384, upload-time = "2025-08-11T15:52:35.088Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0e/3d155e264d2ed2778484006ef04647bc63f55b3e2d12e6a4f787747b5900/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", size = 3329648, upload-time = "2025-08-11T15:56:34.153Z" }, + { url = "https://files.pythonhosted.org/packages/5b/81/635100fb19725c931622c673900da5efb1595c96ff5b441e07e3dd61f2be/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", size = 3258030, upload-time = "2025-08-11T15:52:36.933Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/a99302716d62b4965fded12520c1cbb189f99b17a6d8cf77611d21442e47/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", size = 3294469, upload-time = "2025-08-11T15:56:35.553Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a2/3a11b06715149bf3310b55a98b5c1e84a42cfb949a7b800bc75cb4e33abc/sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", size = 2098906, upload-time = "2025-08-11T15:55:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/bc/09/405c915a974814b90aa591280623adc6ad6b322f61fd5cff80aeaef216c9/sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", size = 2126260, upload-time = "2025-08-11T15:55:02.965Z" }, + { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, ] [[package]] name = "sqlmodel" -version = "0.0.22" +version = "0.0.24" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/39/8641040ab0d5e1d8a1c2325ae89a01ae659fc96c61a43d158fb71c9a0bf0/sqlmodel-0.0.22.tar.gz", hash = "sha256:7d37c882a30c43464d143e35e9ecaf945d88035e20117bf5ec2834a23cbe505e", size = 116392, upload-time = "2024-08-31T09:43:24.088Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload-time = "2025-03-07T05:43:32.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/b1/3af5104b716c420e40a6ea1b09886cae3a1b9f4538343875f637755cae5b/sqlmodel-0.0.22-py3-none-any.whl", hash = "sha256:a1ed13e28a1f4057cbf4ff6cdb4fc09e85702621d3259ba17b3c230bfb2f941b", size = 28276, upload-time = "2024-08-31T09:43:22.358Z" }, + { url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload-time = "2025-03-07T05:43:30.37Z" }, ] [[package]] name = "sse-starlette" -version = "2.3.6" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284, upload-time = "2025-05-30T13:34:12.914Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload-time = "2025-05-30T13:34:11.703Z" }, + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, ] [[package]] name = "starlette" -version = "0.46.2" +version = "0.47.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" }, ] [[package]] @@ -1259,24 +1290,24 @@ dev = [ [package.metadata] requires-dist = [ - { name = "dask", specifier = "==2025.4.1" }, - { name = "fastapi", specifier = "==0.116.1" }, - { name = "fastmcp", specifier = "==2.11.3" }, - { name = "mcp", extras = ["cli"], specifier = "==1.12.4" }, - { name = "oauthlib", specifier = "==3.2.2" }, - { name = "python-dotenv", specifier = ">=1.1.0" }, - { name = "pyyaml", specifier = "==6.0.2" }, + { name = "dask", specifier = "~=2025.4" }, + { name = "fastapi", specifier = "~=0.116.1" }, + { name = "fastmcp", specifier = "~=2.11" }, + { name = "mcp", extras = ["cli"], specifier = "~=1.12" }, + { name = "oauthlib", specifier = "~=3.2" }, + { name = "python-dotenv", specifier = "~=1.1" }, + { name = "pyyaml", specifier = "~=6.0" }, { name = "requests" }, - { name = "sqlalchemy", specifier = "==2.0.36" }, - { name = "sqlmodel", specifier = "==0.0.22" }, + { name = "sqlalchemy", specifier = "~=2.0" }, + { name = "sqlmodel", specifier = "~=0.0.22" }, { name = "sysdig-sdk-python", git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=852ee2ccad12a8b445dd4732e7f3bd44d78a37f7" }, ] [package.metadata.requires-dev] dev = [ - { name = "pytest", specifier = "==8.4.1" }, - { name = "pytest-cov", specifier = "==6.2.1" }, - { name = "ruff", specifier = "==0.12.1" }, + { name = "pytest", specifier = "~=8.4" }, + { name = "pytest-cov", specifier = "~=6.2" }, + { name = "ruff", specifier = "~=0.12.1" }, ] [[package]] @@ -1301,7 +1332,7 @@ wheels = [ [[package]] name = "typer" -version = "0.16.0" +version = "0.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1309,18 +1340,18 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/78/d90f616bf5f88f8710ad067c1f8705bf7618059836ca084e5bb2a0855d75/typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614", size = 102836, upload-time = "2025-08-18T19:18:22.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, + { url = "https://files.pythonhosted.org/packages/2d/76/06dbe78f39b2203d2a47d5facc5df5102d0561e2807396471b5f7c5a30a1/typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9", size = 46397, upload-time = "2025-08-18T19:18:21.663Z" }, ] [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] From e0ce77c572249352f0f980792b7cf2132054165c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Thu, 28 Aug 2025 11:50:44 +0200 Subject: [PATCH 11/11] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b053c8c..5d4bcd0 100644 --- a/README.md +++ b/README.md @@ -350,3 +350,5 @@ For the Claude Desktop app, you can manually configure the MCP server by editing 3. Have fun ![goose_results](./docs/assets/goose_results.png) + +