From 33a0e17627ac6c37b89a4c600e223c18d9e7f94d Mon Sep 17 00:00:00 2001 From: S3B4SZ17 Date: Thu, 14 Aug 2025 12:02:05 -0600 Subject: [PATCH 1/2] feat: Updating middleware for authorization of tools and better management of context data Signed-off-by: S3B4SZ17 --- app_config.yaml | 10 +- charts/sysdig-mcp/values.yaml | 15 +- main.py | 2 +- pyproject.toml | 8 +- tests/events_feed_test.py | 6 +- tools/events_feed/tool.py | 70 ++- tools/inventory/tool.py | 14 +- tools/sysdig_sage/tool.py | 29 +- tools/vulnerability_management/tool.py | 35 +- utils/mcp_server.py | 135 +++-- utils/middleware/auth.py | 191 ++++-- utils/query_helpers.py | 8 +- utils/sysdig/api.py | 4 +- utils/sysdig/helpers.py | 13 + ...old_sysdig_api.py => legacy_sysdig_api.py} | 15 +- uv.lock | 554 ++++++++++++++++-- 16 files changed, 869 insertions(+), 240 deletions(-) create mode 100644 utils/sysdig/helpers.py rename utils/sysdig/{old_sysdig_api.py => legacy_sysdig_api.py} (85%) diff --git a/app_config.yaml b/app_config.yaml index 3c04fab..d047b53 100644 --- a/app_config.yaml +++ b/app_config.yaml @@ -10,11 +10,9 @@ sysdig: mcp: transport: stdio - host: "localhost" - port: 8080 allowed_tools: - - "events-feed" - - "sysdig-cli-scanner" # Only available in stdio local transport mode - - "vulnerability-management" + - "threat-detection" + - "cli-scanner" # You need the sysdig-cli-scanner binary installed in your server to use this tool + - "vulnerability" - "inventory" - - "sysdig-sage" + - "sage" diff --git a/charts/sysdig-mcp/values.yaml b/charts/sysdig-mcp/values.yaml index e84f93a..d165830 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.1.5" imagePullSecrets: [] nameOverride: "" @@ -121,14 +121,13 @@ configMap: sysdig: host: "https://us2.app.sysdig.com" + # public_api_url: "https://custom.api.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" + - "threat-detection" + - "cli-scanner" # You need the sysdig-cli-scanner binary installed in your server to use this tool + - "vulnerability" + - "inventory" + - "sage" diff --git a/main.py b/main.py index 1abc108..6b95912 100644 --- a/main.py +++ b/main.py @@ -41,7 +41,7 @@ def main(): # Choose transport: "stdio" or "sse" (HTTP/SSE) handle_signals() transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() - log.info(""" + print(""" ▄▖ ▌▘ ▖ ▖▄▖▄▖ ▄▖ ▚ ▌▌▛▘▛▌▌▛▌ ▛▖▞▌▌ ▙▌ ▚ █▌▛▘▌▌█▌▛▘ ▄▌▙▌▄▌▙▌▌▙▌ ▌▝ ▌▙▖▌ ▄▌▙▖▌ ▚▘▙▖▌ diff --git a/pyproject.toml b/pyproject.toml index 0f31efc..6f0f9b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,20 @@ [project] name = "sysdig-mcp-server" -version = "0.1.4" +version = "0.1.5" description = "Sysdig MCP Server" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "mcp[cli]==1.9.4", + "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@15d2c6bd92d9cb26b8ca19c6eef3be991b8e43d2", "dask==2025.4.1", "oauthlib==3.2.2", "fastapi==0.115.12", - "fastmcp==2.5.1", + "fastmcp==2.11.3", "requests", ] diff --git a/tests/events_feed_test.py b/tests/events_feed_test.py index 0ce2a1a..e8df5fd 100644 --- a/tests/events_feed_test.py +++ b/tests/events_feed_test.py @@ -7,6 +7,8 @@ from .conftest import util_load_json from unittest.mock import MagicMock, AsyncMock 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__) @@ -25,13 +27,14 @@ def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds """ # Override the environment variable for MCP transport os.environ["MCP_TRANSPORT"] = "stdio" + ctx = Context(FastMCP()) # Mocking FastMCP context # 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() # 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 @@ -39,3 +42,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 43a6e1d..b3c3a26 100644 --- a/tools/events_feed/tool.py +++ b/tools/events_feed/tool.py @@ -8,15 +8,15 @@ 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 fastmcp.server.context import Context from starlette.requests import Request from sysdig_client.api import SecureEventsApi -from utils.sysdig.old_sysdig_api import OldSysdigApi +from utils.sysdig.legacy_sysdig_api import LegacySysdigApi from fastmcp.server.dependencies import get_http_request from utils.query_helpers import create_standard_response from utils.sysdig.client_config import get_configuration @@ -36,41 +36,40 @@ class EventsFeedTools: This class provides methods to retrieve event information and list runtime events. """ - def init_client(self, old_api: bool = False) -> SecureEventsApi | OldSysdigApi: + def init_client(self, transport: str, old_api: bool = False) -> SecureEventsApi | LegacySysdigApi: """ 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. + old_api (bool): If True, initializes the LegacySysdigApi client instead of SecureEventsApi. Returns: - SecureEventsApi | OldSysdigApi: An instance of the SecureEventsApi or OldSysdigApi client. + SecureEventsApi | LegacySysdigApi: An instance of the SecureEventsApi or LegacySysdigApi client. """ secure_events_api: SecureEventsApi = None - old_sysdig_api: OldSysdigApi = None - transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() + legacy_sysdig_api: LegacySysdigApi = None 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.") request: Request = get_http_request() secure_events_api = request.state.api_instances["secure_events"] - old_sysdig_api = request.state.api_instances["old_sysdig_api"] + legacy_sysdig_api = request.state.api_instances["legacy_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.") - cfg = get_configuration() - api_client = initialize_api_client(cfg) - secure_events_api = SecureEventsApi(api_client) + log.debug("Trying to init the Sysdig API client from environment variables.") # 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: + if old_api: + old_cfg = get_configuration(old_api=True) + legacy_sysdig_api = initialize_api_client(old_cfg) + legacy_sysdig_api = LegacySysdigApi(legacy_sysdig_api) + else: + cfg = get_configuration() + api_client = initialize_api_client(cfg) + secure_events_api = SecureEventsApi(api_client) + + return legacy_sysdig_api if old_api else secure_events_api + + def tool_get_event_info(self, ctx: Context, event_id: str) -> dict: """ Retrieves detailed information for a specific security event. @@ -81,7 +80,7 @@ def tool_get_event_info(self, event_id: str) -> dict: Event: The Event object containing detailed information about the specified event. """ # Init of the sysdig client - secure_events_api = self.init_client() + secure_events_api = self.init_client(ctx.get_state("transport_method")) try: # Get the HTTP request start_time = time.time() @@ -99,6 +98,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, @@ -148,7 +148,7 @@ 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() + secure_events_api = self.init_client(ctx.get_state("transport_method")) start_time = time.time() # Compute time window now_ns = time.time_ns() @@ -180,7 +180,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. @@ -194,7 +194,7 @@ def tool_get_event_process_tree(self, event_id: str) -> dict: try: start_time = time.time() # Get process tree branches - old_api_client = self.init_client(old_api=True) + old_api_client = self.init_client(transport=ctx.get_state("transport_method"), old_api=True) branches = old_api_client.request_process_tree_branches(event_id) # Get process tree tree = old_api_client.request_process_tree_trees(event_id) @@ -205,16 +205,14 @@ def tool_get_event_process_tree(self, event_id: str) -> dict: 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.get("results", []), + "tree": tree.get("results", []), + "metadata": { + "execution_time_ms": execution_time, + "timestamp": datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z"), }, - ) + } return response except ToolError as e: diff --git a/tools/inventory/tool.py b/tools/inventory/tool.py index c057150..3dac3aa 100644 --- a/tools/inventory/tool.py +++ b/tools/inventory/tool.py @@ -10,7 +10,7 @@ from fastmcp.server.dependencies import get_http_request from fastmcp.exceptions import ToolError from starlette.requests import Request -from sysdig_client import ApiException +from fastmcp.server.context import Context from sysdig_client.api import InventoryApi from utils.sysdig.client_config import get_configuration from utils.app_config import get_app_config @@ -31,7 +31,7 @@ class InventoryTools: This class provides methods to list resources and retrieve a single resource by its hash. """ - def init_client(self) -> InventoryApi: + def init_client(self, transport: str) -> 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 @@ -48,7 +48,7 @@ def init_client(self) -> InventoryApi: 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.") + log.debug("Trying to init the Sysdig API client from environment variables.") cfg = get_configuration() api_client = initialize_api_client(cfg) inventory_api = InventoryApi(api_client) @@ -56,6 +56,7 @@ def init_client(self) -> InventoryApi: def tool_list_resources( self, + ctx: Context, filter_exp: Annotated[ str, Field( @@ -64,7 +65,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 @@ -166,7 +167,7 @@ def tool_list_resources( Or a dict containing an error message if the call fails. """ try: - inventory_api = self.init_client() + inventory_api = self.init_client(ctx.get_state("transport_method")) start_time = time.time() api_response = inventory_api.get_resources_without_preload_content( @@ -184,6 +185,7 @@ 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: """ @@ -196,7 +198,7 @@ def tool_get_resource( dict: A dictionary containing the details of the requested inventory resource. """ try: - inventory_api = self.init_client() + inventory_api = self.init_client(ctx.get_state("transport_method")) 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 186d8f0..f00dc0c 100644 --- a/tools/sysdig_sage/tool.py +++ b/tools/sysdig_sage/tool.py @@ -7,9 +7,9 @@ import logging import os import time -from typing import Any, Dict from fastmcp.exceptions import ToolError -from utils.sysdig.old_sysdig_api import OldSysdigApi +from fastmcp.server.context import Context +from utils.sysdig.legacy_sysdig_api import LegacySysdigApi from starlette.requests import Request from fastmcp.server.dependencies import get_http_request from utils.sysdig.client_config import get_configuration @@ -30,30 +30,29 @@ class SageTools: language questions and execute them against the Sysdig API. """ - def init_client(self) -> OldSysdigApi: + def init_client(self, transport: str) -> LegacySysdigApi: """ - Initializes the OldSysdigApi client from the request state. + Initializes the LegacySysdigApi 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. + LegacySysdigApi: An instance of the LegacySysdigApi client. """ - old_sysdig_api: OldSysdigApi = None - transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() + legacy_sysdig_api: LegacySysdigApi = None 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.") request: Request = get_http_request() - old_sysdig_api = request.state.api_instances["old_sysdig_api"] + legacy_sysdig_api = request.state.api_instances["legacy_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.") + log.debug("Trying to init 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 + legacy_sysdig_api = LegacySysdigApi(api_client) + return legacy_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. @@ -75,8 +74,8 @@ 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) + legacy_sysdig_api = self.init_client(ctx.get_state("transport_method")) + sysql_response = await legacy_sysdig_api.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: @@ -90,7 +89,7 @@ async def tool_sage_to_sysql(self, question: str) -> dict: # 2) Execute generated SysQL query try: log.debug(f"Executing SysQL query: {syslq_query}") - results = old_sysdig_api.execute_sysql_query(syslq_query) + results = legacy_sysdig_api.execute_sysql_query(syslq_query) execution_time = (time.time() - start_time) * 1000 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 c9f93dd..4d30fec 100644 --- a/tools/vulnerability_management/tool.py +++ b/tools/vulnerability_management/tool.py @@ -11,6 +11,7 @@ from sysdig_client.models.get_policy_response import GetPolicyResponse from fastmcp.prompts.prompt import PromptMessage, TextContent from fastmcp.exceptions import ToolError +from fastmcp.server.context import Context from starlette.requests import Request from sysdig_client.api import VulnerabilityManagementApi from fastmcp.server.dependencies import get_http_request @@ -33,7 +34,7 @@ class VulnerabilityManagementTools: and vulnerability policies. """ - def init_client(self) -> VulnerabilityManagementApi: + def init_client(self, transport: str) -> 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 @@ -42,7 +43,6 @@ 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() 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.") @@ -50,7 +50,7 @@ def init_client(self) -> VulnerabilityManagementApi: 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.") + log.debug("Trying to init the Sysdig API client from environment variables.") cfg = get_configuration() api_client = initialize_api_client(cfg) vulnerability_management_api = VulnerabilityManagementApi(api_client) @@ -58,6 +58,7 @@ def init_client(self) -> VulnerabilityManagementApi: 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], @@ -145,7 +146,7 @@ def tool_list_runtime_vulnerabilities( - execution_time_ms (float): Execution duration in milliseconds. """ try: - vulnerability_api = self.init_client() + vulnerability_api = self.init_client(ctx.get_state("transport_method")) # Record start time for execution duration start_time = time.time() api_response = vulnerability_api.scanner_api_service_list_runtime_results_without_preload_content( @@ -166,6 +167,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, @@ -186,7 +188,7 @@ def tool_list_accepted_risks( dict: The API response as a dictionary, or an error dict on failure. """ try: - vulnerability_api = self.init_client() + vulnerability_api = self.init_client(ctx.get_state("transport_method")) 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 @@ -204,7 +206,7 @@ def tool_list_accepted_risks( 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. @@ -215,7 +217,7 @@ def tool_get_accepted_risk(self, accepted_risk_id: str) -> dict: dict: The accepted risk details as a dictionary, or an error dict on failure. """ try: - vulnerability_api = self.init_client() + vulnerability_api = self.init_client(ctx.get_state("transport_method")) 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: @@ -224,6 +226,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( @@ -268,7 +271,7 @@ 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() + vulnerability_api = self.init_client(ctx.get_state("transport_method")) start_time = time.time() api_response = vulnerability_api.scanner_api_service_list_registry_results_without_preload_content( filter=filter, limit=limit, cursor=cursor @@ -286,7 +289,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. @@ -299,7 +302,7 @@ def tool_get_vulnerability_policy( dict: An error dict on failure. """ try: - vulnerability_api = self.init_client() + vulnerability_api = self.init_client(ctx.get_state("transport_method")) 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: @@ -308,6 +311,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, @@ -327,7 +331,7 @@ def tool_list_vulnerability_policies( """ start_time = time.time() try: - vulnerability_api = self.init_client() + vulnerability_api = self.init_client(ctx.get_state("transport_method")) api_response = vulnerability_api.secure_vulnerability_v1_policies_get_without_preload_content( cursor=cursor, limit=limit, name=name, stages=stages ) @@ -347,6 +351,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], @@ -395,7 +400,7 @@ def tool_list_pipeline_scan_results( """ start_time = time.time() try: - vulnerability_api = self.init_client() + vulnerability_api = self.init_client(ctx.get_state("transport_method")) api_response = vulnerability_api.secure_vulnerability_v1_pipeline_results_get_without_preload_content( cursor=cursor, filter=filter, limit=limit ) @@ -411,7 +416,7 @@ def tool_list_pipeline_scan_results( 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: + def tool_get_scan_result(self, ctx: Context, scan_id: str) -> dict: """ Retrieve the result of a specific scan. @@ -422,14 +427,14 @@ def tool_get_scan_result(self, scan_id: str) -> dict: dict: ScanResultResponse as dict, or {"error": ...}. """ try: - vulnerability_api = self.init_client() + vulnerability_api = self.init_client(ctx.get_state("transport_method")) 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}") raise e - def explore_vulnerabilities_prompt(self, filters: str) -> PromptMessage: + def explore_vulnerabilities_prompt(self, ctx: Context, filters: str) -> PromptMessage: """ Generates a prompt message for exploring vulnerabilities based on provided filters. diff --git a/utils/mcp_server.py b/utils/mcp_server.py index de0ee84..e395338 100644 --- a/utils/mcp_server.py +++ b/utils/mcp_server.py @@ -13,9 +13,10 @@ from typing_extensions import Literal from fastapi import FastAPI from fastmcp import FastMCP +from fastmcp.prompts import Prompt +from fastmcp.settings import Settings from fastmcp.resources import HttpResource, TextResource -from utils.middleware.auth import CustomAuthMiddleware -from starlette.middleware import Middleware +from utils.middleware.auth import CustomMiddleware from tools.events_feed.tool import EventsFeedTools from tools.inventory.tool import InventoryTools from tools.vulnerability_management.tool import VulnerabilityManagementTools @@ -37,7 +38,7 @@ _mcp_instance: Optional[FastMCP] = None -middlewares = [Middleware(CustomAuthMiddleware)] +middlewares = [CustomMiddleware()] MCP_MOUNT_PATH = "/sysdig-mcp-server" @@ -52,9 +53,8 @@ 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()], + include_tags=["sysdig_secure"], + middleware=middlewares, ) @@ -97,10 +97,11 @@ def run_http(): add_tools(mcp=mcp, allowed_tools=app_config["mcp"]["allowed_tools"], transport_type=app_config["mcp"]["transport"]) # Add resources to the MCP server add_resources(mcp) + settings = Settings() # 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 + mcp_app = mcp.http_app(transport=transport) + suffix_path = settings.streamable_http_path if transport == "streamable-http" else settings.sse_path app = FastAPI(lifespan=mcp_app.lifespan) app.mount(MCP_MOUNT_PATH, mcp_app) @@ -150,36 +151,41 @@ def add_tools(mcp: FastMCP, allowed_tools: list, transport_type: Literal["stdio" transport_type (Literal["stdio", "streamable-http"]): The transport type for the MCP server. """ - if "events-feed" in allowed_tools: + if "threat-detection" 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, + mcp.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"], ) - mcp.add_tool( - events_feed_tools.tool_list_runtime_events, + mcp.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"], ) 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"}, + 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"], + ) ) - mcp.add_tool( - events_feed_tools.tool_get_event_process_tree, + mcp.tool( + name_or_fn=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. - """ + """ ), + tags=["threat-detection", "sysdig_secure"], ) # Register the Sysdig Inventory tools @@ -187,83 +193,96 @@ def add_tools(mcp: FastMCP, allowed_tools: list, transport_type: Literal["stdio" # Register the Sysdig Inventory tools log.info("Adding Sysdig Inventory Tools...") inventory_tools = InventoryTools() - mcp.add_tool( - inventory_tools.tool_list_resources, + mcp.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.' + List inventory resources based on Sysdig Filter Query Language expression with optional pagination. """ ), + tags=["inventory", "sysdig_secure"], ) - mcp.add_tool( - inventory_tools.tool_get_resource, + mcp.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"], ) - if "vulnerability-management" in allowed_tools: + if "vulnerability" 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, + mcp.tool( + name_or_fn=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). - """ + (Supports pagination using cursor). + """ ), + tags=["vulnerability", "sysdig_secure"], ) - mcp.add_tool( - vulnerability_tools.tool_list_accepted_risks, + mcp.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"], ) - mcp.add_tool( - vulnerability_tools.tool_get_accepted_risk, + mcp.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"], ) - mcp.add_tool( - vulnerability_tools.tool_list_registry_scan_results, + mcp.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"], ) - mcp.add_tool( - vulnerability_tools.tool_get_vulnerability_policy, + mcp.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"], ) - mcp.add_tool( - vulnerability_tools.tool_list_vulnerability_policies, + + mcp.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"], ) - mcp.add_tool( - vulnerability_tools.tool_list_pipeline_scan_results, + mcp.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"], ) - mcp.add_tool( - vulnerability_tools.tool_get_scan_result, + mcp.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"], ) mcp.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"], + ) ) - if "sysdig-sage" in allowed_tools: + if "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, + mcp.tool( + name_or_fn=sysdig_sage_tools.tool_sage_to_sysql, name="sysdig_sysql_sage_query", description=( """ @@ -271,14 +290,15 @@ def add_tools(mcp: FastMCP, allowed_tools: list, transport_type: Literal["stdio" execute it against the Sysdig API, and return the results. """ ), + tags=["sage", "sysdig_secure"], ) - if "sysdig-cli-scanner" in allowed_tools: + if "cli-scanner" in allowed_tools: # Register the tools for STDIO transport cli_scanner_tool = CLIScannerTool() log.info("Adding Sysdig CLI Scanner Tool...") - mcp.add_tool( - cli_scanner_tool.run_sysdig_cli_scanner, + mcp.tool( + name_or_fn=cli_scanner_tool.run_sysdig_cli_scanner, name="run_sysdig_cli_scanner", description=( """ @@ -286,6 +306,7 @@ def add_tools(mcp: FastMCP, allowed_tools: list, transport_type: Literal["stdio" and posture and misconfigurations. """ ), + tags=["cli-scanner", "sysdig_secure"], ) @@ -300,7 +321,7 @@ def add_resources(mcp: FastMCP) -> None: description="Sysdig Secure Vulnerability Management documentation.", uri="resource://sysdig-secure-vulnerability-management", url="https://docs.sysdig.com/en/sysdig-secure/vulnerability-management/", - tags=["documentation"], + tags=["documentation", "sysdig_secure"], ) filter_query_language = TextResource( name="Sysdig Filter Query Language", @@ -325,13 +346,13 @@ def add_resources(mcp: FastMCP) -> None: 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"], + tags=["query-language", "documentation", "sysdig_secure"], ) mcp.add_resource(vm_docs) mcp.add_resource(filter_query_language) diff --git a/utils/middleware/auth.py b/utils/middleware/auth.py index 67ae8b6..562f825 100644 --- a/utils/middleware/auth.py +++ b/utils/middleware/auth.py @@ -1,16 +1,18 @@ """ -Token-based authentication middleware +Custom middleware for authorization and access control """ import json import logging import os -from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request -from starlette.responses import Response +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.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.sysdig.legacy_sysdig_api import LegacySysdigApi from utils.app_config import get_app_config # Set up logging @@ -21,48 +23,155 @@ app_config = get_app_config() -class CustomAuthMiddleware(BaseHTTPMiddleware): +async def _get_permissions(context: MiddlewareContext) -> None: """ - Custom middleware for handling token-based authentication in the MCP server and initializing Sysdig API clients. + Get the permissions for the current user and set them in the context. + Args: + context (MiddlewareContext): The middleware context. """ + try: + legacy_sysdig_api = _init_legacy_sysdig_api(context) + permissions = legacy_sysdig_api.get_me_permissions() + context.fastmcp_context.set_state("permissions", permissions.json().get("permissions", [])) + except Exception as e: + log.error(f"Error fetching permissions: {e}") + raise - async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + +def _init_legacy_sysdig_api(context: MiddlewareContext) -> LegacySysdigApi: + """ + Initialize the legacy Sysdig API client either from the HTTP request state or from environment variables. + Args: + context (MiddlewareContext): The middleware context. + + Returns: + LegacySysdigApi: The initialized legacy Sysdig API client. + """ + try: + legacy_sysdig_api: LegacySysdigApi = None + transport = context.fastmcp_context.get_state("transport_method") + 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.") + request: Request = get_http_request() + legacy_sysdig_api = request.state.api_instances["legacy_sysdig_api"] + else: + # If running in STDIO mode, we need to initialize the API client from environment variables + log.debug("Trying to init the Sysdig API client from environment variables.") + cfg = get_configuration(old_api=True) + api_client = initialize_api_client(cfg) + legacy_sysdig_api = LegacySysdigApi(api_client) + return legacy_sysdig_api + except Exception as e: + log.error(f"Error initializing legacy Sysdig API: {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_id (str): The ID of 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 + await _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_instance() -> None: + """ + Save the API client instance to the request state. + + Raises: + Exception: If the Authorization header is missing or invalid. + """ + request = get_http_request() + auth_header = request.headers.get("Authorization") + if not auth_header or not auth_header.startswith("Bearer "): + raise Exception("Missing or invalid Authorization header") + # set header to be used by the API client + + # Extract releavant information from the request headers + token = auth_header.removeprefix("Bearer ").strip() + base_url = request.headers.get("X-Sysdig-Host", app_config["sysdig"]["host"]) or str(request.base_url) + log.info(f"Using Sysdig API base URL: {base_url}") + + # Initialize the API client with the token and base URL + # TODO: Implement a more elegant solution for API client initialization, we will end up having multiple API instances + cfg = get_configuration(token, base_url) + legacy_cfg = get_configuration(token, base_url, 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) + _legacy_sysdig_api = LegacySysdigApi(legacy_sysdig_api) + api_instances["legacy_sysdig_api"] = _legacy_sysdig_api + # Having access to the Sysdig API instances per request to be used by the MCP tools + request.state.api_instances = api_instances + + +class CustomMiddleware(Middleware): + """ + Custom middleware for filtering tool listings and performing authentication. + """ + + # 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: """ - 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. + Handle incoming messages and initialize the Sysdig API client if needed. Returns: - Response: The response from the next middleware or endpoint, or an error response if authentication fails. + CallNext: The next middleware or route handler to call. + Raises: + Exception: If a problem occurs while initializing the API clients. """ + # FIXME: Currently not able to get the notifications/initialized that should be the one initializing the API instances + # for the whole session + allowed_notifications = ["notifications/initialized", "tools/list", "tools/call"] + # Save transport method in context + if not context.fastmcp_context.get_state("transport_method"): + transport_method = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() + context.fastmcp_context.set_state("transport_method", transport_method) + try: + if ( + context.fastmcp_context.get_state("transport_method") in ["streamable-http", "sse"] + and context.method in allowed_notifications + ): + await _save_api_instance() - 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", 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}") - - # 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 + return await call_next(context) + except Exception as error: + log.error(f"Error in {context.method}: {type(error).__name__}: {error}") + raise Exception(f"Error in {context.method}: {type(error).__name__}: {error}") - try: - response = await call_next(request) - return response - except Exception as e: - return Response(f"Internal server error: {str(e)}", status_code=500) + 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) + + filtered_tools = [tool for tool in result if await allowed_tool(context, tool)] + + if not filtered_tools: + log.warning(f"No allowed tools found for session: {context.fastmcp_context.session_id}") + raise Exception("No allowed tools found for the user.") + # Return modified list + return filtered_tools diff --git a/utils/query_helpers.py b/utils/query_helpers.py index 1ab58d0..409cbb6 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 @@ -32,6 +32,10 @@ def create_standard_response(results: RESTResponseType, execution_time_ms: str, return { "results": response, - "metadata": {"execution_time_ms": execution_time_ms, "timestamp": datetime.utcnow().isoformat() + "Z", **metadata_kwargs}, + "metadata": { + "execution_time_ms": execution_time_ms, + "timestamp": datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z"), + **metadata_kwargs, + }, "status_code": results.status, } diff --git a/utils/sysdig/api.py b/utils/sysdig/api.py index 299796c..90d063c 100644 --- a/utils/sysdig/api.py +++ b/utils/sysdig/api.py @@ -3,7 +3,7 @@ """ from sysdig_client import ApiClient, SecureEventsApi, VulnerabilityManagementApi, InventoryApi -from utils.sysdig.old_sysdig_api import OldSysdigApi +from utils.sysdig.legacy_sysdig_api import LegacySysdigApi from sysdig_client.configuration import Configuration @@ -47,5 +47,5 @@ def get_sysdig_api_instances(api_client: ApiClient) -> dict: "secure_events": SecureEventsApi(api_client), "vulnerability_management": VulnerabilityManagementApi(api_client), "inventory": InventoryApi(api_client), - "old_sysdig_api": OldSysdigApi(api_client), + "legacy_sysdig_api": LegacySysdigApi(api_client), } diff --git a/utils/sysdig/helpers.py b/utils/sysdig/helpers.py new file mode 100644 index 0000000..1339d0a --- /dev/null +++ b/utils/sysdig/helpers.py @@ -0,0 +1,13 @@ +""" +Helper functions for working with Sysdig API clients. +""" + +# Tool permissions by tag + +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"], +} diff --git a/utils/sysdig/old_sysdig_api.py b/utils/sysdig/legacy_sysdig_api.py similarity index 85% rename from utils/sysdig/old_sysdig_api.py rename to utils/sysdig/legacy_sysdig_api.py index eedf21c..1c48348 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,7 +8,7 @@ from sysdig_client import ApiClient -class OldSysdigApi: +class LegacySysdigApi: """ Wrapper for Old non public Sysdig API. """ @@ -75,3 +75,14 @@ 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 diff --git a/uv.lock b/uv.lock index 731cd15..3c8e791 100644 --- a/uv.lock +++ b/uv.lock @@ -25,6 +25,27 @@ 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 = "authlib" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" }, +] + [[package]] name = "certifi" version = "2025.6.15" @@ -34,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" @@ -141,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.22.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser", marker = "python_full_version < '4'" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/d5/24c6c894f3833bc93d4944c2064309dfd633c0becf93e16fc79d76edd388/cyclopts-3.22.5.tar.gz", hash = "sha256:fa2450b9840abc41c6aa37af5eaeafc7a1264e08054e3a2fe39d49aa154f592a", size = 74890, upload-time = "2025-07-31T18:18:37.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/e5/a7b6db64f08cfe065e531ec6b508fa7dac704fab70d05adb5bc0c2c1d1b6/cyclopts-3.22.5-py3-none-any.whl", hash = "sha256:92efb4a094d9812718d7efe0bffa319a19cb661f230dbf24406c18cd8809fb82", size = 84994, upload-time = "2025-07-31T18:18:35.939Z" }, +] + [[package]] name = "dask" version = "2025.4.1" @@ -159,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" @@ -187,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]] @@ -310,6 +457,72 @@ 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.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, +] + +[[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" +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 = "lazy-object-proxy" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/1f56571ed82fb324f293661690635cf42c41deb8a70a6c9e6edc3e9bb3c8/lazy_object_proxy-1.11.0.tar.gz", hash = "sha256:18874411864c9fbbbaa47f9fc1dd7aea754c86cfde21278ef427639d1dd78e9c", size = 44736, upload-time = "2025-04-16T16:53:48.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/24/dae4759469e9cd318fef145f7cfac7318261b47b23a4701aa477b0c3b42c/lazy_object_proxy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a9f39098e93a63618a79eef2889ae3cf0605f676cd4797fdfd49fcd7ddc318b", size = 28142, upload-time = "2025-04-16T16:53:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/de/0c/645a881f5f27952a02f24584d96f9f326748be06ded2cee25f8f8d1cd196/lazy_object_proxy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee13f67f4fcd044ef27bfccb1c93d39c100046fec1fad6e9a1fcdfd17492aeb3", size = 28380, upload-time = "2025-04-16T16:53:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0f/6e004f928f7ff5abae2b8e1f68835a3870252f886e006267702e1efc5c7b/lazy_object_proxy-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4c84eafd8dd15ea16f7d580758bc5c2ce1f752faec877bb2b1f9f827c329cd", size = 28149, upload-time = "2025-04-16T16:53:40.135Z" }, + { url = "https://files.pythonhosted.org/packages/63/cb/b8363110e32cc1fd82dc91296315f775d37a39df1c1cfa976ec1803dac89/lazy_object_proxy-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d2503427bda552d3aefcac92f81d9e7ca631e680a2268cbe62cd6a58de6409b7", size = 28389, upload-time = "2025-04-16T16:53:43.612Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/68c50fcfd81e11480cd8ee7f654c9bd790a9053b9a0efe9983d46106f6a9/lazy_object_proxy-1.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0613116156801ab3fccb9e2b05ed83b08ea08c2517fdc6c6bc0d4697a1a376e3", size = 28777, upload-time = "2025-04-16T16:53:41.371Z" }, + { url = "https://files.pythonhosted.org/packages/39/d0/7e967689e24de8ea6368ec33295f9abc94b9f3f0cd4571bfe148dc432190/lazy_object_proxy-1.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bb03c507d96b65f617a6337dedd604399d35face2cdf01526b913fb50c4cb6e8", size = 29598, upload-time = "2025-04-16T16:53:42.513Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1e/fb441c07b6662ec1fc92b249225ba6e6e5221b05623cb0131d082f782edc/lazy_object_proxy-1.11.0-py3-none-any.whl", hash = "sha256:a56a5093d433341ff7da0e89f9b486031ccd222ec8e52ec84d0ec1cdc819674b", size = 16635, upload-time = "2025-04-16T16:53:47.198Z" }, +] + [[package]] name = "locket" version = "1.0.0" @@ -331,24 +544,64 @@ 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.9.4" +version = "1.12.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "httpx-sse" }, + { name = "jsonschema" }, { 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/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/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/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/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] @@ -366,6 +619,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" @@ -375,6 +637,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" @@ -387,6 +669,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" @@ -396,6 +707,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" @@ -409,6 +729,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" @@ -418,6 +747,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" @@ -433,6 +771,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" @@ -498,6 +841,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" @@ -558,6 +907,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" @@ -584,6 +949,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" @@ -599,6 +978,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" @@ -612,6 +1003,100 @@ 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" +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 +1217,7 @@ wheels = [ [[package]] name = "sysdig-mcp-server" -version = "0.1.4" +version = "0.1.5" source = { editable = "." } dependencies = [ { name = "dask" }, @@ -745,7 +1230,7 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, { name = "sqlmodel" }, - { name = "sysdig-sdk" }, + { name = "sysdig-sdk-python" }, ] [package.dev-dependencies] @@ -759,15 +1244,15 @@ dev = [ requires-dist = [ { name = "dask", specifier = "==2025.4.1" }, { name = "fastapi", specifier = "==0.115.12" }, - { name = "fastmcp", specifier = "==2.5.1" }, - { name = "mcp", extras = ["cli"], specifier = "==1.9.4" }, + { 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=15d2c6bd92d9cb26b8ca19c6eef3be991b8e43d2" }, ] [package.metadata.requires-dev] @@ -778,9 +1263,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=15d2c6bd92d9cb26b8ca19c6eef3be991b8e43d2#15d2c6bd92d9cb26b8ca19c6eef3be991b8e43d2" } dependencies = [ { name = "pydantic" }, { name = "python-dateutil" }, @@ -856,32 +1341,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 fe9f89645187c5a878165e11fc111a5ae792669f Mon Sep 17 00:00:00 2001 From: S3B4SZ17 Date: Wed, 20 Aug 2025 18:04:21 -0600 Subject: [PATCH 2/2] feat: Moving logic of init API instances to the middleware and FastMCP context Signed-off-by: S3B4SZ17 --- app_config.yaml | 10 ++ charts/sysdig-mcp/values.yaml | 12 +- pyproject.toml | 4 +- tools/events_feed/tool.py | 52 ++------ tools/inventory/tool.py | 30 +---- tools/sysdig_sage/tool.py | 34 +---- tools/vulnerability_management/tool.py | 58 ++++---- utils/{middleware => auth}/__init__.py | 0 utils/auth/auth_config.py | 28 ++++ utils/auth/middleware/__init__.py | 1 + utils/auth/middleware/auth.py | 161 ++++++++++++++++++++++ utils/mcp_server.py | 5 +- utils/middleware/auth.py | 177 ------------------------- utils/sysdig/api.py | 51 ------- utils/sysdig/client_config.py | 37 +++++- utils/sysdig/helpers.py | 3 +- utils/sysdig/legacy_sysdig_api.py | 3 +- uv.lock | 6 +- 18 files changed, 299 insertions(+), 373 deletions(-) rename utils/{middleware => auth}/__init__.py (100%) create mode 100644 utils/auth/auth_config.py create mode 100644 utils/auth/middleware/__init__.py create mode 100644 utils/auth/middleware/auth.py delete mode 100644 utils/middleware/auth.py delete mode 100644 utils/sysdig/api.py diff --git a/app_config.yaml b/app_config.yaml index d047b53..f496585 100644 --- a/app_config.yaml +++ b/app_config.yaml @@ -16,3 +16,13 @@ mcp: - "vulnerability" - "inventory" - "sage" + +oauth: + enabled: false + jwks_uri: "https://auth.yourcompany.com/.well-known/jwks.json" + issuer: "https://auth.yourcompany.com" + audience: "mcp-production-api" + resource_server_url: "https://mcp.ingress.com" # or http://localhost:8080 + required_scopes: + - "read:users" + - "read:orgs" diff --git a/charts/sysdig-mcp/values.yaml b/charts/sysdig-mcp/values.yaml index d165830..3417220 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.5" + tag: "v0.2.0" imagePullSecrets: [] nameOverride: "" @@ -131,3 +131,13 @@ configMap: - "vulnerability" - "inventory" - "sage" + + oauth: + enabled: false + jwks_uri: "https://auth.yourcompany.com/.well-known/jwks.json" + issuer: "https://auth.yourcompany.com" + audience: "mcp-production-api" + resource_server_url: "https://mcp.ingress.com" # or http://localhost:8080 + required_scopes: + - "read:users" + - "read:orgs" diff --git a/pyproject.toml b/pyproject.toml index 6f0f9b4..7f3a049 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sysdig-mcp-server" -version = "0.1.5" +version = "0.2.0" description = "Sysdig MCP Server" readme = "README.md" requires-python = ">=3.12" @@ -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@15d2c6bd92d9cb26b8ca19c6eef3be991b8e43d2", + "sysdig-sdk-python @ git+https://github.com/sysdiglabs/sysdig-sdk-python@597285143188019cd0e86fde43f94b1139f5441d", "dask==2025.4.1", "oauthlib==3.2.2", "fastapi==0.115.12", diff --git a/tools/events_feed/tool.py b/tools/events_feed/tool.py index b3c3a26..556131b 100644 --- a/tools/events_feed/tool.py +++ b/tools/events_feed/tool.py @@ -14,14 +14,10 @@ from fastmcp.prompts.prompt import PromptMessage, TextContent from fastmcp.exceptions import ToolError from fastmcp.server.context import Context -from starlette.requests import Request from sysdig_client.api import SecureEventsApi from utils.sysdig.legacy_sysdig_api import LegacySysdigApi -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.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__) @@ -36,39 +32,6 @@ class EventsFeedTools: This class provides methods to retrieve event information and list runtime events. """ - def init_client(self, transport: str, old_api: bool = False) -> SecureEventsApi | LegacySysdigApi: - """ - 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 LegacySysdigApi client instead of SecureEventsApi. - Returns: - SecureEventsApi | LegacySysdigApi: An instance of the SecureEventsApi or LegacySysdigApi client. - """ - secure_events_api: SecureEventsApi = None - legacy_sysdig_api: LegacySysdigApi = None - 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.") - request: Request = get_http_request() - secure_events_api = request.state.api_instances["secure_events"] - legacy_sysdig_api = request.state.api_instances["legacy_sysdig_api"] - else: - # If running in STDIO mode, we need to initialize the API client from environment variables - log.debug("Trying to init the Sysdig API client from environment variables.") - # Initialize the old Sysdig API client for process tree requests - if old_api: - old_cfg = get_configuration(old_api=True) - legacy_sysdig_api = initialize_api_client(old_cfg) - legacy_sysdig_api = LegacySysdigApi(legacy_sysdig_api) - else: - cfg = get_configuration() - api_client = initialize_api_client(cfg) - secure_events_api = SecureEventsApi(api_client) - - return legacy_sysdig_api if old_api else secure_events_api - def tool_get_event_info(self, ctx: Context, event_id: str) -> dict: """ Retrieves detailed information for a specific security event. @@ -80,7 +43,8 @@ def tool_get_event_info(self, ctx: Context, event_id: str) -> dict: Event: The Event object containing detailed information about the specified event. """ # Init of the sysdig client - secure_events_api = self.init_client(ctx.get_state("transport_method")) + 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() @@ -148,7 +112,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(ctx.get_state("transport_method")) + 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() @@ -192,12 +158,14 @@ def tool_get_event_process_tree(self, ctx: Context, event_id: str) -> dict: dict: A dictionary containing the process tree information for the specified event. """ 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(transport=ctx.get_state("transport_method"), 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 branches = create_standard_response(results=branches, execution_time_ms=(time.time() - start_time) * 1000) diff --git a/tools/inventory/tool.py b/tools/inventory/tool.py index 3dac3aa..4293911 100644 --- a/tools/inventory/tool.py +++ b/tools/inventory/tool.py @@ -9,12 +9,9 @@ from pydantic import Field from fastmcp.server.dependencies import get_http_request from fastmcp.exceptions import ToolError -from starlette.requests import Request from fastmcp.server.context import Context from sysdig_client.api import InventoryApi -from utils.sysdig.client_config import get_configuration from utils.app_config import get_app_config -from utils.sysdig.api import initialize_api_client from utils.query_helpers import create_standard_response # Configure logging @@ -31,29 +28,6 @@ class InventoryTools: This class provides methods to list resources and retrieve a single resource by its hash. """ - def init_client(self, transport: str) -> 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 = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() - 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.") - 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("Trying to init 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, @@ -167,7 +141,9 @@ def tool_list_resources( Or a dict containing an error message if the call fails. """ try: - inventory_api = self.init_client(ctx.get_state("transport_method")) + 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( diff --git a/tools/sysdig_sage/tool.py b/tools/sysdig_sage/tool.py index f00dc0c..758541e 100644 --- a/tools/sysdig_sage/tool.py +++ b/tools/sysdig_sage/tool.py @@ -10,11 +10,7 @@ from fastmcp.exceptions import ToolError from fastmcp.server.context import Context from utils.sysdig.legacy_sysdig_api import LegacySysdigApi -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.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")) @@ -30,28 +26,6 @@ class SageTools: language questions and execute them against the Sysdig API. """ - def init_client(self, transport: str) -> LegacySysdigApi: - """ - Initializes the LegacySysdigApi 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: - LegacySysdigApi: An instance of the LegacySysdigApi client. - """ - legacy_sysdig_api: LegacySysdigApi = None - 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.") - request: Request = get_http_request() - legacy_sysdig_api = request.state.api_instances["legacy_sysdig_api"] - else: - # If running in STDIO mode, we need to initialize the API client from environment variables - log.debug("Trying to init the Sysdig API client from environment variables.") - cfg = get_configuration(old_api=True) - api_client = initialize_api_client(cfg) - legacy_sysdig_api = LegacySysdigApi(api_client) - return legacy_sysdig_api - async def tool_sage_to_sysql(self, ctx: Context, question: str) -> dict: """ Queries Sysdig Sage with a natural language question, retrieves a SysQL query, @@ -74,8 +48,10 @@ async def tool_sage_to_sysql(self, ctx: Context, question: str) -> dict: # 1) Generate SysQL query try: start_time = time.time() - legacy_sysdig_api = self.init_client(ctx.get_state("transport_method")) - sysql_response = await legacy_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 +65,7 @@ async def tool_sage_to_sysql(self, ctx: Context, question: str) -> dict: # 2) Execute generated SysQL query try: log.debug(f"Executing SysQL query: {syslq_query}") - results = legacy_sysdig_api.execute_sysql_query(syslq_query) + results = legacy_api_client.execute_sysql_query(syslq_query) execution_time = (time.time() - start_time) * 1000 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 4d30fec..468b915 100644 --- a/tools/vulnerability_management/tool.py +++ b/tools/vulnerability_management/tool.py @@ -12,12 +12,8 @@ from fastmcp.prompts.prompt import PromptMessage, TextContent from fastmcp.exceptions import ToolError from fastmcp.server.context import Context -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 utils.app_config import get_app_config -from utils.sysdig.api import initialize_api_client from utils.query_helpers import create_standard_response # Configure logging @@ -34,28 +30,6 @@ class VulnerabilityManagementTools: and vulnerability policies. """ - def init_client(self, transport: str) -> 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 - 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.") - 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("Trying to init 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, @@ -146,7 +120,9 @@ def tool_list_runtime_vulnerabilities( - execution_time_ms (float): Execution duration in milliseconds. """ try: - vulnerability_api = self.init_client(ctx.get_state("transport_method")) + 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( @@ -188,7 +164,9 @@ def tool_list_accepted_risks( dict: The API response as a dictionary, or an error dict on failure. """ try: - vulnerability_api = self.init_client(ctx.get_state("transport_method")) + 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 @@ -217,7 +195,9 @@ 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: - vulnerability_api = self.init_client(ctx.get_state("transport_method")) + 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: @@ -271,7 +251,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(ctx.get_state("transport_method")) + 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 @@ -302,7 +284,9 @@ def tool_get_vulnerability_policy( dict: An error dict on failure. """ try: - vulnerability_api = self.init_client(ctx.get_state("transport_method")) + 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: @@ -331,7 +315,9 @@ def tool_list_vulnerability_policies( """ start_time = time.time() try: - vulnerability_api = self.init_client(ctx.get_state("transport_method")) + 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 ) @@ -400,7 +386,9 @@ def tool_list_pipeline_scan_results( """ start_time = time.time() try: - vulnerability_api = self.init_client(ctx.get_state("transport_method")) + 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 ) @@ -427,7 +415,9 @@ def tool_get_scan_result(self, ctx: Context, scan_id: str) -> dict: dict: ScanResultResponse as dict, or {"error": ...}. """ try: - vulnerability_api = self.init_client(ctx.get_state("transport_method")) + 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/middleware/__init__.py b/utils/auth/__init__.py similarity index 100% rename from utils/middleware/__init__.py rename to utils/auth/__init__.py diff --git a/utils/auth/auth_config.py b/utils/auth/auth_config.py new file mode 100644 index 0000000..df88c6b --- /dev/null +++ b/utils/auth/auth_config.py @@ -0,0 +1,28 @@ +""" +Auth configuration for the MCP server. +""" + +from fastmcp.server.auth import RemoteAuthProvider +from fastmcp.server.auth.providers.jwt import JWTVerifier +from pydantic import AnyHttpUrl +from utils.app_config import get_app_config + +# Load app config (expects keys: mcp.host, mcp.port, mcp.transport) +app_config = get_app_config() + +# Configure token validation for your identity provider +token_verifier = JWTVerifier( + jwks_uri=get_app_config().get("oauth", {}).get("jwks_uri", ""), + issuer=get_app_config().get("oauth", {}).get("issuer", ""), + audience=get_app_config().get("oauth", {}).get("audience", ""), + required_scopes=get_app_config().get("oauth", {}).get("required_scopes", []), +) + +# Create the remote auth provider +remote_auth_provider: RemoteAuthProvider = None +if get_app_config().get("oauth", {}).get("enabled", False): + remote_auth_provider = RemoteAuthProvider( + token_verifier=token_verifier, + authorization_servers=[AnyHttpUrl(get_app_config().get("oauth", {}).get("issuer", ""))], + resource_server_url=AnyHttpUrl(get_app_config().get("oauth", {}).get("resource_server_url", "")), + ) diff --git a/utils/auth/middleware/__init__.py b/utils/auth/middleware/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/utils/auth/middleware/__init__.py @@ -0,0 +1 @@ + diff --git a/utils/auth/middleware/auth.py b/utils/auth/middleware/auth.py new file mode 100644 index 0000000..0e1ccb0 --- /dev/null +++ b/utils/auth/middleware/auth.py @@ -0,0 +1,161 @@ +""" +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 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() + +# 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_id (str): The ID of 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) -> 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"]["host"]) or str(request.base_url) + log.info(f"Using Sysdig API base URL: {base_url}") + + cfg = get_configuration(token, base_url) + legacy_cfg = get_configuration(token, base_url, old_api=True) + else: + # If running in STDIO mode, we initialize the API client from environment variables + cfg = get_configuration() + legacy_cfg = get_configuration(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. + """ + + # 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", app_config["mcp"]["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) + + 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 diff --git a/utils/mcp_server.py b/utils/mcp_server.py index e395338..04df290 100644 --- a/utils/mcp_server.py +++ b/utils/mcp_server.py @@ -16,7 +16,8 @@ from fastmcp.prompts import Prompt from fastmcp.settings import Settings from fastmcp.resources import HttpResource, TextResource -from utils.middleware.auth import CustomMiddleware +from utils.auth.middleware.auth import CustomMiddleware +from utils.auth.auth_config import remote_auth_provider from tools.events_feed.tool import EventsFeedTools from tools.inventory.tool import InventoryTools from tools.vulnerability_management.tool import VulnerabilityManagementTools @@ -55,6 +56,7 @@ def create_simple_mcp_server() -> FastMCP: instructions="Provides Sysdig Secure tools and resources.", include_tags=["sysdig_secure"], middleware=middlewares, + auth=remote_auth_provider, ) @@ -109,7 +111,6 @@ def run_http(): async def health_check(request: Request) -> Response: """ Health check endpoint. - Args: request (Request): The incoming HTTP request. Returns: diff --git a/utils/middleware/auth.py b/utils/middleware/auth.py deleted file mode 100644 index 562f825..0000000 --- a/utils/middleware/auth.py +++ /dev/null @@ -1,177 +0,0 @@ -""" -Custom middleware for authorization and access control -""" - -import json -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.api 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 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() - - -async def _get_permissions(context: MiddlewareContext) -> None: - """ - Get the permissions for the current user and set them in the context. - Args: - context (MiddlewareContext): The middleware context. - """ - try: - legacy_sysdig_api = _init_legacy_sysdig_api(context) - permissions = legacy_sysdig_api.get_me_permissions() - context.fastmcp_context.set_state("permissions", permissions.json().get("permissions", [])) - except Exception as e: - log.error(f"Error fetching permissions: {e}") - raise - - -def _init_legacy_sysdig_api(context: MiddlewareContext) -> LegacySysdigApi: - """ - Initialize the legacy Sysdig API client either from the HTTP request state or from environment variables. - Args: - context (MiddlewareContext): The middleware context. - - Returns: - LegacySysdigApi: The initialized legacy Sysdig API client. - """ - try: - legacy_sysdig_api: LegacySysdigApi = None - transport = context.fastmcp_context.get_state("transport_method") - 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.") - request: Request = get_http_request() - legacy_sysdig_api = request.state.api_instances["legacy_sysdig_api"] - else: - # If running in STDIO mode, we need to initialize the API client from environment variables - log.debug("Trying to init the Sysdig API client from environment variables.") - cfg = get_configuration(old_api=True) - api_client = initialize_api_client(cfg) - legacy_sysdig_api = LegacySysdigApi(api_client) - return legacy_sysdig_api - except Exception as e: - log.error(f"Error initializing legacy Sysdig API: {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_id (str): The ID of 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 - await _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_instance() -> None: - """ - Save the API client instance to the request state. - - Raises: - Exception: If the Authorization header is missing or invalid. - """ - request = get_http_request() - auth_header = request.headers.get("Authorization") - if not auth_header or not auth_header.startswith("Bearer "): - raise Exception("Missing or invalid Authorization header") - # set header to be used by the API client - - # Extract releavant information from the request headers - token = auth_header.removeprefix("Bearer ").strip() - base_url = request.headers.get("X-Sysdig-Host", app_config["sysdig"]["host"]) or str(request.base_url) - log.info(f"Using Sysdig API base URL: {base_url}") - - # Initialize the API client with the token and base URL - # TODO: Implement a more elegant solution for API client initialization, we will end up having multiple API instances - cfg = get_configuration(token, base_url) - legacy_cfg = get_configuration(token, base_url, 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) - _legacy_sysdig_api = LegacySysdigApi(legacy_sysdig_api) - api_instances["legacy_sysdig_api"] = _legacy_sysdig_api - # Having access to the Sysdig API instances per request to be used by the MCP tools - request.state.api_instances = api_instances - - -class CustomMiddleware(Middleware): - """ - Custom middleware for filtering tool listings and performing authentication. - """ - - # 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. - """ - # FIXME: Currently not able to get the notifications/initialized that should be the one initializing the API instances - # for the whole session - allowed_notifications = ["notifications/initialized", "tools/list", "tools/call"] - # Save transport method in context - if not context.fastmcp_context.get_state("transport_method"): - transport_method = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower() - context.fastmcp_context.set_state("transport_method", transport_method) - try: - if ( - context.fastmcp_context.get_state("transport_method") in ["streamable-http", "sse"] - and context.method in allowed_notifications - ): - await _save_api_instance() - - return await call_next(context) - except Exception as error: - log.error(f"Error in {context.method}: {type(error).__name__}: {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) - - filtered_tools = [tool for tool in result if await allowed_tool(context, tool)] - - if not filtered_tools: - log.warning(f"No allowed tools found for session: {context.fastmcp_context.session_id}") - raise Exception("No allowed tools found for the user.") - # Return modified list - return filtered_tools diff --git a/utils/sysdig/api.py b/utils/sysdig/api.py deleted file mode 100644 index 90d063c..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.legacy_sysdig_api import LegacySysdigApi -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), - "legacy_sysdig_api": LegacySysdigApi(api_client), - } diff --git a/utils/sysdig/client_config.py b/utils/sysdig/client_config.py index 7849ded..b3d23b1 100644 --- a/utils/sysdig/client_config.py +++ b/utils/sysdig/client_config.py @@ -7,6 +7,8 @@ import logging import re from typing import Optional +from sysdig_client import ApiClient, SecureEventsApi, VulnerabilityManagementApi, InventoryApi +from sysdig_client.configuration import Configuration # Application config loader from utils.app_config import get_app_config @@ -18,6 +20,35 @@ app_config = get_app_config() +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 @@ -57,7 +88,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, @@ -78,7 +108,10 @@ def get_api_env_vars() -> dict: required_vars = ["SYSDIG_SECURE_TOKEN", "SYSDIG_HOST"] env_vars = {} for var in required_vars: - value = os.environ.get(var) + if var == "SYSDIG_HOST": + value = app_config["sysdig"].get("host", "") or os.environ.get(var) + else: + 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.") diff --git a/utils/sysdig/helpers.py b/utils/sysdig/helpers.py index 1339d0a..db687db 100644 --- a/utils/sysdig/helpers.py +++ b/utils/sysdig/helpers.py @@ -2,8 +2,7 @@ Helper functions for working with Sysdig API clients. """ -# Tool permissions by tag - +# Sysdig permissions needed for the different set of tools TOOL_PERMISSIONS = { "inventory": ["explore.read"], "vulnerability": ["scanning.read", "secure.vm.scanresults.read"], diff --git a/utils/sysdig/legacy_sysdig_api.py b/utils/sysdig/legacy_sysdig_api.py index 1c48348..c26bb23 100644 --- a/utils/sysdig/legacy_sysdig_api.py +++ b/utils/sysdig/legacy_sysdig_api.py @@ -10,7 +10,8 @@ class LegacySysdigApi: """ - Wrapper for Old non public Sysdig API. + [Deprecated] + Wrapper for Legacy Sysdig API. """ def __init__(self, api_client: ApiClient): diff --git a/uv.lock b/uv.lock index 3c8e791..2de4d14 100644 --- a/uv.lock +++ b/uv.lock @@ -1217,7 +1217,7 @@ wheels = [ [[package]] name = "sysdig-mcp-server" -version = "0.1.5" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "dask" }, @@ -1252,7 +1252,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=15d2c6bd92d9cb26b8ca19c6eef3be991b8e43d2" }, + { name = "sysdig-sdk-python", git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=597285143188019cd0e86fde43f94b1139f5441d" }, ] [package.metadata.requires-dev] @@ -1265,7 +1265,7 @@ dev = [ [[package]] name = "sysdig-sdk-python" version = "0.19.1" -source = { git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=15d2c6bd92d9cb26b8ca19c6eef3be991b8e43d2#15d2c6bd92d9cb26b8ca19c6eef3be991b8e43d2" } +source = { git = "https://github.com/sysdiglabs/sysdig-sdk-python?rev=597285143188019cd0e86fde43f94b1139f5441d#597285143188019cd0e86fde43f94b1139f5441d" } dependencies = [ { name = "pydantic" }, { name = "python-dateutil" },