diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7abc5a2
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,171 @@
+# GhydraMCP Makefile
+# Convenient commands for Docker and development operations
+
+.PHONY: help build build-dev up up-dev down down-dev logs logs-dev \
+ shell status clean analyze test health
+
+# Default target
+help:
+ @echo "GhydraMCP Docker Management"
+ @echo "============================"
+ @echo ""
+ @echo "Build commands:"
+ @echo " make build Build production Docker image"
+ @echo " make build-dev Build development Docker image"
+ @echo " make build-all Build both images"
+ @echo ""
+ @echo "Run commands:"
+ @echo " make up Start production container"
+ @echo " make up-dev Start development container"
+ @echo " make down Stop production container"
+ @echo " make down-dev Stop development container"
+ @echo " make down-all Stop all containers"
+ @echo ""
+ @echo "Analysis commands:"
+ @echo " make analyze FILE=path/to/binary Analyze a binary"
+ @echo " make analyze-bg FILE=path/to/binary Analyze in background"
+ @echo ""
+ @echo "Utility commands:"
+ @echo " make shell Start interactive shell in container"
+ @echo " make logs View production container logs"
+ @echo " make logs-dev View development container logs"
+ @echo " make status Check container status"
+ @echo " make health Check API health"
+ @echo " make clean Remove containers and volumes"
+ @echo " make clean-all Remove everything including images"
+ @echo ""
+ @echo "MCP Server commands:"
+ @echo " make mcp Start the MCP server (Python)"
+ @echo " make mcp-dev Start MCP server in development mode"
+ @echo ""
+
+# =============================================================================
+# Build Commands
+# =============================================================================
+
+build:
+ docker compose build ghydramcp
+
+build-dev:
+ docker compose build ghydramcp-dev
+
+build-all: build build-dev
+
+# =============================================================================
+# Run Commands
+# =============================================================================
+
+up:
+ docker compose --profile prod up -d ghydramcp
+ @echo "GhydraMCP starting... checking health in 30 seconds"
+ @sleep 30
+ @$(MAKE) health || echo "Server may still be starting up..."
+
+up-dev:
+ docker compose --profile dev up -d ghydramcp-dev
+ @echo "GhydraMCP (dev) starting..."
+
+down:
+ docker compose --profile prod down
+
+down-dev:
+ docker compose --profile dev down
+
+down-all:
+ docker compose --profile prod --profile dev --profile debug down
+
+restart: down up
+
+restart-dev: down-dev up-dev
+
+# =============================================================================
+# Analysis Commands
+# =============================================================================
+
+# Analyze a binary file
+# Usage: make analyze FILE=/path/to/binary
+analyze:
+ifndef FILE
+ @echo "Error: FILE is required. Usage: make analyze FILE=/path/to/binary"
+ @exit 1
+endif
+ @echo "Analyzing: $(FILE)"
+ docker compose run --rm -v "$(dir $(FILE)):/binaries:ro" ghydramcp /binaries/$(notdir $(FILE))
+
+# Analyze in background (detached)
+analyze-bg:
+ifndef FILE
+ @echo "Error: FILE is required. Usage: make analyze-bg FILE=/path/to/binary"
+ @exit 1
+endif
+ @echo "Starting background analysis of: $(FILE)"
+ docker compose run -d -v "$(dir $(FILE)):/binaries:ro" ghydramcp /binaries/$(notdir $(FILE))
+
+# =============================================================================
+# Utility Commands
+# =============================================================================
+
+shell:
+ docker compose --profile debug run --rm ghydramcp-shell
+
+logs:
+ docker compose logs -f ghydramcp
+
+logs-dev:
+ docker compose logs -f ghydramcp-dev
+
+status:
+ @echo "=== Container Status ==="
+ @docker compose ps -a
+ @echo ""
+ @echo "=== Resource Usage ==="
+ @docker stats --no-stream $$(docker compose ps -q 2>/dev/null) 2>/dev/null || echo "No containers running"
+
+health:
+ @echo "Checking GhydraMCP API health..."
+ @curl -sf http://localhost:$${GHYDRA_PORT:-8192}/ | python3 -m json.tool 2>/dev/null \
+ || echo "API not responding (server may be starting or binary being analyzed)"
+
+# =============================================================================
+# Cleanup Commands
+# =============================================================================
+
+clean:
+ docker compose --profile prod --profile dev --profile debug down -v
+ @echo "Containers and volumes removed"
+
+clean-all: clean
+ docker rmi ghydramcp:latest ghydramcp:dev 2>/dev/null || true
+ @echo "Images removed"
+
+prune:
+ docker system prune -f
+ @echo "Docker system pruned"
+
+# =============================================================================
+# MCP Server Commands
+# =============================================================================
+
+mcp:
+ uv run python -m ghydramcp
+
+mcp-dev:
+ uv run python -m ghydramcp --verbose
+
+# =============================================================================
+# Development Commands
+# =============================================================================
+
+test:
+ uv run pytest tests/ -v
+
+lint:
+ uv run ruff check src/
+
+format:
+ uv run ruff format src/
+
+# Check if binaries directory exists
+check-binaries:
+ @mkdir -p binaries
+ @echo "Binaries directory ready at ./binaries/"
diff --git a/QUICKSTART.md b/QUICKSTART.md
new file mode 100644
index 0000000..d4f4916
--- /dev/null
+++ b/QUICKSTART.md
@@ -0,0 +1,328 @@
+# GhydraMCP Quick Start Guide
+
+## What is GhydraMCP?
+
+GhydraMCP is a complete reverse engineering platform that combines:
+- **Ghidra** - NSA's powerful binary analysis tool
+- **Docker** - Containerized, reproducible analysis environment
+- **HTTP REST API** - HATEOAS-compliant REST interface
+- **MCP Server** - FastMCP-based Model Context Protocol integration
+- **ARM Firmware Support** - Tools for analyzing raw embedded firmware
+
+## 5-Minute Quick Start
+
+### 1. Analyze a Standard Binary (ELF/PE/Mach-O)
+
+```bash
+cd /home/rpm/claude/ghydramcp/GhydraMCP
+
+# Build the Docker image (one time)
+docker build -t ghydramcp:latest -f docker/Dockerfile .
+
+# Analyze any standard binary
+docker run -d --name my-analysis \
+ -p 8192:8192 \
+ -v $(pwd)/binaries:/binaries \
+ ghydramcp:latest \
+ /binaries/your-binary
+
+# Wait ~20 seconds for analysis, then access HTTP API
+curl http://localhost:8192/
+curl http://localhost:8192/functions | jq '.functions[] | {name, address}'
+curl http://localhost:8192/functions/
/decompile
+```
+
+### 2. Analyze ARM Firmware (Raw Binary)
+
+```bash
+# Step 1: Create ELF wrapper
+python3 docker/arm_firmware_prep.py \
+ your-firmware.bin \
+ binaries/your-firmware.elf \
+ 0x00000000
+
+# Step 2: Analyze normally
+docker run -d --name arm-firmware \
+ -p 8192:8192 \
+ -v $(pwd)/binaries:/binaries \
+ ghydramcp:latest \
+ /binaries/your-firmware.elf
+```
+
+### 3. Use the MCP Server
+
+```bash
+# The MCP server is located at:
+cd /home/rpm/claude/ghydramcp/GhydraMCP
+./launch.sh
+
+# Or with uv:
+cd GhydraMCP && uv run ghydramcp
+```
+
+## HTTP API Overview
+
+Once analysis completes, the API is available at `http://localhost:8192/`:
+
+### Core Endpoints
+
+```bash
+# Program information
+GET /program
+
+# Functions
+GET /functions # List all functions
+GET /functions/ # Function details
+GET /functions//decompile # Decompiled C code
+GET /functions//disassembly # Assembly listing
+GET /functions//variables # Local variables
+
+# Analysis
+GET /analysis/callgraph?name=main&max_depth=3
+GET /analysis/dataflow?address=&direction=forward
+
+# Memory
+GET /memory/?length=256&format=hex
+POST /memory/ # Write bytes
+
+# Data & Structures
+GET /data/strings
+GET /structs
+GET /xrefs?to_addr=
+```
+
+### Response Format (HATEOAS)
+
+All responses include navigation links:
+
+```json
+{
+ "success": true,
+ "result": {
+ "name": "main",
+ "address": "00101380",
+ "signature": "int main(void)"
+ },
+ "_links": {
+ "self": "/functions/00101380",
+ "decompile": "/functions/00101380/decompile",
+ "disassembly": "/functions/00101380/disassembly"
+ }
+}
+```
+
+## MCP Tools Overview
+
+Use with Claude Code or any MCP client:
+
+```python
+# Functions
+functions_list(port=8192, page_size=50)
+functions_decompile(address="00101380", port=8192)
+functions_get(name="main", port=8192)
+
+# Analysis
+analysis_get_callgraph(name="main", max_depth=3, port=8192)
+analysis_get_dataflow(address="00101380", direction="forward", port=8192)
+
+# Data
+data_list_strings(port=8192, grep="password")
+structs_list(port=8192)
+
+# Docker Management
+docker_status()
+docker_start(binary_path="/path/to/binary", port=8192)
+docker_stop(name_or_id="container-name")
+docker_logs(name_or_id="container-name", tail=100)
+```
+
+## Common Workflows
+
+### Find Interesting Functions
+
+```bash
+# List all functions
+curl http://localhost:8192/functions | jq '.functions[].name'
+
+# Search for crypto-related functions
+curl http://localhost:8192/functions | jq '.functions[] | select(.name | test("crypt|hash|encrypt"; "i"))'
+
+# Get call graph from main
+curl 'http://localhost:8192/analysis/callgraph?name=main&max_depth=2' | jq .
+```
+
+### Analyze Strings
+
+```bash
+# List all strings
+curl http://localhost:8192/data/strings | jq '.strings[] | {address, value}'
+
+# Find passwords/keys
+curl http://localhost:8192/data/strings | jq '.strings[] | select(.value | test("password|key|secret"; "i"))'
+```
+
+### Decompile Entry Point
+
+```bash
+# Get program entry point
+ENTRY=$(curl -s http://localhost:8192/program | jq -r '.program.entryPoint')
+
+# Decompile it
+curl "http://localhost:8192/functions/$ENTRY/decompile" | jq -r '.result'
+```
+
+## Docker Management
+
+### List Running Containers
+
+```bash
+docker ps | grep ghydramcp
+```
+
+### View Logs
+
+```bash
+docker logs -f my-analysis
+```
+
+### Stop Analysis
+
+```bash
+docker stop my-analysis
+docker rm my-analysis
+```
+
+### Persistent Projects
+
+```bash
+# Mount project directory for persistence
+docker run -d --name persistent \
+ -p 8192:8192 \
+ -v $(pwd)/projects:/projects \
+ -v $(pwd)/binaries:/binaries \
+ -e PROJECT_NAME=MyProject \
+ ghydramcp:latest \
+ /binaries/my-binary
+
+# Projects are saved in ./projects/MyProject/
+```
+
+## Troubleshooting
+
+### Import Failed
+
+```bash
+# Check logs
+docker logs my-analysis 2>&1 | grep ERROR
+
+# Common issues:
+# 1. Binary not found → Check volume mount path
+# 2. AutoImporter failed → Use arm_firmware_prep.py for raw binaries
+# 3. Unsupported format → Check file type with `file binary`
+```
+
+### Script Errors
+
+```bash
+# If you see "Failed to get OSGi bundle" errors
+# Fix script permissions in running container:
+docker exec my-analysis sh -c 'chmod 644 /opt/ghidra/scripts/*.java'
+
+# Then restart the analysis
+```
+
+### Port Already in Use
+
+```bash
+# Use different port
+docker run -d --name analysis2 \
+ -p 8193:8192 \
+ -v $(pwd)/binaries:/binaries \
+ ghydramcp:latest \
+ /binaries/binary
+
+# Access at http://localhost:8193/
+```
+
+## Examples
+
+### Example 1: Analyze Test Binary
+
+```bash
+# Create simple test binary
+cat > test.c << 'EOF'
+#include
+int secret_value = 0x42;
+void hidden() { printf("Hidden: %d\n", secret_value); }
+int main() { printf("Hello!\n"); return 0; }
+EOF
+
+gcc -o binaries/test test.c
+
+# Analyze
+docker run -d --name test-analysis \
+ -p 8192:8192 \
+ -v $(pwd)/binaries:/binaries \
+ ghydramcp:latest \
+ /binaries/test
+
+# Find hidden function
+sleep 15
+curl http://localhost:8192/functions | jq '.functions[] | select(.name == "hidden")'
+```
+
+### Example 2: Cisco Phone Firmware
+
+```bash
+# Prepare firmware
+python3 docker/arm_firmware_prep.py \
+ cisco-firmware/P003-8-12-00.bin \
+ binaries/cisco.elf \
+ 0x00000000
+
+# Analyze
+docker run -d --name cisco \
+ -p 8192:8192 \
+ -v $(pwd)/binaries:/binaries \
+ ghydramcp:latest \
+ /binaries/cisco.elf
+
+# Explore
+sleep 30
+curl http://localhost:8192/functions | jq '.functions | length' # Function count
+curl http://localhost:8192/data/strings | jq '.strings[] | select(.value | test("SIP|RTP"))'
+```
+
+## Next Steps
+
+- **Read ARM_FIRMWARE_SUCCESS.md** for ARM firmware details
+- **Check docker/README_ARM_SOLUTION.md** for advanced ARM workflows
+- **Explore MCP integration** with Claude Code
+- **Build automations** using the HTTP API
+
+## Project Structure
+
+```
+GhydraMCP/
+├── docker/
+│ ├── Dockerfile # Main Docker image
+│ ├── entrypoint.sh # Container entry point
+│ ├── GhydraMCPServer.java # HTTP API server (1724 lines)
+│ ├── ImportRawARM.java # Raw binary import script
+│ ├── arm_firmware_prep.py # ELF wrapper tool ⭐
+│ └── README*.md # Documentation
+├── src/ghydramcp/ # MCP server implementation
+│ ├── __init__.py
+│ ├── server.py # FastMCP server
+│ └── mixins/ # Modular functionality
+│ ├── docker.py # Docker management
+│ ├── instances.py # Instance registry
+│ ├── functions.py # Function operations
+│ ├── analysis.py # Analysis tools
+│ └── ...
+├── binaries/ # Binary files for analysis
+├── projects/ # Ghidra project persistence
+└── launch.sh # MCP server launcher
+```
+
+Happy reverse engineering! 🔍
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..4a6cfe5
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,116 @@
+# GhydraMCP Docker Compose Configuration
+# Provides both development and production modes for Ghidra + GhydraMCP
+#
+# Usage:
+# Development: docker compose up ghydramcp-dev
+# Production: docker compose up ghydramcp
+#
+# Set MODE in .env file to switch between dev/prod behaviors
+
+services:
+ # =============================================================================
+ # Production Service - Optimized for stability and security
+ # =============================================================================
+ ghydramcp:
+ build:
+ context: .
+ dockerfile: docker/Dockerfile
+ args:
+ GHIDRA_VERSION: ${GHIDRA_VERSION:-11.4.2}
+ GHIDRA_DATE: ${GHIDRA_DATE:-20250826}
+ image: ghydramcp:${GHYDRAMCP_VERSION:-latest}
+ container_name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-server
+ restart: unless-stopped
+ ports:
+ - "${GHYDRA_PORT:-8192}:8192"
+ volumes:
+ # Mount binaries to analyze (read-only in prod)
+ - ${BINARIES_PATH:-./binaries}:/binaries:ro
+ # Persist Ghidra projects between runs
+ - ghydra-projects:/projects
+ environment:
+ - GHYDRA_MODE=${GHYDRA_MODE:-headless}
+ - GHYDRA_PORT=8192
+ - GHYDRA_MAXMEM=${GHYDRA_MAXMEM:-2G}
+ - PROJECT_NAME=${PROJECT_NAME:-GhydraMCP}
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8192/"]
+ interval: 30s
+ timeout: 10s
+ start_period: 60s
+ retries: 3
+ deploy:
+ resources:
+ limits:
+ memory: ${GHYDRA_MAXMEM:-2G}
+ profiles:
+ - prod
+ - default
+
+ # =============================================================================
+ # Development Service - Hot-reload and debugging friendly
+ # =============================================================================
+ ghydramcp-dev:
+ build:
+ context: .
+ dockerfile: docker/Dockerfile
+ args:
+ GHIDRA_VERSION: ${GHIDRA_VERSION:-11.4.2}
+ GHIDRA_DATE: ${GHIDRA_DATE:-20250826}
+ image: ghydramcp:dev
+ container_name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-dev
+ ports:
+ - "${GHYDRA_PORT:-8192}:8192"
+ # Additional ports for debugging/multiple instances
+ - "8193:8193"
+ - "8194:8194"
+ volumes:
+ # Mount binaries (read-write in dev)
+ - ${BINARIES_PATH:-./binaries}:/binaries:rw
+ # Persist projects
+ - ghydra-projects-dev:/projects
+ # Mount scripts for live editing (development only)
+ - ./docker/GhydraMCPServer.java:/opt/ghidra/scripts/GhydraMCPServer.java:ro
+ - ./docker/entrypoint.sh:/entrypoint.sh:ro
+ environment:
+ - GHYDRA_MODE=${GHYDRA_MODE:-headless}
+ - GHYDRA_PORT=8192
+ - GHYDRA_MAXMEM=${GHYDRA_MAXMEM:-4G}
+ - PROJECT_NAME=${PROJECT_NAME:-GhydraMCP-Dev}
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8192/"]
+ interval: 15s
+ timeout: 5s
+ start_period: 120s
+ retries: 5
+ profiles:
+ - dev
+
+ # =============================================================================
+ # Shell Service - Interactive debugging container
+ # =============================================================================
+ ghydramcp-shell:
+ build:
+ context: .
+ dockerfile: docker/Dockerfile
+ image: ghydramcp:${GHYDRAMCP_VERSION:-latest}
+ container_name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-shell
+ stdin_open: true
+ tty: true
+ volumes:
+ - ${BINARIES_PATH:-./binaries}:/binaries:rw
+ - ghydra-projects-dev:/projects
+ environment:
+ - GHYDRA_MODE=shell
+ profiles:
+ - debug
+
+volumes:
+ ghydra-projects:
+ name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-projects
+ ghydra-projects-dev:
+ name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-projects-dev
+
+networks:
+ default:
+ name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-network
diff --git a/src/ghydramcp/__init__.py b/src/ghydramcp/__init__.py
new file mode 100644
index 0000000..372aa62
--- /dev/null
+++ b/src/ghydramcp/__init__.py
@@ -0,0 +1,15 @@
+"""GhydraMCP - AI-assisted reverse engineering bridge for Ghidra.
+
+A multi-instance Ghidra plugin exposed via HATEOAS REST API plus an MCP
+Python bridge for decompilation, analysis & binary manipulation.
+"""
+
+try:
+ from importlib.metadata import version
+ __version__ = version("ghydramcp")
+except Exception:
+ __version__ = "2025.12.1"
+
+from .server import create_server, main
+
+__all__ = ["create_server", "main", "__version__"]
diff --git a/src/ghydramcp/__main__.py b/src/ghydramcp/__main__.py
new file mode 100644
index 0000000..f71f25e
--- /dev/null
+++ b/src/ghydramcp/__main__.py
@@ -0,0 +1,9 @@
+"""GhydraMCP package entry point.
+
+Allows running with: python -m ghydramcp
+"""
+
+from .server import main
+
+if __name__ == "__main__":
+ main()
diff --git a/src/ghydramcp/config.py b/src/ghydramcp/config.py
new file mode 100644
index 0000000..56dcb13
--- /dev/null
+++ b/src/ghydramcp/config.py
@@ -0,0 +1,114 @@
+"""Configuration management for GhydraMCP.
+
+Handles environment variables, default settings, and runtime configuration.
+"""
+
+import os
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Optional
+
+
+@dataclass
+class DockerConfig:
+ """Docker-specific configuration."""
+
+ # Docker image settings
+ image_name: str = "ghydramcp"
+ image_tag: str = field(default_factory=lambda: os.environ.get("GHYDRAMCP_VERSION", "latest"))
+
+ # Default container settings
+ default_port: int = field(default_factory=lambda: int(os.environ.get("GHYDRA_PORT", "8192")))
+ default_memory: str = field(default_factory=lambda: os.environ.get("GHYDRA_MAXMEM", "2G"))
+
+ # Project directory (for building)
+ project_dir: Optional[Path] = None
+
+ # Auto-start settings
+ auto_start_enabled: bool = field(default_factory=lambda: os.environ.get("GHYDRA_DOCKER_AUTO", "false").lower() == "true")
+ auto_start_wait: bool = True
+ auto_start_timeout: float = 300.0
+
+
+# Docker configuration instance
+_docker_config: Optional[DockerConfig] = None
+
+
+def get_docker_config() -> DockerConfig:
+ """Get the Docker configuration instance."""
+ global _docker_config
+ if _docker_config is None:
+ _docker_config = DockerConfig()
+ return _docker_config
+
+
+def set_docker_config(config: DockerConfig) -> None:
+ """Set the Docker configuration instance."""
+ global _docker_config
+ _docker_config = config
+
+
+@dataclass
+class GhydraConfig:
+ """Configuration for GhydraMCP server."""
+
+ # Ghidra connection settings
+ ghidra_host: str = field(default_factory=lambda: os.environ.get("GHIDRA_HOST", "localhost"))
+ default_port: Optional[int] = None
+
+ # Port scanning ranges for instance discovery
+ quick_discovery_range: range = field(default_factory=lambda: range(18489, 18499))
+ full_discovery_range: range = field(default_factory=lambda: range(18400, 18600))
+
+ # HTTP client settings
+ request_timeout: float = 30.0
+ discovery_timeout: float = 0.5
+
+ # Pagination defaults
+ default_page_size: int = 50
+ max_page_size: int = 500
+
+ # Cursor management
+ cursor_ttl_seconds: int = 300 # 5 minutes
+ max_cursors_per_session: int = 100
+
+ # Expected API version
+ expected_api_version: int = 2
+
+ # Resource caps for enumeration endpoints
+ resource_caps: dict = field(default_factory=lambda: {
+ "functions": 1000,
+ "strings": 500,
+ "data": 1000,
+ "structs": 500,
+ "xrefs": 500,
+ })
+
+ def __post_init__(self):
+ """Validate configuration after initialization."""
+ if self.default_page_size > self.max_page_size:
+ self.default_page_size = self.max_page_size
+
+
+# Global configuration instance (can be replaced for testing)
+_config: Optional[GhydraConfig] = None
+
+
+def get_config() -> GhydraConfig:
+ """Get the global configuration instance."""
+ global _config
+ if _config is None:
+ _config = GhydraConfig()
+ return _config
+
+
+def set_config(config: GhydraConfig) -> None:
+ """Set the global configuration instance."""
+ global _config
+ _config = config
+
+
+def reset_config() -> None:
+ """Reset to default configuration."""
+ global _config
+ _config = None
diff --git a/src/ghydramcp/core/__init__.py b/src/ghydramcp/core/__init__.py
new file mode 100644
index 0000000..a19b5f4
--- /dev/null
+++ b/src/ghydramcp/core/__init__.py
@@ -0,0 +1,58 @@
+"""Core infrastructure for GhydraMCP.
+
+Contains HTTP client, pagination, progress reporting, and logging utilities.
+"""
+
+from .http_client import (
+ safe_get,
+ safe_post,
+ safe_put,
+ safe_patch,
+ safe_delete,
+ simplify_response,
+ get_instance_url,
+)
+from .pagination import (
+ CursorManager,
+ CursorState,
+ paginate_response,
+ get_cursor_manager,
+ estimate_tokens,
+)
+from .progress import (
+ ProgressReporter,
+ report_progress,
+ report_step,
+)
+from .logging import (
+ log_info,
+ log_debug,
+ log_warning,
+ log_error,
+)
+
+__all__ = [
+ # HTTP client
+ "safe_get",
+ "safe_post",
+ "safe_put",
+ "safe_patch",
+ "safe_delete",
+ "simplify_response",
+ "get_instance_url",
+ # Pagination
+ "CursorManager",
+ "CursorState",
+ "paginate_response",
+ "get_cursor_manager",
+ "estimate_tokens",
+ # Progress
+ "ProgressReporter",
+ "report_progress",
+ "report_step",
+ # Logging
+ "log_info",
+ "log_debug",
+ "log_warning",
+ "log_error",
+]
diff --git a/src/ghydramcp/core/http_client.py b/src/ghydramcp/core/http_client.py
new file mode 100644
index 0000000..08b1f81
--- /dev/null
+++ b/src/ghydramcp/core/http_client.py
@@ -0,0 +1,392 @@
+"""HTTP client for Ghidra REST API communication.
+
+Provides safe request methods with error handling, HATEOAS compliance,
+and response simplification for AI agent consumption.
+"""
+
+import time
+from typing import Any, Dict, Optional, Union
+from urllib.parse import urlparse
+
+import requests
+
+from ..config import get_config
+
+
+# Allowed origins for CORS-like validation
+ALLOWED_ORIGINS = {
+ "http://localhost",
+ "http://127.0.0.1",
+ "https://localhost",
+ "https://127.0.0.1",
+}
+
+
+def validate_origin(headers: Dict[str, str]) -> bool:
+ """Validate request origin against allowed origins.
+
+ Args:
+ headers: Request headers dict
+
+ Returns:
+ True if origin is allowed or not present
+ """
+ origin = headers.get("Origin")
+ if not origin:
+ # No origin header - allow (browser same-origin policy applies)
+ return True
+
+ try:
+ parsed = urlparse(origin)
+ origin_base = f"{parsed.scheme}://{parsed.hostname}"
+ if parsed.port:
+ origin_base += f":{parsed.port}"
+ except Exception:
+ return False
+
+ return origin_base in ALLOWED_ORIGINS
+
+
+def get_instance_url(port: int, host: Optional[str] = None) -> str:
+ """Get URL for a Ghidra instance by port.
+
+ Args:
+ port: Port number
+ host: Optional host override (defaults to config)
+
+ Returns:
+ Full URL for the Ghidra instance
+ """
+ if host is None:
+ host = get_config().ghidra_host
+ return f"http://{host}:{port}"
+
+
+def _make_request(
+ method: str,
+ port: int,
+ endpoint: str,
+ params: Optional[Dict[str, Any]] = None,
+ json_data: Optional[Dict[str, Any]] = None,
+ data: Optional[str] = None,
+ headers: Optional[Dict[str, str]] = None,
+ host: Optional[str] = None,
+) -> Dict[str, Any]:
+ """Make HTTP request to Ghidra instance with error handling.
+
+ Args:
+ method: HTTP method (GET, POST, PUT, PATCH, DELETE)
+ port: Ghidra instance port
+ endpoint: API endpoint path
+ params: Query parameters
+ json_data: JSON payload for POST/PUT/PATCH
+ data: Raw text payload
+ headers: Additional headers
+ host: Optional host override
+
+ Returns:
+ Response dict with success flag and result or error
+ """
+ config = get_config()
+ url = f"{get_instance_url(port, host)}/{endpoint}"
+
+ # Set up headers for HATEOAS API
+ request_headers = {
+ "Accept": "application/json",
+ "X-Request-ID": f"mcp-bridge-{int(time.time() * 1000)}",
+ }
+
+ if headers:
+ request_headers.update(headers)
+
+ # Validate origin for state-changing requests
+ is_state_changing = method.upper() in ["POST", "PUT", "PATCH", "DELETE"]
+ if is_state_changing:
+ check_headers = (
+ json_data.get("headers", {})
+ if isinstance(json_data, dict)
+ else (headers or {})
+ )
+ if not validate_origin(check_headers):
+ return {
+ "success": False,
+ "error": {
+ "code": "ORIGIN_NOT_ALLOWED",
+ "message": "Origin not allowed for state-changing request",
+ },
+ "status_code": 403,
+ "timestamp": int(time.time() * 1000),
+ }
+ if json_data is not None:
+ request_headers["Content-Type"] = "application/json"
+ elif data is not None:
+ request_headers["Content-Type"] = "text/plain"
+
+ try:
+ response = requests.request(
+ method,
+ url,
+ params=params,
+ json=json_data,
+ data=data,
+ headers=request_headers,
+ timeout=config.request_timeout,
+ )
+
+ try:
+ parsed_json = response.json()
+
+ # Add timestamp if not present
+ if isinstance(parsed_json, dict) and "timestamp" not in parsed_json:
+ parsed_json["timestamp"] = int(time.time() * 1000)
+
+ # Normalize error format
+ if (
+ not response.ok
+ and isinstance(parsed_json, dict)
+ and "success" in parsed_json
+ and not parsed_json["success"]
+ ):
+ if "error" in parsed_json and not isinstance(
+ parsed_json["error"], dict
+ ):
+ error_message = parsed_json["error"]
+ parsed_json["error"] = {
+ "code": f"HTTP_{response.status_code}",
+ "message": error_message,
+ }
+
+ return parsed_json
+
+ except ValueError:
+ if response.ok:
+ return {
+ "success": False,
+ "error": {
+ "code": "NON_JSON_RESPONSE",
+ "message": "Received non-JSON success response",
+ },
+ "status_code": response.status_code,
+ "response_text": response.text[:500],
+ "timestamp": int(time.time() * 1000),
+ }
+ else:
+ return {
+ "success": False,
+ "error": {
+ "code": f"HTTP_{response.status_code}",
+ "message": f"Non-JSON error: {response.text[:100]}...",
+ },
+ "status_code": response.status_code,
+ "response_text": response.text[:500],
+ "timestamp": int(time.time() * 1000),
+ }
+
+ except requests.exceptions.Timeout:
+ return {
+ "success": False,
+ "error": {"code": "REQUEST_TIMEOUT", "message": "Request timed out"},
+ "status_code": 408,
+ "timestamp": int(time.time() * 1000),
+ }
+ except requests.exceptions.ConnectionError:
+ return {
+ "success": False,
+ "error": {
+ "code": "CONNECTION_ERROR",
+ "message": f"Failed to connect to Ghidra instance at {url}",
+ },
+ "status_code": 503,
+ "timestamp": int(time.time() * 1000),
+ }
+ except Exception as e:
+ return {
+ "success": False,
+ "error": {
+ "code": "UNEXPECTED_ERROR",
+ "message": f"Unexpected error: {str(e)}",
+ },
+ "exception": e.__class__.__name__,
+ "timestamp": int(time.time() * 1000),
+ }
+
+
+def safe_get(
+ port: int, endpoint: str, params: Optional[Dict[str, Any]] = None
+) -> Dict[str, Any]:
+ """Make GET request to Ghidra instance.
+
+ Args:
+ port: Ghidra instance port
+ endpoint: API endpoint path
+ params: Query parameters
+
+ Returns:
+ Response dict
+ """
+ return _make_request("GET", port, endpoint, params=params)
+
+
+def safe_post(
+ port: int, endpoint: str, data: Union[Dict[str, Any], str]
+) -> Dict[str, Any]:
+ """Make POST request to Ghidra instance.
+
+ Args:
+ port: Ghidra instance port
+ endpoint: API endpoint path
+ data: JSON dict or raw string payload
+
+ Returns:
+ Response dict
+ """
+ headers = None
+ json_payload = None
+ text_payload = None
+
+ if isinstance(data, dict):
+ headers = data.pop("headers", None)
+ json_payload = data
+ else:
+ text_payload = data
+
+ return _make_request(
+ "POST", port, endpoint, json_data=json_payload, data=text_payload, headers=headers
+ )
+
+
+def safe_put(port: int, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
+ """Make PUT request to Ghidra instance.
+
+ Args:
+ port: Ghidra instance port
+ endpoint: API endpoint path
+ data: JSON payload
+
+ Returns:
+ Response dict
+ """
+ headers = data.pop("headers", None) if isinstance(data, dict) else None
+ return _make_request("PUT", port, endpoint, json_data=data, headers=headers)
+
+
+def safe_patch(port: int, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
+ """Make PATCH request to Ghidra instance.
+
+ Args:
+ port: Ghidra instance port
+ endpoint: API endpoint path
+ data: JSON payload
+
+ Returns:
+ Response dict
+ """
+ headers = data.pop("headers", None) if isinstance(data, dict) else None
+ return _make_request("PATCH", port, endpoint, json_data=data, headers=headers)
+
+
+def safe_delete(port: int, endpoint: str) -> Dict[str, Any]:
+ """Make DELETE request to Ghidra instance.
+
+ Args:
+ port: Ghidra instance port
+ endpoint: API endpoint path
+
+ Returns:
+ Response dict
+ """
+ return _make_request("DELETE", port, endpoint)
+
+
+def simplify_response(response: Dict[str, Any]) -> Dict[str, Any]:
+ """Simplify HATEOAS response for AI agent consumption.
+
+ - Removes _links from result entries
+ - Flattens nested structures
+ - Preserves important metadata
+ - Converts structured data to text
+
+ Args:
+ response: Raw API response
+
+ Returns:
+ Simplified response dict
+ """
+ if not isinstance(response, dict):
+ return response
+
+ result = response.copy()
+
+ # Store API metadata
+ api_metadata = {}
+ for key in ["id", "instance", "timestamp", "size", "offset", "limit"]:
+ if key in result:
+ api_metadata[key] = result.get(key)
+
+ # Simplify result data
+ if "result" in result:
+ if isinstance(result["result"], list):
+ simplified_items = []
+ for item in result["result"]:
+ if isinstance(item, dict):
+ item_copy = item.copy()
+ links = item_copy.pop("_links", None)
+ if isinstance(links, dict):
+ for link_name, link_data in links.items():
+ if isinstance(link_data, dict) and "href" in link_data:
+ item_copy[f"{link_name}_url"] = link_data["href"]
+ simplified_items.append(item_copy)
+ else:
+ simplified_items.append(item)
+ result["result"] = simplified_items
+
+ elif isinstance(result["result"], dict):
+ result_copy = result["result"].copy()
+ links = result_copy.pop("_links", None)
+
+ if isinstance(links, dict):
+ for link_name, link_data in links.items():
+ if isinstance(link_data, dict) and "href" in link_data:
+ result_copy[f"{link_name}_url"] = link_data["href"]
+
+ # Convert disassembly to text
+ if "instructions" in result_copy and isinstance(
+ result_copy["instructions"], list
+ ):
+ disasm_text = ""
+ for instr in result_copy["instructions"]:
+ if isinstance(instr, dict):
+ addr = instr.get("address", "")
+ mnemonic = instr.get("mnemonic", "")
+ operands = instr.get("operands", "")
+ bytes_str = instr.get("bytes", "")
+ disasm_text += (
+ f"{addr}: {bytes_str.ljust(10)} {mnemonic} {operands}\n"
+ )
+ result_copy["disassembly_text"] = disasm_text
+ result_copy.pop("instructions", None)
+
+ # Make decompiled code accessible
+ if "ccode" in result_copy:
+ result_copy["decompiled_text"] = result_copy["ccode"]
+ elif "decompiled" in result_copy:
+ result_copy["decompiled_text"] = result_copy["decompiled"]
+
+ result["result"] = result_copy
+
+ # Simplify top-level links
+ links = result.pop("_links", None)
+ if isinstance(links, dict):
+ api_links = {}
+ for link_name, link_data in links.items():
+ if isinstance(link_data, dict) and "href" in link_data:
+ api_links[link_name] = link_data["href"]
+ if api_links:
+ result["api_links"] = api_links
+
+ # Restore metadata
+ for key, value in api_metadata.items():
+ if key not in result:
+ result[key] = value
+
+ return result
diff --git a/src/ghydramcp/core/logging.py b/src/ghydramcp/core/logging.py
new file mode 100644
index 0000000..b595472
--- /dev/null
+++ b/src/ghydramcp/core/logging.py
@@ -0,0 +1,88 @@
+"""Logging utilities for MCP context-aware logging.
+
+Provides async logging functions that use FastMCP's Context for
+client-visible logging when available, with fallback to standard logging.
+"""
+
+import logging
+from typing import Optional, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from mcp.server.fastmcp import Context
+
+# Standard Python logger as fallback
+logger = logging.getLogger("ghydramcp")
+
+
+async def log_debug(ctx: Optional["Context"], message: str) -> None:
+ """Log a debug message to the MCP client and/or standard logger.
+
+ Args:
+ ctx: FastMCP context (may be None)
+ message: Debug message to log
+ """
+ logger.debug(message)
+ if ctx is not None:
+ try:
+ await ctx.debug(message)
+ except Exception:
+ pass # Silently ignore if context doesn't support logging
+
+
+async def log_info(ctx: Optional["Context"], message: str) -> None:
+ """Log an info message to the MCP client and/or standard logger.
+
+ Args:
+ ctx: FastMCP context (may be None)
+ message: Info message to log
+ """
+ logger.info(message)
+ if ctx is not None:
+ try:
+ await ctx.info(message)
+ except Exception:
+ pass
+
+
+async def log_warning(ctx: Optional["Context"], message: str) -> None:
+ """Log a warning message to the MCP client and/or standard logger.
+
+ Args:
+ ctx: FastMCP context (may be None)
+ message: Warning message to log
+ """
+ logger.warning(message)
+ if ctx is not None:
+ try:
+ await ctx.warning(message)
+ except Exception:
+ pass
+
+
+async def log_error(ctx: Optional["Context"], message: str) -> None:
+ """Log an error message to the MCP client and/or standard logger.
+
+ Args:
+ ctx: FastMCP context (may be None)
+ message: Error message to log
+ """
+ logger.error(message)
+ if ctx is not None:
+ try:
+ await ctx.error(message)
+ except Exception:
+ pass
+
+
+def configure_logging(level: int = logging.INFO) -> None:
+ """Configure the standard logger for GhydraMCP.
+
+ Args:
+ level: Logging level (default: INFO)
+ """
+ handler = logging.StreamHandler()
+ handler.setFormatter(
+ logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+ )
+ logger.addHandler(handler)
+ logger.setLevel(level)
diff --git a/src/ghydramcp/core/pagination.py b/src/ghydramcp/core/pagination.py
new file mode 100644
index 0000000..e888667
--- /dev/null
+++ b/src/ghydramcp/core/pagination.py
@@ -0,0 +1,511 @@
+"""Cursor-based pagination system for large MCP responses.
+
+Provides efficient pagination with grep filtering, session isolation,
+and TTL-based cursor expiration.
+"""
+
+import hashlib
+import json
+import re
+import time
+from collections import OrderedDict
+from dataclasses import dataclass, field
+from threading import Lock
+from typing import Any, Dict, List, Optional, Tuple
+
+from ..config import get_config
+
+
+# ReDoS Protection Configuration
+MAX_GREP_PATTERN_LENGTH = 500
+MAX_GREP_REPETITION_OPS = 15
+MAX_GREP_RECURSION_DEPTH = 10
+
+# Token estimation (roughly 4 chars per token)
+TOKEN_ESTIMATION_RATIO = 4.0
+
+
+def compile_safe_pattern(pattern: str, flags: int = 0) -> re.Pattern:
+ """Compile regex pattern with ReDoS protection.
+
+ Validates pattern to prevent catastrophic backtracking attacks.
+
+ Args:
+ pattern: Regex pattern string
+ flags: Regex compilation flags
+
+ Returns:
+ Compiled regex pattern
+
+ Raises:
+ ValueError: If pattern fails safety validation
+ """
+ if not pattern:
+ raise ValueError("Empty pattern")
+
+ if len(pattern) > MAX_GREP_PATTERN_LENGTH:
+ raise ValueError(
+ f"Pattern too long ({len(pattern)} chars, max {MAX_GREP_PATTERN_LENGTH}). "
+ "Consider using a simpler pattern."
+ )
+
+ # Count repetition operators
+ repetition_ops = pattern.count("*") + pattern.count("+") + pattern.count("?")
+ repetition_ops += len(re.findall(r"\{[0-9,]+\}", pattern))
+
+ if repetition_ops > MAX_GREP_REPETITION_OPS:
+ raise ValueError(
+ f"Pattern has too many repetition operators ({repetition_ops}, "
+ f"max {MAX_GREP_REPETITION_OPS}). Consider simplifying."
+ )
+
+ # Check for dangerous nested quantifiers
+ dangerous_patterns = [
+ r"\([^)]*[*+][^)]*\)[*+]", # (a+)+ or (a*)*
+ r"\([^)]*[*+][^)]*\)\{", # (a+){n,m}
+ ]
+ for dangerous in dangerous_patterns:
+ if re.search(dangerous, pattern):
+ raise ValueError(
+ "Pattern contains nested quantifiers which could cause "
+ "exponential backtracking. Consider simplifying."
+ )
+
+ try:
+ return re.compile(pattern, flags)
+ except re.error as e:
+ raise ValueError(f"Invalid regex pattern: {e}")
+
+
+@dataclass
+class CursorState:
+ """Represents the state of a paginated query with session isolation."""
+
+ cursor_id: str
+ session_id: str
+ tool_name: str
+ query_hash: str
+ data: List[Any]
+ total_count: int
+ filtered_count: int
+ current_offset: int = 0
+ page_size: int = 50
+ grep_pattern: Optional[str] = None
+ grep_flags: int = 0
+ created_at: float = field(default_factory=time.time)
+ last_accessed: float = field(default_factory=time.time)
+
+ @property
+ def is_expired(self) -> bool:
+ config = get_config()
+ return time.time() - self.last_accessed > config.cursor_ttl_seconds
+
+ @property
+ def has_more(self) -> bool:
+ return self.current_offset + self.page_size < self.filtered_count
+
+ @property
+ def current_page(self) -> int:
+ return (self.current_offset // self.page_size) + 1
+
+ @property
+ def total_pages(self) -> int:
+ return max(1, (self.filtered_count + self.page_size - 1) // self.page_size)
+
+ @property
+ def ttl_remaining(self) -> int:
+ config = get_config()
+ return max(0, int(config.cursor_ttl_seconds - (time.time() - self.last_accessed)))
+
+ def verify_session(self, session_id: str) -> bool:
+ """Verify cursor belongs to requesting session."""
+ return self.session_id == session_id
+
+
+class CursorManager:
+ """Thread-safe cursor manager with TTL-based expiration and session isolation."""
+
+ def __init__(self):
+ self._cursors: OrderedDict[str, CursorState] = OrderedDict()
+ self._session_cursors: Dict[str, set] = {}
+ self._lock = Lock()
+
+ def _generate_cursor_id(self, query_hash: str, session_id: str) -> str:
+ """Generate a unique cursor ID."""
+ unique = f"{session_id}-{query_hash}-{time.time()}-{id(self)}"
+ return hashlib.sha256(unique.encode()).hexdigest()[:16]
+
+ def _cleanup_expired(self) -> None:
+ """Remove expired cursors (call while holding lock)."""
+ config = get_config()
+
+ expired = [cid for cid, state in self._cursors.items() if state.is_expired]
+ for cid in expired:
+ state = self._cursors[cid]
+ if state.session_id in self._session_cursors:
+ self._session_cursors[state.session_id].discard(cid)
+ del self._cursors[cid]
+
+ # LRU eviction
+ while len(self._cursors) > config.max_cursors_per_session:
+ oldest_id, oldest_state = self._cursors.popitem(last=False)
+ if oldest_state.session_id in self._session_cursors:
+ self._session_cursors[oldest_state.session_id].discard(oldest_id)
+
+ def create_cursor(
+ self,
+ data: List[Any],
+ query_params: Dict[str, Any],
+ tool_name: str = "unknown",
+ session_id: str = "default",
+ grep_pattern: Optional[str] = None,
+ grep_flags: int = 0,
+ page_size: int = 50,
+ ) -> Tuple[str, CursorState]:
+ """Create a new cursor for paginated results.
+
+ Args:
+ data: The full result set to paginate
+ query_params: Original query parameters (for hashing)
+ tool_name: Name of tool creating cursor
+ session_id: Session identifier for isolation
+ grep_pattern: Optional regex pattern to filter results
+ grep_flags: Regex flags
+ page_size: Items per page
+
+ Returns:
+ Tuple of (cursor_id, cursor_state)
+ """
+ config = get_config()
+
+ # Apply grep filtering
+ filtered_data = data
+ if grep_pattern:
+ pattern = compile_safe_pattern(grep_pattern, grep_flags)
+ filtered_data = [
+ item for item in data if self._matches_grep(item, pattern)
+ ]
+
+ # Create query hash
+ query_hash = hashlib.md5(
+ json.dumps(query_params, sort_keys=True, default=str).encode()
+ ).hexdigest()[:12]
+
+ with self._lock:
+ self._cleanup_expired()
+
+ cursor_id = self._generate_cursor_id(query_hash, session_id)
+ state = CursorState(
+ cursor_id=cursor_id,
+ session_id=session_id,
+ tool_name=tool_name,
+ query_hash=query_hash,
+ data=filtered_data,
+ total_count=len(data),
+ filtered_count=len(filtered_data),
+ page_size=min(page_size, config.max_page_size),
+ grep_pattern=grep_pattern,
+ grep_flags=grep_flags,
+ )
+ self._cursors[cursor_id] = state
+
+ if session_id not in self._session_cursors:
+ self._session_cursors[session_id] = set()
+ self._session_cursors[session_id].add(cursor_id)
+
+ return cursor_id, state
+
+ def get_cursor(
+ self, cursor_id: str, session_id: Optional[str] = None
+ ) -> Optional[CursorState]:
+ """Retrieve a cursor by ID, optionally validating session."""
+ with self._lock:
+ self._cleanup_expired()
+
+ if cursor_id not in self._cursors:
+ return None
+
+ state = self._cursors[cursor_id]
+ if state.is_expired:
+ del self._cursors[cursor_id]
+ if state.session_id in self._session_cursors:
+ self._session_cursors[state.session_id].discard(cursor_id)
+ return None
+
+ if session_id and not state.verify_session(session_id):
+ return None
+
+ state.last_accessed = time.time()
+ self._cursors.move_to_end(cursor_id)
+ return state
+
+ def advance_cursor(
+ self, cursor_id: str, session_id: Optional[str] = None
+ ) -> Optional[CursorState]:
+ """Advance cursor to next page."""
+ with self._lock:
+ state = self._cursors.get(cursor_id)
+ if not state or state.is_expired:
+ return None
+
+ if session_id and not state.verify_session(session_id):
+ return None
+
+ state.current_offset += state.page_size
+ state.last_accessed = time.time()
+ self._cursors.move_to_end(cursor_id)
+ return state
+
+ def delete_cursor(
+ self, cursor_id: str, session_id: Optional[str] = None
+ ) -> bool:
+ """Explicitly delete a cursor."""
+ with self._lock:
+ if cursor_id not in self._cursors:
+ return False
+
+ state = self._cursors[cursor_id]
+ if session_id and not state.verify_session(session_id):
+ return False
+
+ if state.session_id in self._session_cursors:
+ self._session_cursors[state.session_id].discard(cursor_id)
+ del self._cursors[cursor_id]
+ return True
+
+ def delete_session_cursors(self, session_id: str) -> int:
+ """Delete all cursors for a session."""
+ with self._lock:
+ if session_id not in self._session_cursors:
+ return 0
+
+ cursor_ids = list(self._session_cursors[session_id])
+ count = 0
+ for cid in cursor_ids:
+ if cid in self._cursors:
+ del self._cursors[cid]
+ count += 1
+ del self._session_cursors[session_id]
+ return count
+
+ def get_page(self, state: CursorState) -> List[Any]:
+ """Get current page of data from cursor state."""
+ start = state.current_offset
+ end = start + state.page_size
+ return state.data[start:end]
+
+ def _matches_grep(
+ self, item: Any, pattern: re.Pattern, depth: int = 0
+ ) -> bool:
+ """Check if an item matches the grep pattern.
+
+ Searches through string representations of dict values,
+ list items, or the item itself.
+ """
+ if depth > MAX_GREP_RECURSION_DEPTH:
+ return False
+
+ if isinstance(item, dict):
+ for value in item.values():
+ if isinstance(value, str) and pattern.search(value):
+ return True
+ elif isinstance(value, (int, float)):
+ if pattern.search(str(value)):
+ return True
+ elif isinstance(value, dict):
+ if self._matches_grep(value, pattern, depth + 1):
+ return True
+ elif isinstance(value, (list, tuple)):
+ if self._matches_grep(value, pattern, depth + 1):
+ return True
+ return False
+ elif isinstance(item, (list, tuple)):
+ return any(self._matches_grep(i, pattern, depth + 1) for i in item)
+ elif isinstance(item, str):
+ return bool(pattern.search(item))
+ else:
+ return bool(pattern.search(str(item)))
+
+ def list_cursors(self, session_id: Optional[str] = None) -> List[Dict[str, Any]]:
+ """List active cursors, optionally filtered by session."""
+ with self._lock:
+ self._cleanup_expired()
+ return [
+ {
+ "cursor_id": cid,
+ "session_id": state.session_id,
+ "tool_name": state.tool_name,
+ "total_count": state.total_count,
+ "filtered_count": state.filtered_count,
+ "current_page": state.current_page,
+ "total_pages": state.total_pages,
+ "current_offset": state.current_offset,
+ "page_size": state.page_size,
+ "has_more": state.has_more,
+ "grep_pattern": state.grep_pattern,
+ "age_seconds": int(time.time() - state.created_at),
+ "ttl_remaining": state.ttl_remaining,
+ }
+ for cid, state in self._cursors.items()
+ if session_id is None or state.session_id == session_id
+ ]
+
+ def get_stats(self) -> Dict[str, Any]:
+ """Get cursor manager statistics."""
+ config = get_config()
+ with self._lock:
+ self._cleanup_expired()
+ return {
+ "total_cursors": len(self._cursors),
+ "total_sessions": len(self._session_cursors),
+ "max_cache_size": config.max_cursors_per_session,
+ "ttl_seconds": config.cursor_ttl_seconds,
+ "cursors_per_session": {
+ sid: len(cids) for sid, cids in self._session_cursors.items()
+ },
+ }
+
+
+# Global cursor manager instance
+_cursor_manager: Optional[CursorManager] = None
+
+
+def get_cursor_manager() -> CursorManager:
+ """Get the global cursor manager instance."""
+ global _cursor_manager
+ if _cursor_manager is None:
+ _cursor_manager = CursorManager()
+ return _cursor_manager
+
+
+def estimate_tokens(data: List[Any]) -> int:
+ """Estimate token count for a list of items."""
+ text = json.dumps(data, default=str)
+ return int(len(text) / TOKEN_ESTIMATION_RATIO)
+
+
+def paginate_response(
+ data: List[Any],
+ query_params: Dict[str, Any],
+ tool_name: str = "unknown",
+ session_id: str = "default",
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+) -> Dict[str, Any]:
+ """Create a paginated response with optional grep filtering.
+
+ Args:
+ data: Full result list to paginate
+ query_params: Original query parameters (for cursor creation)
+ tool_name: Name of the tool creating this response
+ session_id: Session identifier for cursor isolation
+ page_size: Items per page (default: 50, max: 500)
+ grep: Optional regex pattern to filter results
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Bypass pagination and return all results (with warning)
+
+ Returns:
+ dict with pagination metadata and results
+ """
+ config = get_config()
+ cursor_manager = get_cursor_manager()
+ grep_flags = re.IGNORECASE if grep_ignorecase else 0
+
+ # Handle return_all bypass
+ if return_all:
+ filtered_data = data
+ if grep:
+ try:
+ pattern = compile_safe_pattern(grep, grep_flags)
+ filtered_data = [
+ item
+ for item in data
+ if cursor_manager._matches_grep(item, pattern)
+ ]
+ except ValueError as e:
+ return {
+ "success": False,
+ "error": {"code": "INVALID_GREP_PATTERN", "message": str(e)},
+ "timestamp": int(time.time() * 1000),
+ }
+
+ estimated_tokens = estimate_tokens(filtered_data)
+ warning = None
+
+ if estimated_tokens > 50000:
+ warning = f"EXTREMELY LARGE response (~{estimated_tokens:,} tokens)"
+ elif estimated_tokens > 20000:
+ warning = f"VERY LARGE response (~{estimated_tokens:,} tokens)"
+ elif estimated_tokens > 8000:
+ warning = f"Large response (~{estimated_tokens:,} tokens)"
+
+ return {
+ "success": True,
+ "result": filtered_data,
+ "pagination": {
+ "bypassed": True,
+ "total_count": len(data),
+ "filtered_count": len(filtered_data),
+ "grep_pattern": grep,
+ "estimated_tokens": estimated_tokens,
+ "warning": warning,
+ },
+ "timestamp": int(time.time() * 1000),
+ }
+
+ # Normal pagination flow
+ try:
+ cursor_id, state = cursor_manager.create_cursor(
+ data=data,
+ query_params=query_params,
+ tool_name=tool_name,
+ session_id=session_id,
+ grep_pattern=grep,
+ grep_flags=grep_flags,
+ page_size=min(page_size, config.max_page_size),
+ )
+ except ValueError as e:
+ return {
+ "success": False,
+ "error": {"code": "INVALID_GREP_PATTERN", "message": str(e)},
+ "timestamp": int(time.time() * 1000),
+ }
+
+ current_page = cursor_manager.get_page(state)
+ response_cursor = cursor_id if state.has_more else None
+
+ response = {
+ "success": True,
+ "result": current_page,
+ "pagination": {
+ "cursor_id": response_cursor,
+ "session_id": session_id,
+ "total_count": state.total_count,
+ "filtered_count": state.filtered_count,
+ "page_size": state.page_size,
+ "current_page": state.current_page,
+ "total_pages": state.total_pages,
+ "has_more": state.has_more,
+ "grep_pattern": grep,
+ "items_returned": len(current_page),
+ },
+ "timestamp": int(time.time() * 1000),
+ }
+
+ # Add LLM-friendly continuation message
+ if state.has_more:
+ remaining = state.filtered_count - (state.current_page * state.page_size)
+ response["_message"] = (
+ f"Showing {len(current_page)} of {state.filtered_count} items "
+ f"(page {state.current_page}/{state.total_pages}). "
+ f"To get the next {min(state.page_size, remaining)} items, call: "
+ f"cursor_next(cursor_id='{cursor_id}')"
+ )
+ else:
+ response["_message"] = (
+ f"Complete: {len(current_page)} items returned (all results)"
+ )
+
+ return response
diff --git a/src/ghydramcp/core/progress.py b/src/ghydramcp/core/progress.py
new file mode 100644
index 0000000..5a63cd8
--- /dev/null
+++ b/src/ghydramcp/core/progress.py
@@ -0,0 +1,161 @@
+"""Progress reporting utilities for long-running operations.
+
+Provides async progress reporting using FastMCP's Context for
+real-time progress notifications to MCP clients.
+"""
+
+from typing import Optional, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from mcp.server.fastmcp import Context
+
+
+class ProgressReporter:
+ """Helper class for reporting progress during long operations.
+
+ Provides throttled progress updates to avoid spamming the client
+ with too many notifications.
+
+ Example:
+ async def long_operation(ctx: Context):
+ progress = ProgressReporter(ctx, "Scanning", total=100)
+ for i in range(100):
+ await progress.update(message=f"Processing item {i}")
+ await progress.complete("Scan finished")
+ """
+
+ def __init__(
+ self,
+ ctx: Optional["Context"],
+ operation: str,
+ total: int = 100
+ ):
+ """Initialize the progress reporter.
+
+ Args:
+ ctx: FastMCP context for progress reporting (may be None)
+ operation: Name of the operation (used in log messages)
+ total: Total number of steps (default: 100)
+ """
+ self.ctx = ctx
+ self.operation = operation
+ self.total = total
+ self.current = 0
+ self._last_reported = 0
+ # Report every 5% at minimum
+ self._report_threshold = max(1, total // 20)
+
+ async def update(
+ self,
+ progress: Optional[int] = None,
+ message: Optional[str] = None
+ ) -> None:
+ """Update progress, reporting to client if threshold reached.
+
+ Args:
+ progress: Current progress value (if None, increments by 1)
+ message: Optional message to log with the progress update
+ """
+ if progress is not None:
+ self.current = progress
+ else:
+ self.current += 1
+
+ # Only report if we've crossed a threshold or reached the end
+ should_report = (
+ self.current - self._last_reported >= self._report_threshold
+ or self.current >= self.total
+ )
+
+ if self.ctx and should_report:
+ try:
+ await self.ctx.report_progress(
+ progress=self.current,
+ total=self.total
+ )
+ if message:
+ await self.ctx.info(f"{self.operation}: {message}")
+ self._last_reported = self.current
+ except Exception:
+ pass # Silently ignore if context doesn't support progress
+
+ async def info(self, message: str) -> None:
+ """Send an info message to the client.
+
+ Args:
+ message: Message to send
+ """
+ if self.ctx:
+ try:
+ await self.ctx.info(f"{self.operation}: {message}")
+ except Exception:
+ pass
+
+ async def complete(self, message: Optional[str] = None) -> None:
+ """Mark operation as complete.
+
+ Args:
+ message: Optional completion message (supports format placeholders:
+ {count}, {total}, {operation})
+ """
+ self.current = self.total
+ if self.ctx:
+ try:
+ await self.ctx.report_progress(
+ progress=self.total,
+ total=self.total
+ )
+ if message:
+ formatted = message.format(
+ count=self.current,
+ total=self.total,
+ operation=self.operation
+ )
+ await self.ctx.info(formatted)
+ except Exception:
+ pass
+
+
+async def report_progress(
+ ctx: Optional["Context"],
+ progress: int,
+ total: int,
+ message: Optional[str] = None
+) -> None:
+ """Convenience function for one-off progress updates.
+
+ Args:
+ ctx: FastMCP context (may be None)
+ progress: Current progress value
+ total: Total progress value
+ message: Optional message to log
+ """
+ if ctx:
+ try:
+ await ctx.report_progress(progress=progress, total=total)
+ if message:
+ await ctx.info(message)
+ except Exception:
+ pass
+
+
+async def report_step(
+ ctx: Optional["Context"],
+ step: int,
+ total_steps: int,
+ description: str
+) -> None:
+ """Report a discrete step in a multi-step operation.
+
+ Args:
+ ctx: FastMCP context (may be None)
+ step: Current step number (1-indexed)
+ total_steps: Total number of steps
+ description: Description of the current step
+ """
+ if ctx:
+ try:
+ await ctx.report_progress(progress=step, total=total_steps)
+ await ctx.info(f"Step {step}/{total_steps}: {description}")
+ except Exception:
+ pass
diff --git a/src/ghydramcp/mixins/__init__.py b/src/ghydramcp/mixins/__init__.py
new file mode 100644
index 0000000..919112b
--- /dev/null
+++ b/src/ghydramcp/mixins/__init__.py
@@ -0,0 +1,29 @@
+"""MCP Mixins for GhydraMCP.
+
+Domain-specific mixins that organize tools, resources, and prompts by functionality.
+Uses FastMCP's contrib.mcp_mixin pattern for clean modular organization.
+"""
+
+from .base import GhydraMixinBase
+from .instances import InstancesMixin
+from .functions import FunctionsMixin
+from .data import DataMixin
+from .structs import StructsMixin
+from .analysis import AnalysisMixin
+from .memory import MemoryMixin
+from .xrefs import XrefsMixin
+from .cursors import CursorsMixin
+from .docker import DockerMixin
+
+__all__ = [
+ "GhydraMixinBase",
+ "InstancesMixin",
+ "FunctionsMixin",
+ "DataMixin",
+ "StructsMixin",
+ "AnalysisMixin",
+ "MemoryMixin",
+ "XrefsMixin",
+ "CursorsMixin",
+ "DockerMixin",
+]
diff --git a/src/ghydramcp/mixins/analysis.py b/src/ghydramcp/mixins/analysis.py
new file mode 100644
index 0000000..86126b4
--- /dev/null
+++ b/src/ghydramcp/mixins/analysis.py
@@ -0,0 +1,356 @@
+"""Analysis mixin for GhydraMCP.
+
+Provides tools for program analysis operations.
+"""
+
+from typing import Any, Dict, Optional
+
+from fastmcp import Context
+from fastmcp.contrib.mcp_mixin import mcp_tool
+
+from .base import GhydraMixinBase
+from ..config import get_config
+
+
+class AnalysisMixin(GhydraMixinBase):
+ """Mixin for analysis operations.
+
+ Provides tools for:
+ - Running program analysis
+ - Call graph analysis
+ - Data flow analysis
+ - UI state queries
+ - Comment management
+ """
+
+ @mcp_tool()
+ def analysis_run(
+ self,
+ port: Optional[int] = None,
+ analysis_options: Optional[Dict[str, Any]] = None,
+ ) -> Dict[str, Any]:
+ """Run analysis on the current program.
+
+ Args:
+ port: Ghidra instance port (optional)
+ analysis_options: Analysis options to enable/disable
+
+ Returns:
+ Analysis operation result
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ response = self.safe_post(port, "analysis", analysis_options or {})
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def analysis_get_callgraph(
+ self,
+ name: Optional[str] = None,
+ address: Optional[str] = None,
+ max_depth: int = 3,
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Get function call graph with edge pagination.
+
+ Args:
+ name: Starting function name (mutually exclusive with address)
+ address: Starting function address
+ max_depth: Maximum call depth (default: 3)
+ port: Ghidra instance port (optional)
+ page_size: Edges per page (default: 50, max: 500)
+ grep: Regex pattern to filter edges
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all edges without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Call graph with paginated edges
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+
+ params = {"max_depth": max_depth}
+ if address:
+ params["address"] = address
+ func_id = address
+ elif name:
+ params["name"] = name
+ func_id = name
+ else:
+ func_id = "entry_point"
+
+ response = self.safe_get(port, "analysis/callgraph", params)
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ result = simplified.get("result", {})
+ edges = result.get("edges", []) if isinstance(result, dict) else []
+ nodes = result.get("nodes", []) if isinstance(result, dict) else []
+
+ if not edges:
+ return simplified
+
+ query_params = {
+ "tool": "analysis_get_callgraph",
+ "port": port,
+ "name": name,
+ "address": address,
+ "max_depth": max_depth,
+ "grep": grep,
+ }
+ session_id = self._get_session_id(ctx)
+
+ paginated = self.paginate_response(
+ data=edges,
+ query_params=query_params,
+ tool_name="analysis_get_callgraph",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ if paginated.get("success"):
+ paginated["result"] = {
+ "root_function": func_id,
+ "max_depth": max_depth,
+ "nodes": nodes,
+ "edges": paginated.get("result", []),
+ "total_nodes": len(nodes),
+ }
+
+ return paginated
+
+ @mcp_tool()
+ def analysis_get_dataflow(
+ self,
+ address: str,
+ direction: str = "forward",
+ max_steps: int = 50,
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Perform data flow analysis with step pagination.
+
+ Args:
+ address: Starting address in hex format
+ direction: "forward" or "backward" (default: "forward")
+ max_steps: Maximum analysis steps (default: 50)
+ port: Ghidra instance port (optional)
+ page_size: Steps per page (default: 50, max: 500)
+ grep: Regex pattern to filter steps
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all steps without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Data flow steps with pagination
+ """
+ if not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+
+ params = {
+ "address": address,
+ "direction": direction,
+ "max_steps": max_steps,
+ }
+
+ response = self.safe_get(port, "analysis/dataflow", params)
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ result = simplified.get("result", {})
+ steps = result.get("steps", []) if isinstance(result, dict) else []
+
+ if not steps:
+ return simplified
+
+ query_params = {
+ "tool": "analysis_get_dataflow",
+ "port": port,
+ "address": address,
+ "direction": direction,
+ "max_steps": max_steps,
+ "grep": grep,
+ }
+ session_id = self._get_session_id(ctx)
+
+ paginated = self.paginate_response(
+ data=steps,
+ query_params=query_params,
+ tool_name="analysis_get_dataflow",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ if paginated.get("success"):
+ paginated["result"] = {
+ "start_address": address,
+ "direction": direction,
+ "steps": paginated.get("result", []),
+ }
+ if isinstance(result, dict):
+ for key in ["sources", "sinks", "total_steps"]:
+ if key in result:
+ paginated["result"][key] = result[key]
+
+ return paginated
+
+ @mcp_tool()
+ def ui_get_current_address(self, port: Optional[int] = None) -> Dict[str, Any]:
+ """Get the address currently selected in Ghidra's UI.
+
+ Args:
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Current address information
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ response = self.safe_get(port, "address")
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def ui_get_current_function(self, port: Optional[int] = None) -> Dict[str, Any]:
+ """Get the function currently selected in Ghidra's UI.
+
+ Args:
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Current function information
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ response = self.safe_get(port, "function")
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def comments_set(
+ self,
+ address: str,
+ comment: str = "",
+ comment_type: str = "plate",
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Set a comment at the specified address.
+
+ Args:
+ address: Memory address in hex format
+ comment: Comment text (empty string removes comment)
+ comment_type: "plate", "pre", "post", "eol", "repeatable"
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {"comment": comment}
+ response = self.safe_post(port, f"memory/{address}/comments/{comment_type}", payload)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def functions_set_comment(
+ self,
+ address: str,
+ comment: str = "",
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Set a decompiler-friendly comment (function comment with fallback).
+
+ Args:
+ address: Memory address (preferably function entry point)
+ comment: Comment text (empty string removes comment)
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ # Try setting as function comment first
+ try:
+ payload = {"comment": comment}
+ response = self.safe_patch(port, f"functions/{address}", payload)
+ if response.get("success", False):
+ return self.simplify_response(response)
+ except Exception:
+ pass
+
+ # Fallback to pre-comment
+ return self.comments_set(
+ address=address,
+ comment=comment,
+ comment_type="pre",
+ port=port,
+ )
diff --git a/src/ghydramcp/mixins/base.py b/src/ghydramcp/mixins/base.py
new file mode 100644
index 0000000..367c8e6
--- /dev/null
+++ b/src/ghydramcp/mixins/base.py
@@ -0,0 +1,240 @@
+"""Base mixin class for GhydraMCP domain mixins.
+
+Provides shared state and utilities for all domain mixins.
+"""
+
+import time
+from threading import Lock
+from typing import Any, Dict, Optional
+
+from fastmcp import Context
+from fastmcp.contrib.mcp_mixin import MCPMixin
+
+from ..config import get_config
+from ..core.http_client import safe_get, safe_post, safe_put, safe_patch, safe_delete, simplify_response
+from ..core.pagination import get_cursor_manager, paginate_response
+from ..core.logging import log_info, log_debug, log_warning, log_error
+
+
+class GhydraMixinBase(MCPMixin):
+ """Base class for GhydraMCP domain mixins.
+
+ Provides shared instance state and common utilities.
+ All domain mixins should inherit from this class.
+ """
+
+ # Shared state across all mixins
+ _instances: Dict[int, Dict[str, Any]] = {}
+ _instances_lock = Lock()
+ _current_port: Optional[int] = None
+
+ def __init__(self):
+ """Initialize the mixin with shared state."""
+ pass
+
+ @classmethod
+ def get_current_port(cls) -> Optional[int]:
+ """Get the current working instance port."""
+ return cls._current_port
+
+ @classmethod
+ def set_current_port(cls, port: int) -> None:
+ """Set the current working instance port."""
+ cls._current_port = port
+
+ @classmethod
+ def get_instance_port(cls, port: Optional[int] = None) -> int:
+ """Get instance port, using current if not specified.
+
+ Args:
+ port: Explicit port (optional)
+
+ Returns:
+ Port number to use
+
+ Raises:
+ ValueError: If no port specified and no current instance set
+ """
+ if port is not None:
+ return port
+ if cls._current_port is not None:
+ return cls._current_port
+ config = get_config()
+ # Try default port
+ default_port = config.quick_discovery_range.start
+ if default_port in cls._instances:
+ return default_port
+ raise ValueError(
+ "No Ghidra instance specified. Use instances_use(port) to set a working instance, "
+ "or pass port= parameter explicitly."
+ )
+
+ @classmethod
+ def register_instance(cls, port: int, url: Optional[str] = None) -> str:
+ """Register a Ghidra instance.
+
+ Args:
+ port: Port number
+ url: Optional URL override
+
+ Returns:
+ Status message
+ """
+ config = get_config()
+ if url is None:
+ url = f"http://{config.ghidra_host}:{port}"
+
+ # Verify instance is responsive
+ try:
+ response = safe_get(port, "")
+ if not response.get("success", False):
+ return f"Failed to connect to Ghidra instance on port {port}"
+
+ # Check API version
+ api_version = response.get("api_version", 0)
+ if api_version < config.expected_api_version:
+ return (
+ f"API version mismatch: got {api_version}, "
+ f"expected {config.expected_api_version}"
+ )
+
+ with cls._instances_lock:
+ cls._instances[port] = {
+ "url": url,
+ "project": response.get("project", ""),
+ "file": response.get("file", ""),
+ "registered_at": time.time(),
+ }
+
+ return f"Registered Ghidra instance on port {port}"
+
+ except Exception as e:
+ return f"Error registering instance: {e}"
+
+ @classmethod
+ def unregister_instance(cls, port: int) -> str:
+ """Unregister a Ghidra instance.
+
+ Args:
+ port: Port number
+
+ Returns:
+ Status message
+ """
+ with cls._instances_lock:
+ if port in cls._instances:
+ del cls._instances[port]
+ if cls._current_port == port:
+ cls._current_port = None
+ return f"Unregistered Ghidra instance on port {port}"
+ return f"No instance registered on port {port}"
+
+ @classmethod
+ def list_instances(cls) -> Dict[int, Dict[str, Any]]:
+ """Get all registered instances.
+
+ Returns:
+ Dict mapping port to instance info
+ """
+ with cls._instances_lock:
+ return dict(cls._instances)
+
+ @classmethod
+ def get_instance_info(cls, port: int) -> Optional[Dict[str, Any]]:
+ """Get info for a specific instance.
+
+ Args:
+ port: Port number
+
+ Returns:
+ Instance info dict or None
+ """
+ with cls._instances_lock:
+ return cls._instances.get(port)
+
+ def _get_session_id(self, ctx: Optional[Context]) -> str:
+ """Extract session ID from FastMCP context.
+
+ Args:
+ ctx: FastMCP context
+
+ Returns:
+ Session identifier string
+ """
+ if ctx is None:
+ return "default"
+
+ # Try various context attributes
+ if hasattr(ctx, "session") and ctx.session:
+ return str(ctx.session)
+ if hasattr(ctx, "client_id") and ctx.client_id:
+ return str(ctx.client_id)
+ if hasattr(ctx, "request_id") and ctx.request_id:
+ return f"req-{ctx.request_id}"
+
+ return "default"
+
+ # Convenience methods for subclasses
+ def safe_get(self, port: int, endpoint: str, params: Optional[Dict] = None) -> Dict:
+ """Make GET request to Ghidra instance."""
+ return safe_get(port, endpoint, params)
+
+ def safe_post(self, port: int, endpoint: str, data: Any) -> Dict:
+ """Make POST request to Ghidra instance."""
+ return safe_post(port, endpoint, data)
+
+ def safe_put(self, port: int, endpoint: str, data: Dict) -> Dict:
+ """Make PUT request to Ghidra instance."""
+ return safe_put(port, endpoint, data)
+
+ def safe_patch(self, port: int, endpoint: str, data: Dict) -> Dict:
+ """Make PATCH request to Ghidra instance."""
+ return safe_patch(port, endpoint, data)
+
+ def safe_delete(self, port: int, endpoint: str) -> Dict:
+ """Make DELETE request to Ghidra instance."""
+ return safe_delete(port, endpoint)
+
+ def simplify_response(self, response: Dict) -> Dict:
+ """Simplify HATEOAS response."""
+ return simplify_response(response)
+
+ def paginate_response(
+ self,
+ data: list,
+ query_params: Dict,
+ tool_name: str,
+ session_id: str = "default",
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ) -> Dict:
+ """Create paginated response."""
+ return paginate_response(
+ data=data,
+ query_params=query_params,
+ tool_name=tool_name,
+ session_id=session_id,
+ page_size=page_size,
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ # Async logging helpers
+ async def log_info(self, ctx: Optional[Context], message: str) -> None:
+ """Log info message."""
+ await log_info(ctx, message)
+
+ async def log_debug(self, ctx: Optional[Context], message: str) -> None:
+ """Log debug message."""
+ await log_debug(ctx, message)
+
+ async def log_warning(self, ctx: Optional[Context], message: str) -> None:
+ """Log warning message."""
+ await log_warning(ctx, message)
+
+ async def log_error(self, ctx: Optional[Context], message: str) -> None:
+ """Log error message."""
+ await log_error(ctx, message)
diff --git a/src/ghydramcp/mixins/cursors.py b/src/ghydramcp/mixins/cursors.py
new file mode 100644
index 0000000..a1cb68c
--- /dev/null
+++ b/src/ghydramcp/mixins/cursors.py
@@ -0,0 +1,174 @@
+"""Cursor management mixin for GhydraMCP.
+
+Provides tools for managing pagination cursors.
+"""
+
+from typing import Any, Dict, Optional
+
+from fastmcp import Context
+from fastmcp.contrib.mcp_mixin import mcp_tool
+
+from .base import GhydraMixinBase
+from ..core.pagination import get_cursor_manager
+
+
+class CursorsMixin(GhydraMixinBase):
+ """Mixin for cursor management.
+
+ Provides tools for navigating paginated results.
+ """
+
+ @mcp_tool()
+ def cursor_next(
+ self, cursor_id: str, ctx: Optional[Context] = None
+ ) -> Dict[str, Any]:
+ """Get the next page of results for a cursor.
+
+ Args:
+ cursor_id: The cursor identifier from a previous paginated response
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Next page of results with updated pagination info
+ """
+ session_id = self._get_session_id(ctx)
+ cursor_manager = get_cursor_manager()
+
+ # Get and advance cursor
+ state = cursor_manager.get_cursor(cursor_id, session_id)
+ if not state:
+ return {
+ "success": False,
+ "error": {
+ "code": "CURSOR_NOT_FOUND",
+ "message": f"Cursor '{cursor_id}' not found or expired. "
+ "Cursors expire after 5 minutes of inactivity.",
+ },
+ }
+
+ # Advance to next page
+ state = cursor_manager.advance_cursor(cursor_id, session_id)
+ if not state:
+ return {
+ "success": False,
+ "error": {
+ "code": "CURSOR_ADVANCE_FAILED",
+ "message": "Failed to advance cursor",
+ },
+ }
+
+ current_page = cursor_manager.get_page(state)
+ response_cursor = cursor_id if state.has_more else None
+
+ response = {
+ "success": True,
+ "result": current_page,
+ "pagination": {
+ "cursor_id": response_cursor,
+ "session_id": session_id,
+ "total_count": state.total_count,
+ "filtered_count": state.filtered_count,
+ "page_size": state.page_size,
+ "current_page": state.current_page,
+ "total_pages": state.total_pages,
+ "has_more": state.has_more,
+ "grep_pattern": state.grep_pattern,
+ "items_returned": len(current_page),
+ },
+ }
+
+ if state.has_more:
+ remaining = state.filtered_count - (state.current_page * state.page_size)
+ response["_message"] = (
+ f"Showing {len(current_page)} of {state.filtered_count} items "
+ f"(page {state.current_page}/{state.total_pages}). "
+ f"To get the next {min(state.page_size, remaining)} items, call: "
+ f"cursor_next(cursor_id='{cursor_id}')"
+ )
+ else:
+ response["_message"] = (
+ f"Complete: {len(current_page)} items returned (final page)"
+ )
+
+ return response
+
+ @mcp_tool()
+ def cursor_list(
+ self, ctx: Optional[Context] = None, all_sessions: bool = False
+ ) -> Dict[str, Any]:
+ """List active cursors for the current session.
+
+ Args:
+ ctx: FastMCP context (auto-injected)
+ all_sessions: Include cursors from all sessions (admin only)
+
+ Returns:
+ List of active cursors with their status
+ """
+ session_id = self._get_session_id(ctx)
+ cursor_manager = get_cursor_manager()
+
+ if all_sessions:
+ cursors = cursor_manager.list_cursors()
+ else:
+ cursors = cursor_manager.list_cursors(session_id)
+
+ return {
+ "success": True,
+ "cursors": cursors,
+ "session_id": session_id,
+ "count": len(cursors),
+ }
+
+ @mcp_tool()
+ def cursor_delete(
+ self, cursor_id: str, ctx: Optional[Context] = None
+ ) -> Dict[str, Any]:
+ """Delete a specific cursor.
+
+ Args:
+ cursor_id: The cursor identifier to delete
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Confirmation of deletion
+ """
+ session_id = self._get_session_id(ctx)
+ cursor_manager = get_cursor_manager()
+
+ deleted = cursor_manager.delete_cursor(cursor_id, session_id)
+
+ if deleted:
+ return {
+ "success": True,
+ "message": f"Cursor '{cursor_id}' deleted",
+ }
+ else:
+ return {
+ "success": False,
+ "error": {
+ "code": "CURSOR_NOT_FOUND",
+ "message": f"Cursor '{cursor_id}' not found or belongs to another session",
+ },
+ }
+
+ @mcp_tool()
+ def cursor_delete_all(self, ctx: Optional[Context] = None) -> Dict[str, Any]:
+ """Delete all cursors for the current session.
+
+ Args:
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Number of cursors deleted
+ """
+ session_id = self._get_session_id(ctx)
+ cursor_manager = get_cursor_manager()
+
+ count = cursor_manager.delete_session_cursors(session_id)
+
+ return {
+ "success": True,
+ "message": f"Deleted {count} cursor(s) for session",
+ "deleted_count": count,
+ }
diff --git a/src/ghydramcp/mixins/data.py b/src/ghydramcp/mixins/data.py
new file mode 100644
index 0000000..a63a73e
--- /dev/null
+++ b/src/ghydramcp/mixins/data.py
@@ -0,0 +1,384 @@
+"""Data mixin for GhydraMCP.
+
+Provides tools for data items and strings operations.
+"""
+
+from typing import Any, Dict, Optional
+
+from fastmcp import Context
+from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
+
+from .base import GhydraMixinBase
+from ..config import get_config
+
+
+class DataMixin(GhydraMixinBase):
+ """Mixin for data operations.
+
+ Provides tools for:
+ - Listing and searching data items
+ - Creating and modifying data
+ - Working with strings
+ - Setting data types
+ """
+
+ @mcp_tool()
+ def data_list(
+ self,
+ addr: Optional[str] = None,
+ name: Optional[str] = None,
+ name_contains: Optional[str] = None,
+ type: Optional[str] = None,
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """List defined data items with filtering and cursor-based pagination.
+
+ Args:
+ addr: Filter by address (hexadecimal)
+ name: Exact name match filter (case-sensitive)
+ name_contains: Substring name filter (case-insensitive)
+ type: Filter by data type (e.g. "string", "dword")
+ port: Ghidra instance port (optional)
+ page_size: Items per page (default: 50, max: 500)
+ grep: Regex pattern to filter results
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all results without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Data items with pagination metadata
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+
+ params = {"offset": 0, "limit": 10000}
+ if addr:
+ params["addr"] = addr
+ if name:
+ params["name"] = name
+ if name_contains:
+ params["name_contains"] = name_contains
+ if type:
+ params["type"] = type
+
+ response = self.safe_get(port, "data", params)
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ all_data = simplified.get("result", [])
+ if not isinstance(all_data, list):
+ all_data = []
+
+ query_params = {
+ "tool": "data_list",
+ "port": port,
+ "addr": addr,
+ "name": name,
+ "name_contains": name_contains,
+ "type": type,
+ "grep": grep,
+ }
+ session_id = self._get_session_id(ctx)
+
+ return self.paginate_response(
+ data=all_data,
+ query_params=query_params,
+ tool_name="data_list",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ @mcp_tool()
+ def data_list_strings(
+ self,
+ filter: Optional[str] = None,
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """List all defined strings in the binary with pagination.
+
+ Args:
+ filter: Server-side string content filter
+ port: Ghidra instance port (optional)
+ page_size: Items per page (default: 50, max: 500)
+ grep: Regex pattern to filter results (e.g., "password|key")
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all strings without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ List of strings with pagination info
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+ fetch_limit = 10000 if return_all else max(page_size * 10, 2000)
+
+ params = {"offset": 0, "limit": fetch_limit}
+ if filter:
+ params["filter"] = filter
+
+ response = self.safe_get(port, "strings", params)
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ result_data = simplified.get("result", [])
+ if not isinstance(result_data, list):
+ return simplified
+
+ query_params = {
+ "tool": "data_list_strings",
+ "port": port,
+ "filter": filter,
+ "grep": grep,
+ }
+ session_id = self._get_session_id(ctx)
+
+ return self.paginate_response(
+ data=result_data,
+ query_params=query_params,
+ tool_name="data_list_strings",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ @mcp_tool()
+ def data_create(
+ self,
+ address: str,
+ data_type: str,
+ size: Optional[int] = None,
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Define a new data item at the specified address.
+
+ Args:
+ address: Memory address in hex format
+ data_type: Data type (e.g. "string", "dword", "byte")
+ size: Optional size in bytes
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result with created data information
+ """
+ if not address or not data_type:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Address and data_type parameters are required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {"address": address, "type": data_type}
+ if size is not None:
+ payload["size"] = size
+
+ response = self.safe_post(port, "data", payload)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def data_rename(
+ self,
+ address: str,
+ name: str,
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Rename a data item.
+
+ Args:
+ address: Memory address in hex format
+ name: New name for the data item
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not address or not name:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Address and name parameters are required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {"address": address, "newName": name}
+ response = self.safe_post(port, "data", payload)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def data_delete(
+ self,
+ address: str,
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Delete data at the specified address.
+
+ Args:
+ address: Memory address in hex format
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {"address": address, "action": "delete"}
+ response = self.safe_post(port, "data/delete", payload)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def data_set_type(
+ self,
+ address: str,
+ data_type: str,
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Set the data type of a data item.
+
+ Args:
+ address: Memory address in hex format
+ data_type: Data type name (e.g. "uint32_t", "char[10]")
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not address or not data_type:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Address and data_type parameters are required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {"address": address, "type": data_type}
+ response = self.safe_post(port, "data/type", payload)
+ return self.simplify_response(response)
+
+ # Resources
+
+ @mcp_resource(uri="ghidra://instance/{port}/strings")
+ def resource_strings_list(self, port: Optional[int] = None) -> Dict[str, Any]:
+ """MCP Resource: List strings (capped).
+
+ Args:
+ port: Ghidra instance port
+
+ Returns:
+ List of strings (capped at 1000)
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"error": str(e)}
+
+ config = get_config()
+ cap = config.resource_caps.get("strings", 1000)
+
+ response = self.safe_get(port, "strings", {"limit": cap})
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ strings = simplified.get("result", [])
+ if not isinstance(strings, list):
+ strings = []
+
+ return {
+ "strings": strings[:cap],
+ "count": len(strings),
+ "capped_at": cap if len(strings) >= cap else None,
+ "_hint": "Use data_list_strings() tool for full pagination" if len(strings) >= cap else None,
+ }
+
+ @mcp_resource(uri="ghidra://instance/{port}/data")
+ def resource_data_list(self, port: Optional[int] = None) -> Dict[str, Any]:
+ """MCP Resource: List data items (capped).
+
+ Args:
+ port: Ghidra instance port
+
+ Returns:
+ List of data items (capped at 1000)
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"error": str(e)}
+
+ config = get_config()
+ cap = config.resource_caps.get("data", 1000)
+
+ response = self.safe_get(port, "data", {"limit": cap})
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ data_items = simplified.get("result", [])
+ if not isinstance(data_items, list):
+ data_items = []
+
+ return {
+ "data": data_items[:cap],
+ "count": len(data_items),
+ "capped_at": cap if len(data_items) >= cap else None,
+ "_hint": "Use data_list() tool for full pagination" if len(data_items) >= cap else None,
+ }
diff --git a/src/ghydramcp/mixins/docker.py b/src/ghydramcp/mixins/docker.py
new file mode 100644
index 0000000..2c8d1a2
--- /dev/null
+++ b/src/ghydramcp/mixins/docker.py
@@ -0,0 +1,656 @@
+"""Docker management mixin for GhydraMCP.
+
+Provides tools for managing Ghidra Docker containers programmatically.
+Allows the MCP server to automatically start containers when Ghidra isn't available.
+"""
+
+import asyncio
+import os
+import shutil
+import subprocess
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from fastmcp import Context
+from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
+
+from ..config import get_config, get_docker_config
+
+
+class DockerMixin(MCPMixin):
+ """Docker container management for GhydraMCP.
+
+ Provides tools to start, stop, and manage Ghidra containers
+ with the GhydraMCP plugin pre-installed.
+ """
+
+ # Track running containers
+ _containers: Dict[str, Dict[str, Any]] = {}
+
+ def __init__(self):
+ """Initialize Docker mixin."""
+ self._check_docker_available()
+
+ def _check_docker_available(self) -> bool:
+ """Check if Docker is available on the system."""
+ return shutil.which("docker") is not None
+
+ def _run_docker_cmd(
+ self, args: List[str], check: bool = True, capture: bool = True
+ ) -> subprocess.CompletedProcess:
+ """Run a docker command.
+
+ Args:
+ args: Command arguments (after 'docker')
+ check: Raise exception on non-zero exit
+ capture: Capture stdout/stderr
+
+ Returns:
+ CompletedProcess result
+ """
+ cmd = ["docker"] + args
+ return subprocess.run(
+ cmd,
+ check=check,
+ capture_output=capture,
+ text=True,
+ )
+
+ def _run_compose_cmd(
+ self,
+ args: List[str],
+ project_dir: Optional[Path] = None,
+ check: bool = True,
+ capture: bool = True,
+ ) -> subprocess.CompletedProcess:
+ """Run a docker compose command.
+
+ Args:
+ args: Command arguments (after 'docker compose')
+ project_dir: Directory containing docker-compose.yml
+ check: Raise exception on non-zero exit
+ capture: Capture stdout/stderr
+
+ Returns:
+ CompletedProcess result
+ """
+ cmd = ["docker", "compose"]
+
+ # Use project directory if specified
+ if project_dir:
+ cmd.extend(["-f", str(project_dir / "docker-compose.yml")])
+
+ cmd.extend(args)
+
+ env = os.environ.copy()
+ if project_dir:
+ env["COMPOSE_PROJECT_NAME"] = "ghydramcp"
+
+ return subprocess.run(
+ cmd,
+ check=check,
+ capture_output=capture,
+ text=True,
+ cwd=project_dir,
+ env=env,
+ )
+
+ @mcp_tool(
+ name="docker_status",
+ description="Check Docker availability and running GhydraMCP containers",
+ )
+ async def docker_status(self, ctx: Optional[Context] = None) -> Dict[str, Any]:
+ """Check Docker status and list running GhydraMCP containers.
+
+ Returns:
+ Status information including:
+ - docker_available: Whether Docker is installed
+ - docker_running: Whether Docker daemon is running
+ - containers: List of GhydraMCP containers with their status
+ - images: Available GhydraMCP images
+ """
+ result = {
+ "docker_available": False,
+ "docker_running": False,
+ "containers": [],
+ "images": [],
+ "compose_available": False,
+ }
+
+ # Check if docker is installed
+ if not self._check_docker_available():
+ return result
+
+ result["docker_available"] = True
+
+ # Check if docker daemon is running
+ try:
+ self._run_docker_cmd(["info"], check=True)
+ result["docker_running"] = True
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return result
+
+ # Check for docker compose
+ try:
+ self._run_docker_cmd(["compose", "version"], check=True)
+ result["compose_available"] = True
+ except subprocess.CalledProcessError:
+ pass
+
+ # List GhydraMCP containers
+ try:
+ ps_result = self._run_docker_cmd(
+ [
+ "ps",
+ "-a",
+ "--filter",
+ "label=org.opencontainers.image.title=ghydramcp",
+ "--format",
+ "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}",
+ ]
+ )
+ for line in ps_result.stdout.strip().split("\n"):
+ if line:
+ parts = line.split("\t")
+ if len(parts) >= 3:
+ result["containers"].append(
+ {
+ "id": parts[0],
+ "name": parts[1],
+ "status": parts[2],
+ "ports": parts[3] if len(parts) > 3 else "",
+ }
+ )
+ except subprocess.CalledProcessError:
+ pass
+
+ # Also check by name pattern
+ try:
+ ps_result = self._run_docker_cmd(
+ [
+ "ps",
+ "-a",
+ "--filter",
+ "name=ghydramcp",
+ "--format",
+ "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}",
+ ]
+ )
+ existing_ids = {c["id"] for c in result["containers"]}
+ for line in ps_result.stdout.strip().split("\n"):
+ if line:
+ parts = line.split("\t")
+ if len(parts) >= 3 and parts[0] not in existing_ids:
+ result["containers"].append(
+ {
+ "id": parts[0],
+ "name": parts[1],
+ "status": parts[2],
+ "ports": parts[3] if len(parts) > 3 else "",
+ }
+ )
+ except subprocess.CalledProcessError:
+ pass
+
+ # List GhydraMCP images
+ try:
+ images_result = self._run_docker_cmd(
+ [
+ "images",
+ "--filter",
+ "reference=ghydramcp*",
+ "--format",
+ "{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}",
+ ]
+ )
+ for line in images_result.stdout.strip().split("\n"):
+ if line:
+ parts = line.split("\t")
+ if len(parts) >= 2:
+ result["images"].append(
+ {
+ "name": parts[0],
+ "size": parts[1],
+ "created": parts[2] if len(parts) > 2 else "",
+ }
+ )
+ except subprocess.CalledProcessError:
+ pass
+
+ return result
+
+ @mcp_tool(
+ name="docker_start",
+ description="Start a GhydraMCP Docker container to analyze a binary",
+ )
+ async def docker_start(
+ self,
+ binary_path: str,
+ port: int = 8192,
+ memory: str = "2G",
+ name: Optional[str] = None,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Start a GhydraMCP Docker container for binary analysis.
+
+ This creates a new Ghidra instance in Docker with the GhydraMCP
+ plugin pre-installed. The binary will be imported and analyzed,
+ then the HTTP API will be available on the specified port.
+
+ Args:
+ binary_path: Path to the binary file to analyze
+ port: Port to expose the HTTP API (default: 8192)
+ memory: Max JVM heap memory (default: 2G)
+ name: Container name (auto-generated if not specified)
+
+ Returns:
+ Container info including ID, name, and API URL
+ """
+ if not self._check_docker_available():
+ return {"error": "Docker is not available on this system"}
+
+ # Verify binary exists
+ binary_file = Path(binary_path).resolve()
+ if not binary_file.exists():
+ return {"error": f"Binary not found: {binary_path}"}
+
+ # Generate container name if not specified
+ if name is None:
+ name = f"ghydramcp-{binary_file.stem}-{port}"
+
+ # Clean up invalid characters in container name
+ name = "".join(c if c.isalnum() or c in "-_" else "-" for c in name)
+
+ try:
+ # Check if container with this name already exists
+ check_result = self._run_docker_cmd(
+ ["ps", "-a", "-q", "-f", f"name={name}"], check=False
+ )
+ if check_result.stdout.strip():
+ return {
+ "error": f"Container '{name}' already exists. Stop it first with docker_stop."
+ }
+
+ # Check if port is already in use
+ port_check = self._run_docker_cmd(
+ ["ps", "-q", "-f", f"publish={port}"], check=False
+ )
+ if port_check.stdout.strip():
+ return {
+ "error": f"Port {port} is already in use by another container"
+ }
+
+ # Start the container
+ run_result = self._run_docker_cmd(
+ [
+ "run",
+ "-d",
+ "--name",
+ name,
+ "-p",
+ f"{port}:8192",
+ "-v",
+ f"{binary_file.parent}:/binaries:ro",
+ "-e",
+ f"GHYDRA_MAXMEM={memory}",
+ "ghydramcp:latest",
+ f"/binaries/{binary_file.name}",
+ ]
+ )
+
+ container_id = run_result.stdout.strip()
+
+ # Track the container
+ self._containers[container_id] = {
+ "name": name,
+ "port": port,
+ "binary": str(binary_file),
+ "memory": memory,
+ }
+
+ return {
+ "success": True,
+ "container_id": container_id[:12],
+ "name": name,
+ "port": port,
+ "api_url": f"http://localhost:{port}/",
+ "binary": str(binary_file),
+ "message": (
+ f"Container started. Analysis in progress. "
+ f"API will be available at http://localhost:{port}/ once analysis completes. "
+ f"Use docker_logs('{name}') to monitor progress."
+ ),
+ }
+
+ except subprocess.CalledProcessError as e:
+ return {"error": f"Failed to start container: {e.stderr or e.stdout}"}
+
+ @mcp_tool(
+ name="docker_stop",
+ description="Stop a running GhydraMCP Docker container",
+ )
+ async def docker_stop(
+ self, name_or_id: str, remove: bool = True, ctx: Optional[Context] = None
+ ) -> Dict[str, Any]:
+ """Stop a GhydraMCP Docker container.
+
+ Args:
+ name_or_id: Container name or ID
+ remove: Also remove the container (default: True)
+
+ Returns:
+ Status message
+ """
+ if not self._check_docker_available():
+ return {"error": "Docker is not available on this system"}
+
+ try:
+ # Stop the container
+ self._run_docker_cmd(["stop", name_or_id])
+
+ if remove:
+ self._run_docker_cmd(["rm", name_or_id])
+ # Remove from tracking
+ self._containers = {
+ k: v
+ for k, v in self._containers.items()
+ if not (k.startswith(name_or_id) or v.get("name") == name_or_id)
+ }
+ return {"success": True, "message": f"Container '{name_or_id}' stopped and removed"}
+ else:
+ return {"success": True, "message": f"Container '{name_or_id}' stopped"}
+
+ except subprocess.CalledProcessError as e:
+ return {"error": f"Failed to stop container: {e.stderr or e.stdout}"}
+
+ @mcp_tool(
+ name="docker_logs",
+ description="Get logs from a GhydraMCP Docker container",
+ )
+ async def docker_logs(
+ self,
+ name_or_id: str,
+ tail: int = 100,
+ follow: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Get logs from a GhydraMCP container.
+
+ Args:
+ name_or_id: Container name or ID
+ tail: Number of lines to show (default: 100)
+ follow: Whether to follow log output (not recommended for MCP)
+
+ Returns:
+ Container logs
+ """
+ if not self._check_docker_available():
+ return {"error": "Docker is not available on this system"}
+
+ try:
+ args = ["logs", "--tail", str(tail)]
+ if follow:
+ args.append("-f")
+ args.append(name_or_id)
+
+ result = self._run_docker_cmd(args)
+ return {
+ "success": True,
+ "container": name_or_id,
+ "logs": result.stdout + result.stderr,
+ }
+
+ except subprocess.CalledProcessError as e:
+ return {"error": f"Failed to get logs: {e.stderr or e.stdout}"}
+
+ @mcp_tool(
+ name="docker_build",
+ description="Build the GhydraMCP Docker image from source",
+ )
+ async def docker_build(
+ self,
+ tag: str = "latest",
+ no_cache: bool = False,
+ project_dir: Optional[str] = None,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Build the GhydraMCP Docker image.
+
+ Args:
+ tag: Image tag (default: 'latest')
+ no_cache: Build without using cache
+ project_dir: Path to GhydraMCP project (auto-detected if not specified)
+
+ Returns:
+ Build status
+ """
+ if not self._check_docker_available():
+ return {"error": "Docker is not available on this system"}
+
+ # Find project directory
+ if project_dir:
+ proj_path = Path(project_dir)
+ else:
+ # Try to find docker/Dockerfile relative to this file
+ module_dir = Path(__file__).parent.parent.parent.parent
+ if (module_dir / "docker" / "Dockerfile").exists():
+ proj_path = module_dir
+ else:
+ return {
+ "error": "Could not find GhydraMCP project directory. Please specify project_dir."
+ }
+
+ dockerfile = proj_path / "docker" / "Dockerfile"
+ if not dockerfile.exists():
+ return {"error": f"Dockerfile not found at {dockerfile}"}
+
+ try:
+ args = [
+ "build",
+ "-t",
+ f"ghydramcp:{tag}",
+ "-f",
+ str(dockerfile),
+ ]
+ if no_cache:
+ args.append("--no-cache")
+ args.append(str(proj_path))
+
+ # Run build (this can take a while)
+ result = self._run_docker_cmd(args, capture=True)
+
+ return {
+ "success": True,
+ "image": f"ghydramcp:{tag}",
+ "message": f"Successfully built ghydramcp:{tag}",
+ "output": result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout,
+ }
+
+ except subprocess.CalledProcessError as e:
+ return {"error": f"Build failed: {e.stderr or e.stdout}"}
+
+ @mcp_tool(
+ name="docker_health",
+ description="Check if a GhydraMCP container's API is responding",
+ )
+ async def docker_health(
+ self, port: int = 8192, timeout: float = 5.0, ctx: Optional[Context] = None
+ ) -> Dict[str, Any]:
+ """Check if a GhydraMCP container's API is healthy.
+
+ Args:
+ port: API port to check (default: 8192)
+ timeout: Request timeout in seconds
+
+ Returns:
+ Health status and API info if available
+ """
+ import urllib.request
+ import urllib.error
+ import json
+
+ url = f"http://localhost:{port}/"
+
+ try:
+ req = urllib.request.Request(url)
+ with urllib.request.urlopen(req, timeout=timeout) as response:
+ data = json.loads(response.read().decode())
+ return {
+ "healthy": True,
+ "port": port,
+ "api_version": data.get("api_version"),
+ "program": data.get("program"),
+ "file": data.get("file"),
+ }
+ except urllib.error.URLError as e:
+ return {
+ "healthy": False,
+ "port": port,
+ "error": str(e.reason),
+ "message": "Container may still be starting or analyzing binary",
+ }
+ except Exception as e:
+ return {
+ "healthy": False,
+ "port": port,
+ "error": str(e),
+ }
+
+ @mcp_tool(
+ name="docker_wait",
+ description="Wait for a GhydraMCP container to become healthy",
+ )
+ async def docker_wait(
+ self,
+ port: int = 8192,
+ timeout: float = 300.0,
+ interval: float = 5.0,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Wait for a GhydraMCP container to become healthy.
+
+ Polls the API endpoint until it responds or timeout is reached.
+
+ Args:
+ port: API port to check (default: 8192)
+ timeout: Maximum time to wait in seconds (default: 300)
+ interval: Polling interval in seconds (default: 5)
+
+ Returns:
+ Health status once healthy, or error on timeout
+ """
+ import time
+
+ start_time = time.time()
+ last_error = None
+
+ while (time.time() - start_time) < timeout:
+ result = await self.docker_health(port=port, timeout=interval, ctx=ctx)
+ if result.get("healthy"):
+ result["waited_seconds"] = round(time.time() - start_time, 1)
+ return result
+ last_error = result.get("error")
+ await asyncio.sleep(interval)
+
+ return {
+ "healthy": False,
+ "port": port,
+ "error": f"Timeout after {timeout}s waiting for container",
+ "last_error": last_error,
+ }
+
+ @mcp_tool(
+ name="docker_auto_start",
+ description="Automatically start a GhydraMCP container if no Ghidra instance is available",
+ )
+ async def docker_auto_start(
+ self,
+ binary_path: str,
+ port: int = 8192,
+ wait: bool = True,
+ timeout: float = 300.0,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Automatically start a Docker container if no Ghidra instance is available.
+
+ This is the main entry point for automatic Docker management:
+ 1. Checks if a Ghidra instance is already running on the port
+ 2. If not, starts a new Docker container
+ 3. Optionally waits for the container to become healthy
+ 4. Returns connection info for the instance
+
+ Args:
+ binary_path: Path to the binary to analyze
+ port: Port for the HTTP API (default: 8192)
+ wait: Wait for container to be ready (default: True)
+ timeout: Max wait time in seconds (default: 300)
+
+ Returns:
+ Instance connection info
+ """
+ # First, check if there's already a Ghidra instance on this port
+ health = await self.docker_health(port=port, ctx=ctx)
+ if health.get("healthy"):
+ return {
+ "source": "existing",
+ "port": port,
+ "api_url": f"http://localhost:{port}/",
+ "program": health.get("program"),
+ "message": "Using existing Ghidra instance",
+ }
+
+ # Check if Docker is available
+ status = await self.docker_status(ctx=ctx)
+ if not status.get("docker_running"):
+ return {
+ "error": "Docker is not available. Please install Docker or start Ghidra manually."
+ }
+
+ # Check if we have the image
+ if not any("ghydramcp" in img.get("name", "") for img in status.get("images", [])):
+ return {
+ "error": (
+ "GhydraMCP Docker image not found. "
+ "Build it with docker_build() or 'make build' first."
+ )
+ }
+
+ # Start a new container
+ start_result = await self.docker_start(
+ binary_path=binary_path, port=port, ctx=ctx
+ )
+
+ if not start_result.get("success"):
+ return start_result
+
+ if wait:
+ # Wait for the container to become healthy
+ wait_result = await self.docker_wait(port=port, timeout=timeout, ctx=ctx)
+ if wait_result.get("healthy"):
+ return {
+ "source": "docker",
+ "container_id": start_result.get("container_id"),
+ "container_name": start_result.get("name"),
+ "port": port,
+ "api_url": f"http://localhost:{port}/",
+ "program": wait_result.get("program"),
+ "waited_seconds": wait_result.get("waited_seconds"),
+ "message": f"Docker container ready after {wait_result.get('waited_seconds')}s",
+ }
+ else:
+ return {
+ "warning": "Container started but not yet healthy",
+ "container_id": start_result.get("container_id"),
+ "port": port,
+ "last_error": wait_result.get("error"),
+ "message": "Container may still be analyzing. Check docker_logs() for progress.",
+ }
+
+ return {
+ "source": "docker",
+ "container_id": start_result.get("container_id"),
+ "container_name": start_result.get("name"),
+ "port": port,
+ "api_url": f"http://localhost:{port}/",
+ "message": "Container starting. Use docker_wait() or docker_health() to check status.",
+ }
diff --git a/src/ghydramcp/mixins/functions.py b/src/ghydramcp/mixins/functions.py
new file mode 100644
index 0000000..73298d0
--- /dev/null
+++ b/src/ghydramcp/mixins/functions.py
@@ -0,0 +1,524 @@
+"""Functions mixin for GhydraMCP.
+
+Provides tools for function analysis, decompilation, and manipulation.
+"""
+
+from typing import Any, Dict, Optional
+from urllib.parse import quote
+
+from fastmcp import Context
+from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
+
+from .base import GhydraMixinBase
+from ..config import get_config
+
+
+class FunctionsMixin(GhydraMixinBase):
+ """Mixin for function operations.
+
+ Provides tools for:
+ - Listing and searching functions
+ - Decompiling functions
+ - Disassembling functions
+ - Renaming functions
+ - Setting function signatures
+ - Managing function variables
+ """
+
+ @mcp_tool()
+ def functions_list(
+ self,
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """List functions with cursor-based pagination.
+
+ Args:
+ port: Ghidra instance port (optional)
+ page_size: Functions per page (default: 50, max: 500)
+ grep: Regex pattern to filter function names
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all functions without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Paginated list of functions
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+ response = self.safe_get(port, "functions", {"limit": 10000})
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ functions = simplified.get("result", [])
+ if not isinstance(functions, list):
+ functions = []
+
+ query_params = {"tool": "functions_list", "port": port, "grep": grep}
+ session_id = self._get_session_id(ctx)
+
+ return self.paginate_response(
+ data=functions,
+ query_params=query_params,
+ tool_name="functions_list",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ @mcp_tool()
+ def functions_get(
+ self,
+ name: Optional[str] = None,
+ address: Optional[str] = None,
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Get detailed information about a function.
+
+ Args:
+ name: Function name (mutually exclusive with address)
+ address: Function address in hex format
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Detailed function information
+ """
+ if not name and not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Either name or address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ if address:
+ endpoint = f"functions/{address}"
+ else:
+ endpoint = f"functions/by-name/{quote(name)}"
+
+ response = self.safe_get(port, endpoint)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def functions_decompile(
+ self,
+ name: Optional[str] = None,
+ address: Optional[str] = None,
+ syntax_tree: bool = False,
+ style: str = "normalize",
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Get decompiled code for a function with line pagination.
+
+ Args:
+ name: Function name (mutually exclusive with address)
+ address: Function address in hex format
+ syntax_tree: Include syntax tree (default: False)
+ style: Decompiler style (default: "normalize")
+ port: Ghidra instance port (optional)
+ page_size: Lines per page (default: 50, max: 500)
+ grep: Regex pattern to filter lines
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all lines without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Decompiled code with pagination
+ """
+ if not name and not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Either name or address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+
+ if address:
+ endpoint = f"functions/{address}/decompile"
+ else:
+ endpoint = f"functions/by-name/{quote(name)}/decompile"
+
+ params = {"syntaxTree": str(syntax_tree).lower(), "style": style}
+ response = self.safe_get(port, endpoint, params)
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ result = simplified.get("result", {})
+ decompiled = result.get("decompiled_text", result.get("ccode", ""))
+
+ if not decompiled:
+ return simplified
+
+ # Split into lines for pagination
+ lines = decompiled.split("\n")
+
+ query_params = {
+ "tool": "functions_decompile",
+ "port": port,
+ "name": name,
+ "address": address,
+ "grep": grep,
+ }
+ session_id = self._get_session_id(ctx)
+
+ paginated = self.paginate_response(
+ data=lines,
+ query_params=query_params,
+ tool_name="functions_decompile",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ # Convert lines back to text in result
+ if paginated.get("success"):
+ paginated["result"] = "\n".join(paginated.get("result", []))
+ paginated["function_name"] = result.get("name", name or address)
+
+ return paginated
+
+ @mcp_tool()
+ def functions_disassemble(
+ self,
+ name: Optional[str] = None,
+ address: Optional[str] = None,
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Get disassembly for a function with instruction pagination.
+
+ Args:
+ name: Function name (mutually exclusive with address)
+ address: Function address in hex format
+ port: Ghidra instance port (optional)
+ page_size: Instructions per page (default: 50, max: 500)
+ grep: Regex pattern to filter instructions
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all instructions without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Disassembly with pagination
+ """
+ if not name and not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Either name or address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+
+ if address:
+ endpoint = f"functions/{address}/disassembly"
+ else:
+ endpoint = f"functions/by-name/{quote(name)}/disassembly"
+
+ response = self.safe_get(port, endpoint)
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ result = simplified.get("result", {})
+ disasm_text = result.get("disassembly_text", "")
+
+ if not disasm_text:
+ return simplified
+
+ # Split into lines for pagination
+ lines = [line for line in disasm_text.split("\n") if line.strip()]
+
+ query_params = {
+ "tool": "functions_disassemble",
+ "port": port,
+ "name": name,
+ "address": address,
+ "grep": grep,
+ }
+ session_id = self._get_session_id(ctx)
+
+ paginated = self.paginate_response(
+ data=lines,
+ query_params=query_params,
+ tool_name="functions_disassemble",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ # Convert lines back to text
+ if paginated.get("success"):
+ paginated["result"] = "\n".join(paginated.get("result", []))
+ paginated["function_name"] = result.get("name", name or address)
+
+ return paginated
+
+ @mcp_tool()
+ def functions_rename(
+ self,
+ old_name: Optional[str] = None,
+ address: Optional[str] = None,
+ new_name: str = "",
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Rename a function.
+
+ Args:
+ old_name: Current function name
+ address: Function address in hex format
+ new_name: New name for the function
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not new_name:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "new_name parameter is required",
+ },
+ }
+
+ if not old_name and not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Either old_name or address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ if address:
+ endpoint = f"functions/{address}"
+ else:
+ endpoint = f"functions/by-name/{quote(old_name)}"
+
+ payload = {"name": new_name}
+ response = self.safe_patch(port, endpoint, payload)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def functions_create(self, address: str, port: Optional[int] = None) -> Dict[str, Any]:
+ """Create a function at the specified address.
+
+ Args:
+ address: Memory address in hex format
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Created function information
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {"address": address}
+ response = self.safe_post(port, "functions", payload)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def functions_set_signature(
+ self,
+ name: Optional[str] = None,
+ address: Optional[str] = None,
+ signature: str = "",
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Set the signature/prototype of a function.
+
+ Args:
+ name: Function name
+ address: Function address in hex format
+ signature: New function signature (e.g., "int foo(char* arg1, int arg2)")
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not signature:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "signature parameter is required",
+ },
+ }
+
+ if not name and not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Either name or address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ if address:
+ endpoint = f"functions/{address}/signature"
+ else:
+ endpoint = f"functions/by-name/{quote(name)}/signature"
+
+ payload = {"signature": signature}
+ response = self.safe_put(port, endpoint, payload)
+ return self.simplify_response(response)
+
+ # Resources
+
+ @mcp_resource(uri="ghidra://instance/{port}/functions")
+ def resource_functions_list(self, port: Optional[int] = None) -> Dict[str, Any]:
+ """MCP Resource: List functions (capped).
+
+ Args:
+ port: Ghidra instance port
+
+ Returns:
+ List of functions (capped at 1000)
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"error": str(e)}
+
+ config = get_config()
+ cap = config.resource_caps.get("functions", 1000)
+
+ response = self.safe_get(port, "functions", {"limit": cap})
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ functions = simplified.get("result", [])
+ if not isinstance(functions, list):
+ functions = []
+
+ return {
+ "functions": functions[:cap],
+ "count": len(functions),
+ "capped_at": cap if len(functions) >= cap else None,
+ "_hint": f"Use functions_list() tool for full pagination" if len(functions) >= cap else None,
+ }
+
+ @mcp_resource(uri="ghidra://instance/{port}/function/decompile/address/{address}")
+ def resource_decompiled_by_address(
+ self, port: Optional[int] = None, address: Optional[str] = None
+ ) -> str:
+ """MCP Resource: Get decompiled code by address.
+
+ Args:
+ port: Ghidra instance port
+ address: Function address
+
+ Returns:
+ Decompiled code as text
+ """
+ if not address:
+ return "Error: address is required"
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return f"Error: {e}"
+
+ response = self.safe_get(port, f"functions/{address}/decompile")
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ error = simplified.get("error", {})
+ return f"Error: {error.get('message', 'Unknown error')}"
+
+ result = simplified.get("result", {})
+ return result.get("decompiled_text", result.get("ccode", "No decompiled code available"))
+
+ @mcp_resource(uri="ghidra://instance/{port}/function/decompile/name/{name}")
+ def resource_decompiled_by_name(
+ self, port: Optional[int] = None, name: Optional[str] = None
+ ) -> str:
+ """MCP Resource: Get decompiled code by function name.
+
+ Args:
+ port: Ghidra instance port
+ name: Function name
+
+ Returns:
+ Decompiled code as text
+ """
+ if not name:
+ return "Error: name is required"
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return f"Error: {e}"
+
+ response = self.safe_get(port, f"functions/by-name/{quote(name)}/decompile")
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ error = simplified.get("error", {})
+ return f"Error: {error.get('message', 'Unknown error')}"
+
+ result = simplified.get("result", {})
+ return result.get("decompiled_text", result.get("ccode", "No decompiled code available"))
diff --git a/src/ghydramcp/mixins/instances.py b/src/ghydramcp/mixins/instances.py
new file mode 100644
index 0000000..20573c8
--- /dev/null
+++ b/src/ghydramcp/mixins/instances.py
@@ -0,0 +1,298 @@
+"""Instance management mixin for GhydraMCP.
+
+Provides tools for discovering, registering, and managing Ghidra instances.
+"""
+
+import time
+from typing import Any, Dict, List, Optional
+
+from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
+
+from .base import GhydraMixinBase
+from ..config import get_config
+from ..core.http_client import safe_get
+
+
+class InstancesMixin(GhydraMixinBase):
+ """Mixin for Ghidra instance management.
+
+ Provides tools for:
+ - Discovering Ghidra instances
+ - Registering/unregistering instances
+ - Setting current working instance
+ - Listing available instances
+ """
+
+ def _discover_instances(
+ self,
+ port_range: range,
+ host: Optional[str] = None,
+ timeout: float = 0.5,
+ ) -> Dict[int, Dict[str, Any]]:
+ """Discover Ghidra instances by scanning ports.
+
+ Args:
+ port_range: Range of ports to scan
+ host: Host to scan (defaults to config)
+ timeout: Connection timeout per port
+
+ Returns:
+ Dict of discovered instances
+ """
+ import requests
+ config = get_config()
+ if host is None:
+ host = config.ghidra_host
+
+ discovered = {}
+
+ for port in port_range:
+ try:
+ url = f"http://{host}:{port}/"
+ response = requests.get(url, timeout=timeout)
+ if response.ok:
+ data = response.json()
+ # Verify it's a Ghidra HATEOAS API
+ if "_links" in data or "api_version" in data:
+ instance_info = {
+ "url": f"http://{host}:{port}",
+ "project": data.get("project", ""),
+ "file": data.get("file", ""),
+ "api_version": data.get("api_version"),
+ "discovered_at": time.time(),
+ }
+ discovered[port] = instance_info
+
+ # Also register it
+ with self._instances_lock:
+ self._instances[port] = instance_info
+
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
+ pass
+ except Exception:
+ pass
+
+ return discovered
+
+ @mcp_tool()
+ def instances_list(self) -> Dict[str, Any]:
+ """List all active Ghidra instances.
+
+ This is the primary tool for working with instances. It automatically
+ discovers new instances on the default host before listing.
+
+ Returns:
+ Dict containing 'instances' list with all available Ghidra instances
+ """
+ config = get_config()
+ # Auto-discover before listing
+ self._discover_instances(config.quick_discovery_range, timeout=0.5)
+
+ with self._instances_lock:
+ return {
+ "instances": [
+ {
+ "port": port,
+ "url": info["url"],
+ "project": info.get("project", ""),
+ "file": info.get("file", ""),
+ }
+ for port, info in self._instances.items()
+ ]
+ }
+
+ @mcp_tool()
+ def instances_discover(self, host: Optional[str] = None) -> Dict[str, Any]:
+ """Discover Ghidra instances on a specific host.
+
+ Use this ONLY when you need to discover instances on a different host.
+ For normal usage, just use instances_list() which auto-discovers.
+
+ Args:
+ host: Host to scan for Ghidra instances (default: configured host)
+
+ Returns:
+ Dict containing 'instances' list with all available instances
+ """
+ config = get_config()
+ self._discover_instances(config.full_discovery_range, host=host, timeout=0.5)
+
+ with self._instances_lock:
+ return {
+ "instances": [
+ {
+ "port": port,
+ "url": info["url"],
+ "project": info.get("project", ""),
+ "file": info.get("file", ""),
+ }
+ for port, info in self._instances.items()
+ ]
+ }
+
+ @mcp_tool()
+ def instances_register(self, port: int, url: Optional[str] = None) -> str:
+ """Register a new Ghidra instance.
+
+ Args:
+ port: Port number of the Ghidra instance
+ url: Optional URL if different from default
+
+ Returns:
+ Confirmation message or error
+ """
+ return self.register_instance(port, url)
+
+ @mcp_tool()
+ def instances_unregister(self, port: int) -> str:
+ """Unregister a Ghidra instance.
+
+ Args:
+ port: Port number of the instance to unregister
+
+ Returns:
+ Confirmation message
+ """
+ return self.unregister_instance(port)
+
+ @mcp_tool()
+ def instances_use(self, port: int) -> str:
+ """Set the current working Ghidra instance.
+
+ All subsequent commands will use this instance by default.
+
+ Args:
+ port: Port number of the instance to use
+
+ Returns:
+ Confirmation message with instance details
+ """
+ with self._instances_lock:
+ if port not in self._instances:
+ # Try to register it first
+ result = self.register_instance(port)
+ if "Failed" in result or "Error" in result:
+ return result
+
+ self.set_current_port(port)
+
+ info = self.get_instance_info(port)
+ if info:
+ return (
+ f"Now using Ghidra instance on port {port}\n"
+ f"Project: {info.get('project', 'N/A')}\n"
+ f"File: {info.get('file', 'N/A')}"
+ )
+ return f"Now using Ghidra instance on port {port}"
+
+ @mcp_tool()
+ def instances_current(self) -> Dict[str, Any]:
+ """Get information about the current working instance.
+
+ Returns:
+ Dict with current instance information or error message
+ """
+ port = self.get_current_port()
+ if port is None:
+ return {
+ "error": "No current instance set. Use instances_use(port) first.",
+ "available_instances": list(self._instances.keys()),
+ }
+
+ info = self.get_instance_info(port)
+ if info:
+ return {
+ "port": port,
+ "url": info["url"],
+ "project": info.get("project", ""),
+ "file": info.get("file", ""),
+ }
+
+ return {"port": port, "status": "registered but no details available"}
+
+ @mcp_resource(uri="ghidra://instances")
+ def resource_instances_list(self) -> Dict[str, Any]:
+ """MCP Resource: List all active Ghidra instances.
+
+ Returns a lightweight enumeration of instances for quick reference.
+ """
+ config = get_config()
+ # Auto-discover before listing
+ self._discover_instances(config.quick_discovery_range, timeout=0.3)
+
+ with self._instances_lock:
+ instances = [
+ {
+ "port": port,
+ "project": info.get("project", ""),
+ "file": info.get("file", ""),
+ }
+ for port, info in self._instances.items()
+ ]
+
+ return {
+ "instances": instances,
+ "count": len(instances),
+ "current_port": self.get_current_port(),
+ }
+
+ @mcp_resource(uri="ghidra://instance/{port}")
+ def resource_instance_info(self, port: int) -> Dict[str, Any]:
+ """MCP Resource: Get detailed information about a Ghidra instance.
+
+ Args:
+ port: Port number of the instance
+
+ Returns:
+ Instance information including program details
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"error": str(e)}
+
+ response = self.safe_get(port, "")
+ if not response.get("success", True):
+ return response
+
+ return self.simplify_response(response)
+
+ @mcp_resource(uri="ghidra://instance/{port}/summary")
+ def resource_program_summary(self, port: Optional[int] = None) -> Dict[str, Any]:
+ """MCP Resource: Get a summary of the program loaded in a Ghidra instance.
+
+ Args:
+ port: Ghidra instance port (optional, uses current if not specified)
+
+ Returns:
+ Program summary with statistics
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"error": str(e)}
+
+ # Get program info
+ response = self.safe_get(port, "")
+ if not response.get("success", True):
+ return response
+
+ simplified = self.simplify_response(response)
+
+ # Get function count
+ funcs_response = self.safe_get(port, "functions", {"limit": 1})
+ func_count = funcs_response.get("size", 0) if funcs_response.get("success", True) else 0
+
+ # Get string count
+ strings_response = self.safe_get(port, "data/strings", {"limit": 1})
+ string_count = strings_response.get("size", 0) if strings_response.get("success", True) else 0
+
+ return {
+ "program_name": simplified.get("result", {}).get("program_name", simplified.get("file", "")),
+ "language": simplified.get("result", {}).get("language", ""),
+ "processor": simplified.get("result", {}).get("processor", ""),
+ "format": simplified.get("result", {}).get("format", ""),
+ "function_count": func_count,
+ "string_count": string_count,
+ "port": port,
+ }
diff --git a/src/ghydramcp/mixins/memory.py b/src/ghydramcp/mixins/memory.py
new file mode 100644
index 0000000..eaa3688
--- /dev/null
+++ b/src/ghydramcp/mixins/memory.py
@@ -0,0 +1,129 @@
+"""Memory mixin for GhydraMCP.
+
+Provides tools for memory read/write operations.
+"""
+
+from typing import Any, Dict, Optional
+
+from fastmcp.contrib.mcp_mixin import mcp_tool
+
+from .base import GhydraMixinBase
+
+
+class MemoryMixin(GhydraMixinBase):
+ """Mixin for memory operations.
+
+ Provides tools for:
+ - Reading memory bytes
+ - Writing memory bytes (use with caution)
+ """
+
+ @mcp_tool()
+ def memory_read(
+ self,
+ address: str,
+ length: int = 16,
+ format: str = "hex",
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Read bytes from memory.
+
+ Args:
+ address: Memory address in hex format
+ length: Number of bytes to read (default: 16)
+ format: Output format - "hex", "base64", or "string" (default: "hex")
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Memory contents in the requested format
+ """
+ if not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Address parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ params = {
+ "address": address,
+ "length": length,
+ "format": format,
+ }
+
+ response = self.safe_get(port, "memory", params)
+ simplified = self.simplify_response(response)
+
+ if "result" in simplified and isinstance(simplified["result"], dict):
+ result = simplified["result"]
+ memory_info = {
+ "success": True,
+ "address": result.get("address", address),
+ "length": result.get("bytesRead", length),
+ "format": format,
+ }
+
+ if "hexBytes" in result:
+ memory_info["hexBytes"] = result["hexBytes"]
+ if "rawBytes" in result:
+ memory_info["rawBytes"] = result["rawBytes"]
+
+ return memory_info
+
+ return simplified
+
+ @mcp_tool()
+ def memory_write(
+ self,
+ address: str,
+ bytes_data: str,
+ format: str = "hex",
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Write bytes to memory (use with caution).
+
+ Args:
+ address: Memory address in hex format
+ bytes_data: Data to write (format depends on 'format' parameter)
+ format: Input format - "hex", "base64", or "string" (default: "hex")
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not address:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Address parameter is required",
+ },
+ }
+
+ if not bytes_data:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Bytes parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {
+ "bytes": bytes_data,
+ "format": format,
+ }
+
+ response = self.safe_patch(port, f"programs/current/memory/{address}", payload)
+ return self.simplify_response(response)
diff --git a/src/ghydramcp/mixins/structs.py b/src/ghydramcp/mixins/structs.py
new file mode 100644
index 0000000..28fda14
--- /dev/null
+++ b/src/ghydramcp/mixins/structs.py
@@ -0,0 +1,419 @@
+"""Structs mixin for GhydraMCP.
+
+Provides tools for struct data type operations.
+"""
+
+from typing import Any, Dict, Optional
+
+from fastmcp import Context
+from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
+
+from .base import GhydraMixinBase
+from ..config import get_config
+
+
+class StructsMixin(GhydraMixinBase):
+ """Mixin for struct operations.
+
+ Provides tools for:
+ - Listing and searching structs
+ - Getting struct details with field pagination
+ - Creating and modifying structs
+ - Managing struct fields
+ """
+
+ @mcp_tool()
+ def structs_list(
+ self,
+ category: Optional[str] = None,
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """List all struct data types with cursor-based pagination.
+
+ Args:
+ category: Filter by category path (e.g. "/winapi")
+ port: Ghidra instance port (optional)
+ page_size: Items per page (default: 50, max: 500)
+ grep: Regex pattern to filter struct names
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all results without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Structs with pagination metadata
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+
+ params = {"offset": 0, "limit": 10000}
+ if category:
+ params["category"] = category
+
+ response = self.safe_get(port, "structs", params)
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ all_structs = simplified.get("result", [])
+ if not isinstance(all_structs, list):
+ all_structs = []
+
+ query_params = {
+ "tool": "structs_list",
+ "port": port,
+ "category": category,
+ "grep": grep,
+ }
+ session_id = self._get_session_id(ctx)
+
+ return self.paginate_response(
+ data=all_structs,
+ query_params=query_params,
+ tool_name="structs_list",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ @mcp_tool()
+ def structs_get(
+ self,
+ name: str,
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """Get detailed information about a struct with field pagination.
+
+ Args:
+ name: Struct name
+ port: Ghidra instance port (optional)
+ page_size: Fields per page (default: 50, max: 500)
+ grep: Regex pattern to filter fields
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all fields without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Struct details with paginated fields
+ """
+ if not name:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Struct name parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+ session_id = self._get_session_id(ctx)
+
+ params = {"name": name}
+ response = self.safe_get(port, "structs", params)
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ result = simplified.get("result", simplified)
+
+ # Extract struct info and fields
+ struct_info = {}
+ fields = []
+
+ if isinstance(result, dict):
+ for key, value in result.items():
+ if key == "fields" and isinstance(value, list):
+ fields = value
+ else:
+ struct_info[key] = value
+
+ # If few fields and no grep, return as-is
+ if len(fields) <= 10 and not grep:
+ return simplified
+
+ query_params = {
+ "tool": "structs_get",
+ "port": port,
+ "name": name,
+ }
+
+ # Paginate fields
+ paginated = self.paginate_response(
+ data=fields,
+ query_params=query_params,
+ tool_name="structs_get",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ # Merge struct metadata with paginated fields
+ if paginated.get("success"):
+ paginated["struct_name"] = struct_info.get("name", name)
+ paginated["struct_size"] = struct_info.get("size", struct_info.get("length"))
+ paginated["struct_category"] = struct_info.get("category", struct_info.get("categoryPath"))
+ paginated["struct_description"] = struct_info.get("description")
+ paginated["fields"] = paginated.pop("result", [])
+
+ if "_message" in paginated:
+ paginated["_message"] = paginated["_message"].replace("items", "fields")
+
+ return paginated
+
+ @mcp_tool()
+ def structs_create(
+ self,
+ name: str,
+ category: Optional[str] = None,
+ description: Optional[str] = None,
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Create a new struct data type.
+
+ Args:
+ name: Name for the new struct
+ category: Category path (e.g. "/custom")
+ description: Optional description
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Created struct information
+ """
+ if not name:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Struct name parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {"name": name}
+ if category:
+ payload["category"] = category
+ if description:
+ payload["description"] = description
+
+ response = self.safe_post(port, "structs/create", payload)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def structs_add_field(
+ self,
+ struct_name: str,
+ field_name: str,
+ field_type: str,
+ offset: Optional[int] = None,
+ comment: Optional[str] = None,
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Add a field to an existing struct.
+
+ Args:
+ struct_name: Name of the struct to modify
+ field_name: Name for the new field
+ field_type: Data type for the field
+ offset: Specific offset (appends if not specified)
+ comment: Optional field comment
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not struct_name or not field_name or not field_type:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "struct_name, field_name, and field_type are required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {
+ "struct": struct_name,
+ "fieldName": field_name,
+ "fieldType": field_type,
+ }
+ if offset is not None:
+ payload["offset"] = offset
+ if comment:
+ payload["comment"] = comment
+
+ response = self.safe_post(port, "structs/addfield", payload)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def structs_update_field(
+ self,
+ struct_name: str,
+ field_name: Optional[str] = None,
+ field_offset: Optional[int] = None,
+ new_name: Optional[str] = None,
+ new_type: Optional[str] = None,
+ new_comment: Optional[str] = None,
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Update an existing field in a struct.
+
+ Args:
+ struct_name: Name of the struct to modify
+ field_name: Name of the field to update (OR field_offset)
+ field_offset: Offset of the field to update (OR field_name)
+ new_name: New name for the field
+ new_type: New data type for the field
+ new_comment: New comment for the field
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not struct_name:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "struct_name parameter is required",
+ },
+ }
+
+ if not field_name and field_offset is None:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Either field_name or field_offset must be provided",
+ },
+ }
+
+ if not new_name and not new_type and new_comment is None:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "At least one of new_name, new_type, or new_comment required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {"struct": struct_name}
+ if field_name:
+ payload["fieldName"] = field_name
+ if field_offset is not None:
+ payload["fieldOffset"] = field_offset
+ if new_name:
+ payload["newName"] = new_name
+ if new_type:
+ payload["newType"] = new_type
+ if new_comment is not None:
+ payload["newComment"] = new_comment
+
+ response = self.safe_post(port, "structs/updatefield", payload)
+ return self.simplify_response(response)
+
+ @mcp_tool()
+ def structs_delete(
+ self,
+ name: str,
+ port: Optional[int] = None,
+ ) -> Dict[str, Any]:
+ """Delete a struct data type.
+
+ Args:
+ name: Name of the struct to delete
+ port: Ghidra instance port (optional)
+
+ Returns:
+ Operation result
+ """
+ if not name:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Struct name parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ payload = {"name": name}
+ response = self.safe_post(port, "structs/delete", payload)
+ return self.simplify_response(response)
+
+ # Resources
+
+ @mcp_resource(uri="ghidra://instance/{port}/structs")
+ def resource_structs_list(self, port: Optional[int] = None) -> Dict[str, Any]:
+ """MCP Resource: List structs (capped).
+
+ Args:
+ port: Ghidra instance port
+
+ Returns:
+ List of structs (capped at 1000)
+ """
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"error": str(e)}
+
+ config = get_config()
+ cap = config.resource_caps.get("structs", 1000)
+
+ response = self.safe_get(port, "structs", {"limit": cap})
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ structs = simplified.get("result", [])
+ if not isinstance(structs, list):
+ structs = []
+
+ return {
+ "structs": structs[:cap],
+ "count": len(structs),
+ "capped_at": cap if len(structs) >= cap else None,
+ "_hint": "Use structs_list() tool for full pagination" if len(structs) >= cap else None,
+ }
diff --git a/src/ghydramcp/mixins/xrefs.py b/src/ghydramcp/mixins/xrefs.py
new file mode 100644
index 0000000..1d17c21
--- /dev/null
+++ b/src/ghydramcp/mixins/xrefs.py
@@ -0,0 +1,196 @@
+"""Cross-references mixin for GhydraMCP.
+
+Provides tools for cross-reference (xref) operations.
+"""
+
+from typing import Any, Dict, Optional
+
+from fastmcp import Context
+from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
+
+from .base import GhydraMixinBase
+from ..config import get_config
+
+
+class XrefsMixin(GhydraMixinBase):
+ """Mixin for cross-reference operations.
+
+ Provides tools for:
+ - Listing references to an address
+ - Listing references from an address
+ - Filtering by reference type
+ """
+
+ @mcp_tool()
+ def xrefs_list(
+ self,
+ to_addr: Optional[str] = None,
+ from_addr: Optional[str] = None,
+ type: Optional[str] = None,
+ port: Optional[int] = None,
+ page_size: int = 50,
+ grep: Optional[str] = None,
+ grep_ignorecase: bool = True,
+ return_all: bool = False,
+ ctx: Optional[Context] = None,
+ ) -> Dict[str, Any]:
+ """List cross-references with filtering and pagination.
+
+ Args:
+ to_addr: Filter references to this address (hex)
+ from_addr: Filter references from this address (hex)
+ type: Filter by reference type ("CALL", "READ", "WRITE", etc.)
+ port: Ghidra instance port (optional)
+ page_size: Items per page (default: 50, max: 500)
+ grep: Regex pattern to filter results
+ grep_ignorecase: Case-insensitive grep (default: True)
+ return_all: Return all results without pagination
+ ctx: FastMCP context (auto-injected)
+
+ Returns:
+ Cross-references with pagination metadata
+ """
+ if not to_addr and not from_addr:
+ return {
+ "success": False,
+ "error": {
+ "code": "MISSING_PARAMETER",
+ "message": "Either to_addr or from_addr parameter is required",
+ },
+ }
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
+
+ config = get_config()
+
+ params = {"offset": 0, "limit": 10000}
+ if to_addr:
+ params["to_addr"] = to_addr
+ if from_addr:
+ params["from_addr"] = from_addr
+ if type:
+ params["type"] = type
+
+ response = self.safe_get(port, "xrefs", params)
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ all_xrefs = simplified.get("result", [])
+ if not isinstance(all_xrefs, list):
+ all_xrefs = []
+
+ query_params = {
+ "tool": "xrefs_list",
+ "port": port,
+ "to_addr": to_addr,
+ "from_addr": from_addr,
+ "type": type,
+ "grep": grep,
+ }
+ session_id = self._get_session_id(ctx)
+
+ return self.paginate_response(
+ data=all_xrefs,
+ query_params=query_params,
+ tool_name="xrefs_list",
+ session_id=session_id,
+ page_size=min(page_size, config.max_page_size),
+ grep=grep,
+ grep_ignorecase=grep_ignorecase,
+ return_all=return_all,
+ )
+
+ # Resources
+
+ @mcp_resource(uri="ghidra://instance/{port}/xrefs/to/{address}")
+ def resource_xrefs_to(
+ self,
+ port: Optional[int] = None,
+ address: Optional[str] = None,
+ ) -> Dict[str, Any]:
+ """MCP Resource: Get references to an address (capped).
+
+ Args:
+ port: Ghidra instance port
+ address: Target address
+
+ Returns:
+ References to the address (capped at 1000)
+ """
+ if not address:
+ return {"error": "Address is required"}
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"error": str(e)}
+
+ config = get_config()
+ cap = config.resource_caps.get("xrefs", 1000)
+
+ response = self.safe_get(port, "xrefs", {"to_addr": address, "limit": cap})
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ xrefs = simplified.get("result", [])
+ if not isinstance(xrefs, list):
+ xrefs = []
+
+ return {
+ "address": address,
+ "xrefs_to": xrefs[:cap],
+ "count": len(xrefs),
+ "capped_at": cap if len(xrefs) >= cap else None,
+ "_hint": "Use xrefs_list(to_addr=...) for full pagination" if len(xrefs) >= cap else None,
+ }
+
+ @mcp_resource(uri="ghidra://instance/{port}/xrefs/from/{address}")
+ def resource_xrefs_from(
+ self,
+ port: Optional[int] = None,
+ address: Optional[str] = None,
+ ) -> Dict[str, Any]:
+ """MCP Resource: Get references from an address (capped).
+
+ Args:
+ port: Ghidra instance port
+ address: Source address
+
+ Returns:
+ References from the address (capped at 1000)
+ """
+ if not address:
+ return {"error": "Address is required"}
+
+ try:
+ port = self.get_instance_port(port)
+ except ValueError as e:
+ return {"error": str(e)}
+
+ config = get_config()
+ cap = config.resource_caps.get("xrefs", 1000)
+
+ response = self.safe_get(port, "xrefs", {"from_addr": address, "limit": cap})
+ simplified = self.simplify_response(response)
+
+ if not simplified.get("success", True):
+ return simplified
+
+ xrefs = simplified.get("result", [])
+ if not isinstance(xrefs, list):
+ xrefs = []
+
+ return {
+ "address": address,
+ "xrefs_from": xrefs[:cap],
+ "count": len(xrefs),
+ "capped_at": cap if len(xrefs) >= cap else None,
+ "_hint": "Use xrefs_list(from_addr=...) for full pagination" if len(xrefs) >= cap else None,
+ }
diff --git a/src/ghydramcp/server.py b/src/ghydramcp/server.py
new file mode 100644
index 0000000..e3e6f2d
--- /dev/null
+++ b/src/ghydramcp/server.py
@@ -0,0 +1,183 @@
+"""GhydraMCP Server - FastMCP server composing all mixins.
+
+This module creates and configures the FastMCP server by composing
+all domain-specific mixins into a single MCP server.
+"""
+
+import signal
+import sys
+import threading
+import time
+from typing import Optional
+
+from fastmcp import FastMCP
+
+from .config import get_config, set_config, GhydraConfig
+from .mixins import (
+ InstancesMixin,
+ FunctionsMixin,
+ DataMixin,
+ StructsMixin,
+ AnalysisMixin,
+ MemoryMixin,
+ XrefsMixin,
+ CursorsMixin,
+ DockerMixin,
+)
+
+
+def create_server(
+ name: str = "GhydraMCP",
+ config: Optional[GhydraConfig] = None,
+) -> FastMCP:
+ """Create and configure the GhydraMCP server.
+
+ Args:
+ name: Server name
+ config: Optional configuration override
+
+ Returns:
+ Configured FastMCP server instance
+ """
+ if config:
+ set_config(config)
+
+ # Create the FastMCP server
+ mcp = FastMCP(name)
+
+ # Instantiate all mixins
+ instances_mixin = InstancesMixin()
+ functions_mixin = FunctionsMixin()
+ data_mixin = DataMixin()
+ structs_mixin = StructsMixin()
+ analysis_mixin = AnalysisMixin()
+ memory_mixin = MemoryMixin()
+ xrefs_mixin = XrefsMixin()
+ cursors_mixin = CursorsMixin()
+ docker_mixin = DockerMixin()
+
+ # Register all mixins with the server
+ # Each mixin registers its tools, resources, and prompts
+ instances_mixin.register_all(mcp)
+ functions_mixin.register_all(mcp)
+ data_mixin.register_all(mcp)
+ structs_mixin.register_all(mcp)
+ analysis_mixin.register_all(mcp)
+ memory_mixin.register_all(mcp)
+ xrefs_mixin.register_all(mcp)
+ cursors_mixin.register_all(mcp)
+ docker_mixin.register_all(mcp)
+
+ return mcp
+
+
+def _periodic_discovery(interval: int = 30):
+ """Background thread for periodic instance discovery.
+
+ Args:
+ interval: Seconds between discovery attempts
+ """
+ from .mixins.base import GhydraMixinBase
+ from .core.http_client import safe_get
+
+ config = get_config()
+
+ while True:
+ time.sleep(interval)
+ try:
+ # Quick scan of common ports
+ for port in config.quick_discovery_range:
+ try:
+ response = safe_get(port, "")
+ if response.get("success", False):
+ with GhydraMixinBase._instances_lock:
+ if port not in GhydraMixinBase._instances:
+ GhydraMixinBase._instances[port] = {
+ "url": f"http://{config.ghidra_host}:{port}",
+ "project": response.get("project", ""),
+ "file": response.get("file", ""),
+ "discovered_at": time.time(),
+ }
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+
+def _handle_sigint(signum, frame):
+ """Handle SIGINT gracefully."""
+ print("\nShutting down GhydraMCP...", file=sys.stderr)
+ sys.exit(0)
+
+
+def main():
+ """Main entry point for the GhydraMCP server."""
+ import shutil
+
+ try:
+ from importlib.metadata import version
+ package_version = version("ghydramcp")
+ except Exception:
+ package_version = "2025.12.1"
+
+ print(f"🔬 GhydraMCP v{package_version}", file=sys.stderr)
+ print(" AI-assisted reverse engineering bridge for Ghidra", file=sys.stderr)
+
+ # Check Docker availability
+ docker_available = shutil.which("docker") is not None
+ if docker_available:
+ print(" 🐳 Docker available (use docker_* tools for container management)", file=sys.stderr)
+ else:
+ print(" ⚠ Docker not found (container management disabled)", file=sys.stderr)
+
+ config = get_config()
+
+ # Create and configure the server
+ mcp = create_server()
+
+ # Initial instance discovery
+ print(f" Discovering Ghidra instances on {config.ghidra_host}...", file=sys.stderr)
+
+ from .mixins.base import GhydraMixinBase
+ from .core.http_client import safe_get
+
+ found = 0
+ for port in config.quick_discovery_range:
+ try:
+ response = safe_get(port, "")
+ if response.get("success", False):
+ GhydraMixinBase._instances[port] = {
+ "url": f"http://{config.ghidra_host}:{port}",
+ "project": response.get("project", ""),
+ "file": response.get("file", ""),
+ "discovered_at": time.time(),
+ }
+ found += 1
+ print(f" ✓ Found instance on port {port}", file=sys.stderr)
+ except Exception:
+ pass
+
+ if found == 0:
+ print(" ⚠ No Ghidra instances found (they can be discovered later)", file=sys.stderr)
+ else:
+ print(f" Found {found} Ghidra instance(s)", file=sys.stderr)
+
+ # Start background discovery thread
+ discovery_thread = threading.Thread(
+ target=_periodic_discovery,
+ daemon=True,
+ name="GhydraMCP-Discovery",
+ )
+ discovery_thread.start()
+
+ # Set up signal handler
+ signal.signal(signal.SIGINT, _handle_sigint)
+
+ print(" Starting MCP server...", file=sys.stderr)
+
+ # Run the server
+ mcp.run(transport="stdio")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..e6e07e4
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,1172 @@
+version = 1
+revision = 3
+requires-python = ">=3.11"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "authlib"
+version = "1.6.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
+]
+
+[[package]]
+name = "beartype"
+version = "0.22.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/1d/794ae2acaa67c8b216d91d5919da2606c2bb14086849ffde7f5555f3a3a5/beartype-0.22.8.tar.gz", hash = "sha256:b19b21c9359722ee3f7cc433f063b3e13997b27ae8226551ea5062e621f61165", size = 1602262, upload-time = "2025-12-03T05:11:10.766Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/2a/fbcbf5a025d3e71ddafad7efd43e34ec4362f4d523c3c471b457148fb211/beartype-0.22.8-py3-none-any.whl", hash = "sha256:b832882d04e41a4097bab9f63e6992bc6de58c414ee84cba9b45b67314f5ab2e", size = 1331895, upload-time = "2025-12-03T05:11:08.373Z" },
+]
+
+[[package]]
+name = "cachetools"
+version = "6.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.11.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
+ { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
+ { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
+ { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
+ { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
+ { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
+ { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
+ { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
+ { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
+ { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
+ { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
+ { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
+ { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
+ { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" },
+ { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
+ { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
+]
+
+[[package]]
+name = "cyclopts"
+version = "4.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "docstring-parser" },
+ { name = "rich" },
+ { name = "rich-rst" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1b/0f/fe026df2ab8301e30a2b0bd425ff1462ad858fd4f991c1ac0389c2059c24/cyclopts-4.3.0.tar.gz", hash = "sha256:e95179cd0a959ce250ecfb2f0262a5996a92c1f9467bccad2f3d829e6833cef5", size = 151411, upload-time = "2025-11-25T02:59:33.572Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7a/e8/77a231ae531cf38765b75ddf27dae28bb5f70b41d8bb4f15ce1650e93f57/cyclopts-4.3.0-py3-none-any.whl", hash = "sha256:91a30b69faf128ada7cfeaefd7d9649dc222e8b2a8697f1fc99e4ee7b7ca44f3", size = 187184, upload-time = "2025-11-25T02:59:32.21Z" },
+]
+
+[[package]]
+name = "diskcache"
+version = "5.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" },
+]
+
+[[package]]
+name = "dnspython"
+version = "2.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
+]
+
+[[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.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d9/02/111134bfeb6e6c7ac4c74594e39a59f6c0195dc4846afbeac3cba60f1927/docutils-0.22.3.tar.gz", hash = "sha256:21486ae730e4ca9f622677b1412b879af1791efcfba517e4c6f60be543fc8cdd", size = 2290153, upload-time = "2025-11-06T02:35:55.655Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" },
+]
+
+[[package]]
+name = "email-validator"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dnspython" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "fastmcp"
+version = "2.13.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "authlib" },
+ { name = "cyclopts" },
+ { name = "exceptiongroup" },
+ { name = "httpx" },
+ { name = "jsonschema-path" },
+ { name = "mcp" },
+ { name = "openapi-pydantic" },
+ { name = "platformdirs" },
+ { name = "py-key-value-aio", extra = ["disk", "memory"] },
+ { name = "pydantic", extra = ["email"] },
+ { name = "pyperclip" },
+ { name = "python-dotenv" },
+ { name = "rich" },
+ { name = "uvicorn" },
+ { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c8/7a/4c6375a56f7458a4a6af62f4c4838a2c957a665cf5edad26fe95395666f1/fastmcp-2.13.2.tar.gz", hash = "sha256:2a206401a6579fea621974162674beba85b467ad72c70c1a3752a31951dff7f0", size = 8185950, upload-time = "2025-12-01T18:48:16.834Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/4b/73c68b0ae9e587f20c5aa13ba5bed9be2bb9248a598555dafcf17df87f70/fastmcp-2.13.2-py3-none-any.whl", hash = "sha256:300c59eb970c235bb9d0575883322922e4f2e2468a3d45e90cbfd6b23b7be245", size = 385643, upload-time = "2025-12-01T18:48:18.515Z" },
+]
+
+[[package]]
+name = "ghydramcp"
+version = "2025.12.3"
+source = { editable = "." }
+dependencies = [
+ { name = "fastmcp" },
+ { name = "mcp" },
+ { name = "requests" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fastmcp", specifier = ">=2.0.0" },
+ { name = "mcp", specifier = ">=1.22.0" },
+ { name = "requests", specifier = ">=2.32.3" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
+]
+
+[[package]]
+name = "jsonschema-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.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.23.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pyjwt", extra = ["crypto"] },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/12/42/10c0c09ca27aceacd8c428956cfabdd67e3d328fe55c4abc16589285d294/mcp-1.23.1.tar.gz", hash = "sha256:7403e053e8e2283b1e6ae631423cb54736933fea70b32422152e6064556cd298", size = 596519, upload-time = "2025-12-02T18:41:12.807Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/9e/26e1d2d2c6afe15dfba5ca6799eeeea7656dce625c22766e4c57305e9cc2/mcp-1.23.1-py3-none-any.whl", hash = "sha256:3ce897fcc20a41bd50b4c58d3aa88085f11f505dcc0eaed48930012d34c731d8", size = 231433, upload-time = "2025-12-02T18:41:11.195Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+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 = "openapi-pydantic"
+version = "0.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" }
+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 = "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 = "pathvalidate"
+version = "3.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
+]
+
+[[package]]
+name = "py-key-value-aio"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beartype" },
+ { name = "py-key-value-shared" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" },
+]
+
+[package.optional-dependencies]
+disk = [
+ { name = "diskcache" },
+ { name = "pathvalidate" },
+]
+memory = [
+ { name = "cachetools" },
+]
+
+[[package]]
+name = "py-key-value-shared"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beartype" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.23"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[package.optional-dependencies]
+email = [
+ { name = "email-validator" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+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 = "pyjwt"
+version = "2.10.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
+]
+
+[[package]]
+name = "pyperclip"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
+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/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
+ { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
+ { 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.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[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.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
+]
+
+[[package]]
+name = "rich-rst"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docutils" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+ { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+ { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+ { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.50.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
+]
+
+[[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/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
+ { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
+ { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
+ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
+ { 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" },
+]