Compare commits
3 Commits
2d837d95fc
...
290252c0db
| Author | SHA1 | Date | |
|---|---|---|---|
| 290252c0db | |||
| 70f226f68e | |||
| 4c112a2421 |
File diff suppressed because it is too large
Load Diff
197
docker/README.md
Normal file
197
docker/README.md
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
# GhydraMCP Docker Setup
|
||||||
|
|
||||||
|
This directory contains Docker configuration for running GhydraMCP in headless mode.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the image
|
||||||
|
docker build -t ghydramcp:latest -f docker/Dockerfile .
|
||||||
|
|
||||||
|
# Analyze a binary
|
||||||
|
docker run -p 8192:8192 -v /path/to/binaries:/binaries ghydramcp /binaries/sample.exe
|
||||||
|
|
||||||
|
# Check API health
|
||||||
|
curl http://localhost:8192/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The Docker container includes:
|
||||||
|
|
||||||
|
1. **Ghidra 11.4.2** - Full headless installation
|
||||||
|
2. **GhydraMCP Extension** - The Java plugin (installed in Extensions/)
|
||||||
|
3. **GhydraMCPServer.py** - Headless HTTP server (Jython, full API parity)
|
||||||
|
|
||||||
|
### Why Two HTTP Servers?
|
||||||
|
|
||||||
|
The GhydraMCP plugin (`GhydraMCPPlugin.java`) is a full Ghidra GUI plugin that requires:
|
||||||
|
- Ghidra's `PluginTool` framework
|
||||||
|
- `ProgramManager` service for program access
|
||||||
|
- GUI event handling
|
||||||
|
|
||||||
|
These GUI services don't exist in headless mode. Instead, the container uses `GhydraMCPServer.py`, a Jython script that:
|
||||||
|
- Runs via `analyzeHeadless -postScript`
|
||||||
|
- Has direct access to `currentProgram` from the script context
|
||||||
|
- Provides **full API parity** with the GUI plugin (45 routes)
|
||||||
|
- Supports all read and write operations
|
||||||
|
|
||||||
|
### Available Endpoints (Headless Mode)
|
||||||
|
|
||||||
|
The headless server implements the complete GhydraMCP HTTP API:
|
||||||
|
|
||||||
|
| Category | Endpoints | Description |
|
||||||
|
|----------|-----------|-------------|
|
||||||
|
| **Info** | `GET /`, `/info`, `/program` | API info, program metadata |
|
||||||
|
| **Functions** | `GET /functions`, `/functions/{addr}`, `/functions/by-name/{name}` | List and detail |
|
||||||
|
| **Decompile** | `GET /functions/{addr}/decompile`, `/functions/by-name/{name}/decompile` | C pseudocode |
|
||||||
|
| **Disassembly** | `GET /functions/{addr}/disassembly`, `/functions/by-name/{name}/disassembly` | Assembly listing |
|
||||||
|
| **Data** | `GET /data`, `/strings` | Defined data and strings |
|
||||||
|
| **Memory** | `GET /memory`, `/memory/blocks` | Read bytes, list segments |
|
||||||
|
| **Xrefs** | `GET /xrefs` | Cross-references (to/from) |
|
||||||
|
| **Structs** | `GET /structs` | Data type structures |
|
||||||
|
| **Symbols** | `GET /symbols`, `/imports`, `/exports` | Symbol tables |
|
||||||
|
| **Analysis** | `GET /analysis/callgraph`, `/analysis/dataflow` | Static analysis |
|
||||||
|
| **Write Ops** | `PATCH /functions/*`, `POST /data`, `POST /structs/*` | Rename, annotate, create |
|
||||||
|
|
||||||
|
See [GHIDRA_HTTP_API.md](../GHIDRA_HTTP_API.md) for the complete API specification.
|
||||||
|
|
||||||
|
## Container Modes
|
||||||
|
|
||||||
|
### Headless Mode (Default)
|
||||||
|
|
||||||
|
Imports a binary, analyzes it, and starts the HTTP API server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8192:8192 \
|
||||||
|
-v ./samples:/binaries \
|
||||||
|
ghydramcp /binaries/sample.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Mode
|
||||||
|
|
||||||
|
Opens an existing project and program:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8192:8192 \
|
||||||
|
-e GHYDRA_MODE=server \
|
||||||
|
-v ./projects:/projects \
|
||||||
|
ghydramcp program_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analyze Mode
|
||||||
|
|
||||||
|
Imports and analyzes without starting HTTP server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run \
|
||||||
|
-e GHYDRA_MODE=analyze \
|
||||||
|
-v ./samples:/binaries \
|
||||||
|
-v ./projects:/projects \
|
||||||
|
ghydramcp /binaries/sample.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell Mode
|
||||||
|
|
||||||
|
Interactive debugging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -it \
|
||||||
|
-e GHYDRA_MODE=shell \
|
||||||
|
ghydramcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `GHYDRA_MODE` | `headless` | Container mode (headless, server, analyze, shell) |
|
||||||
|
| `GHYDRA_PORT` | `8192` | HTTP API port |
|
||||||
|
| `GHYDRA_MAXMEM` | `2G` | JVM heap memory |
|
||||||
|
| `PROJECT_DIR` | `/projects` | Ghidra project directory |
|
||||||
|
| `PROJECT_NAME` | `GhydraMCP` | Ghidra project name |
|
||||||
|
|
||||||
|
## Docker Compose
|
||||||
|
|
||||||
|
Use docker-compose for easier management:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development mode (hot-reload scripts)
|
||||||
|
docker compose --profile dev up ghydramcp-dev
|
||||||
|
|
||||||
|
# Production mode
|
||||||
|
docker compose --profile prod up ghydramcp
|
||||||
|
|
||||||
|
# Interactive shell
|
||||||
|
docker compose --profile debug run --rm ghydramcp-shell
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Integration
|
||||||
|
|
||||||
|
The GhydraMCP Python server includes Docker management tools:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Check Docker status
|
||||||
|
await docker_status()
|
||||||
|
|
||||||
|
# Start container for a binary
|
||||||
|
await docker_start(binary_path="/path/to/binary.exe", port=8192)
|
||||||
|
|
||||||
|
# Wait for container to be ready
|
||||||
|
await docker_wait(port=8192, timeout=300)
|
||||||
|
|
||||||
|
# Automatic mode - starts container if no Ghidra available
|
||||||
|
await docker_auto_start(binary_path="/path/to/binary.exe")
|
||||||
|
|
||||||
|
# Get container logs
|
||||||
|
await docker_logs("ghydramcp-server")
|
||||||
|
|
||||||
|
# Stop container
|
||||||
|
await docker_stop("ghydramcp-server")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Make
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Using Docker directly
|
||||||
|
docker build -t ghydramcp:latest -f docker/Dockerfile .
|
||||||
|
|
||||||
|
# Build with specific Ghidra version
|
||||||
|
docker build -t ghydramcp:latest \
|
||||||
|
--build-arg GHIDRA_VERSION=11.4.2 \
|
||||||
|
--build-arg GHIDRA_DATE=20250826 \
|
||||||
|
-f docker/Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container starts but API doesn't respond
|
||||||
|
|
||||||
|
Analysis takes time. Monitor progress with:
|
||||||
|
```bash
|
||||||
|
docker logs -f ghydramcp-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port already in use
|
||||||
|
|
||||||
|
Stop existing containers:
|
||||||
|
```bash
|
||||||
|
docker stop $(docker ps -q --filter "name=ghydramcp")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory issues with large binaries
|
||||||
|
|
||||||
|
Increase JVM heap:
|
||||||
|
```bash
|
||||||
|
docker run -e GHYDRA_MAXMEM=4G -p 8192:8192 ghydramcp /binaries/large.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission denied on volumes
|
||||||
|
|
||||||
|
The container runs as user `ghidra` (UID 1001). Ensure volume permissions:
|
||||||
|
```bash
|
||||||
|
sudo chown -R 1001:1001 /path/to/binaries
|
||||||
|
```
|
||||||
@ -11,6 +11,7 @@ dependencies = [
|
|||||||
"mcp>=1.22.0",
|
"mcp>=1.22.0",
|
||||||
"requests>=2.32.3",
|
"requests>=2.32.3",
|
||||||
"fastmcp>=2.0.0",
|
"fastmcp>=2.0.0",
|
||||||
|
"fastmcp-feedback>=1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@ -72,9 +72,24 @@ class GhydraConfig:
|
|||||||
cursor_ttl_seconds: int = 300 # 5 minutes
|
cursor_ttl_seconds: int = 300 # 5 minutes
|
||||||
max_cursors_per_session: int = 100
|
max_cursors_per_session: int = 100
|
||||||
|
|
||||||
|
# Response size limits (for return_all guard)
|
||||||
|
max_response_tokens: int = 8000 # Hard budget — guard triggers above this
|
||||||
|
large_response_threshold: int = 4000 # Warn above this in normal pagination
|
||||||
|
|
||||||
# Expected API version
|
# Expected API version
|
||||||
expected_api_version: int = 2
|
expected_api_version: int = 2
|
||||||
|
|
||||||
|
# Feedback collection
|
||||||
|
feedback_enabled: bool = field(
|
||||||
|
default_factory=lambda: os.environ.get("GHYDRA_FEEDBACK", "true").lower() == "true"
|
||||||
|
)
|
||||||
|
feedback_db_path: str = field(
|
||||||
|
default_factory=lambda: os.environ.get(
|
||||||
|
"GHYDRA_FEEDBACK_DB",
|
||||||
|
str(Path.home() / ".ghydramcp" / "feedback.db"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Resource caps for enumeration endpoints
|
# Resource caps for enumeration endpoints
|
||||||
resource_caps: dict = field(default_factory=lambda: {
|
resource_caps: dict = field(default_factory=lambda: {
|
||||||
"functions": 1000,
|
"functions": 1000,
|
||||||
|
|||||||
@ -24,6 +24,11 @@ from .progress import (
|
|||||||
report_progress,
|
report_progress,
|
||||||
report_step,
|
report_step,
|
||||||
)
|
)
|
||||||
|
from .filtering import (
|
||||||
|
project_fields,
|
||||||
|
apply_grep,
|
||||||
|
estimate_and_guard,
|
||||||
|
)
|
||||||
from .logging import (
|
from .logging import (
|
||||||
log_info,
|
log_info,
|
||||||
log_debug,
|
log_debug,
|
||||||
@ -50,6 +55,10 @@ __all__ = [
|
|||||||
"ProgressReporter",
|
"ProgressReporter",
|
||||||
"report_progress",
|
"report_progress",
|
||||||
"report_step",
|
"report_step",
|
||||||
|
# Filtering
|
||||||
|
"project_fields",
|
||||||
|
"apply_grep",
|
||||||
|
"estimate_and_guard",
|
||||||
# Logging
|
# Logging
|
||||||
"log_info",
|
"log_info",
|
||||||
"log_debug",
|
"log_debug",
|
||||||
|
|||||||
208
src/ghydramcp/core/filtering.py
Normal file
208
src/ghydramcp/core/filtering.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
"""Field projection and response size guard for GhydraMCP.
|
||||||
|
|
||||||
|
Provides jq-style field projection, grep filtering, and token budget
|
||||||
|
enforcement to prevent oversized MCP tool results.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from ..config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
# Token estimation (same ratio as pagination.py)
|
||||||
|
TOKEN_ESTIMATION_RATIO = 4.0
|
||||||
|
|
||||||
|
|
||||||
|
def project_fields(items: list, fields: list[str]) -> list:
|
||||||
|
"""Select only specified keys from each item (jq-style projection).
|
||||||
|
|
||||||
|
Works on dicts and strings. For dicts, returns only the requested
|
||||||
|
keys. For non-dict items (e.g. lines of decompiled code), returns
|
||||||
|
them unchanged.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
items: List of items to project
|
||||||
|
fields: List of field names to keep
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of projected items
|
||||||
|
"""
|
||||||
|
if not fields or not items:
|
||||||
|
return items
|
||||||
|
|
||||||
|
field_set = set(fields)
|
||||||
|
projected = []
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
projected.append({k: v for k, v in item.items() if k in field_set})
|
||||||
|
else:
|
||||||
|
projected.append(item)
|
||||||
|
return projected
|
||||||
|
|
||||||
|
|
||||||
|
def apply_grep(items: list, pattern: str, ignorecase: bool = True) -> list:
|
||||||
|
"""Filter items by regex pattern across all string values.
|
||||||
|
|
||||||
|
Searches all string-coercible values in each item. For dicts,
|
||||||
|
searches all values recursively. For strings, searches directly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
items: List of items to filter
|
||||||
|
pattern: Regex pattern string
|
||||||
|
ignorecase: Case-insensitive matching (default True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered list of matching items
|
||||||
|
"""
|
||||||
|
if not pattern or not items:
|
||||||
|
return items
|
||||||
|
|
||||||
|
flags = re.IGNORECASE if ignorecase else 0
|
||||||
|
compiled = re.compile(pattern, flags)
|
||||||
|
|
||||||
|
return [item for item in items if _matches(item, compiled)]
|
||||||
|
|
||||||
|
|
||||||
|
def _matches(item: Any, pattern: re.Pattern, depth: int = 0) -> bool:
|
||||||
|
"""Check if item matches pattern (recursive for nested structures)."""
|
||||||
|
if depth > 10:
|
||||||
|
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, list, tuple)):
|
||||||
|
if _matches(value, pattern, depth + 1):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
elif isinstance(item, (list, tuple)):
|
||||||
|
return any(_matches(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 _estimate_tokens(data: Any) -> int:
|
||||||
|
"""Estimate token count from serialized JSON size."""
|
||||||
|
text = json.dumps(data, default=str)
|
||||||
|
return int(len(text) / TOKEN_ESTIMATION_RATIO)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_available_fields(items: list) -> list[str]:
|
||||||
|
"""Extract the set of field names from the first few dict items."""
|
||||||
|
fields = set()
|
||||||
|
for item in items[:5]:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
fields.update(item.keys())
|
||||||
|
# Remove internal/HATEOAS fields
|
||||||
|
fields.discard("_links")
|
||||||
|
return sorted(fields)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_and_guard(
|
||||||
|
data: list,
|
||||||
|
tool_name: str,
|
||||||
|
budget: Optional[int] = None,
|
||||||
|
query_hints: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Check if data exceeds token budget; return guard response if so.
|
||||||
|
|
||||||
|
If data fits within budget, returns None (caller should proceed
|
||||||
|
normally). If data exceeds budget, returns a structured summary
|
||||||
|
with instructions for narrowing the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: The full data list to check
|
||||||
|
tool_name: Name of the tool (for hint messages)
|
||||||
|
budget: Token budget override (defaults to config.max_response_tokens)
|
||||||
|
query_hints: Original query params (for building hint commands)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None if data fits within budget, or a guard response dict
|
||||||
|
"""
|
||||||
|
config = get_config()
|
||||||
|
if budget is None:
|
||||||
|
budget = config.max_response_tokens
|
||||||
|
|
||||||
|
estimated = _estimate_tokens(data)
|
||||||
|
if estimated <= budget:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Build sample from first 3 items
|
||||||
|
sample = data[:3]
|
||||||
|
available_fields = _extract_available_fields(data)
|
||||||
|
|
||||||
|
# Build actionable hints based on the tool name
|
||||||
|
hints = _build_hints(tool_name, available_fields, query_hints)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"guarded": True,
|
||||||
|
"total_count": len(data),
|
||||||
|
"estimated_tokens": estimated,
|
||||||
|
"budget": budget,
|
||||||
|
"sample": sample,
|
||||||
|
"available_fields": available_fields,
|
||||||
|
"message": (
|
||||||
|
"Response too large (%d items, ~%s tokens, budget: %s). "
|
||||||
|
"To read this data:\n%s"
|
||||||
|
) % (
|
||||||
|
len(data),
|
||||||
|
_format_tokens(estimated),
|
||||||
|
_format_tokens(budget),
|
||||||
|
hints,
|
||||||
|
),
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _format_tokens(n: int) -> str:
|
||||||
|
"""Format token count for display (e.g. 45000 -> '45k')."""
|
||||||
|
if n >= 1000:
|
||||||
|
return "%dk" % (n // 1000)
|
||||||
|
return str(n)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_hints(
|
||||||
|
tool_name: str,
|
||||||
|
available_fields: list[str],
|
||||||
|
query_hints: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Build actionable hint text for the guard message."""
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
# Pagination hint
|
||||||
|
lines.append(
|
||||||
|
" - Paginate: %s(page_size=50) then cursor_next(cursor_id='...')"
|
||||||
|
% tool_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Grep hint
|
||||||
|
grep_example = "main" if "functions" in tool_name else ".*pattern.*"
|
||||||
|
lines.append(
|
||||||
|
" - Filter: %s(grep='%s')" % (tool_name, grep_example)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fields hint (only if we have dict items with fields)
|
||||||
|
if available_fields:
|
||||||
|
short_fields = available_fields[:2]
|
||||||
|
lines.append(
|
||||||
|
" - Project: %s(fields=%s)" % (tool_name, short_fields)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combined hint
|
||||||
|
if available_fields:
|
||||||
|
lines.append(
|
||||||
|
" - Combine: %s(grep='...', fields=%s, return_all=True)"
|
||||||
|
% (tool_name, available_fields[:2])
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
@ -14,6 +14,7 @@ from threading import Lock
|
|||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from ..config import get_config
|
from ..config import get_config
|
||||||
|
from .filtering import project_fields, estimate_and_guard
|
||||||
|
|
||||||
|
|
||||||
# ReDoS Protection Configuration
|
# ReDoS Protection Configuration
|
||||||
@ -393,8 +394,9 @@ def paginate_response(
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Create a paginated response with optional grep filtering.
|
"""Create a paginated response with optional grep filtering and field projection.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Full result list to paginate
|
data: Full result list to paginate
|
||||||
@ -404,7 +406,8 @@ def paginate_response(
|
|||||||
page_size: Items per page (default: 50, max: 500)
|
page_size: Items per page (default: 50, max: 500)
|
||||||
grep: Optional regex pattern to filter results
|
grep: Optional regex pattern to filter results
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Bypass pagination and return all results (with warning)
|
return_all: Bypass pagination and return all results (with budget guard)
|
||||||
|
fields: Optional list of field names to project (jq-style)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict with pagination metadata and results
|
dict with pagination metadata and results
|
||||||
@ -431,6 +434,19 @@ def paginate_response(
|
|||||||
"timestamp": int(time.time() * 1000),
|
"timestamp": int(time.time() * 1000),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Apply field projection before size estimation
|
||||||
|
if fields:
|
||||||
|
filtered_data = project_fields(filtered_data, fields)
|
||||||
|
|
||||||
|
# Check token budget — return guard if exceeded
|
||||||
|
guard = estimate_and_guard(
|
||||||
|
data=filtered_data,
|
||||||
|
tool_name=tool_name,
|
||||||
|
query_hints=query_params,
|
||||||
|
)
|
||||||
|
if guard is not None:
|
||||||
|
return guard
|
||||||
|
|
||||||
estimated_tokens = estimate_tokens(filtered_data)
|
estimated_tokens = estimate_tokens(filtered_data)
|
||||||
warning = None
|
warning = None
|
||||||
|
|
||||||
@ -438,7 +454,7 @@ def paginate_response(
|
|||||||
warning = f"EXTREMELY LARGE response (~{estimated_tokens:,} tokens)"
|
warning = f"EXTREMELY LARGE response (~{estimated_tokens:,} tokens)"
|
||||||
elif estimated_tokens > 20000:
|
elif estimated_tokens > 20000:
|
||||||
warning = f"VERY LARGE response (~{estimated_tokens:,} tokens)"
|
warning = f"VERY LARGE response (~{estimated_tokens:,} tokens)"
|
||||||
elif estimated_tokens > 8000:
|
elif estimated_tokens > config.large_response_threshold:
|
||||||
warning = f"Large response (~{estimated_tokens:,} tokens)"
|
warning = f"Large response (~{estimated_tokens:,} tokens)"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -449,16 +465,19 @@ def paginate_response(
|
|||||||
"total_count": len(data),
|
"total_count": len(data),
|
||||||
"filtered_count": len(filtered_data),
|
"filtered_count": len(filtered_data),
|
||||||
"grep_pattern": grep,
|
"grep_pattern": grep,
|
||||||
|
"fields_projected": fields,
|
||||||
"estimated_tokens": estimated_tokens,
|
"estimated_tokens": estimated_tokens,
|
||||||
"warning": warning,
|
"warning": warning,
|
||||||
},
|
},
|
||||||
"timestamp": int(time.time() * 1000),
|
"timestamp": int(time.time() * 1000),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Normal pagination flow
|
# Normal pagination flow — apply field projection before cursoring
|
||||||
|
paginated_data = project_fields(data, fields) if fields else data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor_id, state = cursor_manager.create_cursor(
|
cursor_id, state = cursor_manager.create_cursor(
|
||||||
data=data,
|
data=paginated_data,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name=tool_name,
|
tool_name=tool_name,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
Provides tools for program analysis operations.
|
Provides tools for program analysis operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
from fastmcp.contrib.mcp_mixin import mcp_tool
|
from fastmcp.contrib.mcp_mixin import mcp_tool
|
||||||
@ -57,6 +57,7 @@ class AnalysisMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Get function call graph with edge pagination.
|
"""Get function call graph with edge pagination.
|
||||||
@ -70,6 +71,7 @@ class AnalysisMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter edges
|
grep: Regex pattern to filter edges
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all edges without pagination
|
return_all: Return all edges without pagination
|
||||||
|
fields: Field names to keep per edge (e.g. ['from', 'to']). Reduces response size.
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -115,7 +117,7 @@ class AnalysisMixin(GhydraMixinBase):
|
|||||||
}
|
}
|
||||||
session_id = self._get_session_id(ctx)
|
session_id = self._get_session_id(ctx)
|
||||||
|
|
||||||
paginated = self.paginate_response(
|
paginated = self.filtered_paginate(
|
||||||
data=edges,
|
data=edges,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="analysis_get_callgraph",
|
tool_name="analysis_get_callgraph",
|
||||||
@ -124,9 +126,10 @@ class AnalysisMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
if paginated.get("success"):
|
if paginated.get("success") and not paginated.get("guarded"):
|
||||||
paginated["result"] = {
|
paginated["result"] = {
|
||||||
"root_function": func_id,
|
"root_function": func_id,
|
||||||
"max_depth": max_depth,
|
"max_depth": max_depth,
|
||||||
@ -148,6 +151,7 @@ class AnalysisMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Perform data flow analysis with step pagination.
|
"""Perform data flow analysis with step pagination.
|
||||||
@ -161,6 +165,7 @@ class AnalysisMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter steps
|
grep: Regex pattern to filter steps
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all steps without pagination
|
return_all: Return all steps without pagination
|
||||||
|
fields: Field names to keep per step. Reduces response size.
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -210,7 +215,7 @@ class AnalysisMixin(GhydraMixinBase):
|
|||||||
}
|
}
|
||||||
session_id = self._get_session_id(ctx)
|
session_id = self._get_session_id(ctx)
|
||||||
|
|
||||||
paginated = self.paginate_response(
|
paginated = self.filtered_paginate(
|
||||||
data=steps,
|
data=steps,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="analysis_get_dataflow",
|
tool_name="analysis_get_dataflow",
|
||||||
@ -219,9 +224,11 @@ class AnalysisMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
if paginated.get("success"):
|
# Merge metadata into result (skip if guarded)
|
||||||
|
if paginated.get("success") and not paginated.get("guarded"):
|
||||||
paginated["result"] = {
|
paginated["result"] = {
|
||||||
"start_address": address,
|
"start_address": address,
|
||||||
"direction": direction,
|
"direction": direction,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from fastmcp.contrib.mcp_mixin import MCPMixin
|
|||||||
|
|
||||||
from ..config import get_config
|
from ..config import get_config
|
||||||
from ..core.http_client import safe_get, safe_post, safe_put, safe_patch, safe_delete, simplify_response
|
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.pagination import paginate_response
|
||||||
from ..core.logging import log_info, log_debug, log_warning, log_error
|
from ..core.logging import log_info, log_debug, log_warning, log_error
|
||||||
|
|
||||||
|
|
||||||
@ -209,8 +209,9 @@ class GhydraMixinBase(MCPMixin):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[list] = None,
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
"""Create paginated response."""
|
"""Create paginated response with optional field projection."""
|
||||||
return paginate_response(
|
return paginate_response(
|
||||||
data=data,
|
data=data,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
@ -220,6 +221,37 @@ class GhydraMixinBase(MCPMixin):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
def filtered_paginate(
|
||||||
|
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,
|
||||||
|
fields: Optional[list] = None,
|
||||||
|
) -> Dict:
|
||||||
|
"""Paginate with field projection and budget guard.
|
||||||
|
|
||||||
|
Convenience wrapper that applies field projection then delegates
|
||||||
|
to paginate_response. Prefer this over paginate_response for any
|
||||||
|
tool that could return large result sets.
|
||||||
|
"""
|
||||||
|
return self.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,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Async logging helpers
|
# Async logging helpers
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
Provides tools for data items and strings operations.
|
Provides tools for data items and strings operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
|
from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
|
||||||
@ -34,6 +34,7 @@ class DataMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""List defined data items with filtering and cursor-based pagination.
|
"""List defined data items with filtering and cursor-based pagination.
|
||||||
@ -48,6 +49,7 @@ class DataMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter results
|
grep: Regex pattern to filter results
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all results without pagination
|
return_all: Return all results without pagination
|
||||||
|
fields: Field names to keep (e.g. ['address', 'name']). Reduces response size.
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -91,7 +93,7 @@ class DataMixin(GhydraMixinBase):
|
|||||||
}
|
}
|
||||||
session_id = self._get_session_id(ctx)
|
session_id = self._get_session_id(ctx)
|
||||||
|
|
||||||
return self.paginate_response(
|
return self.filtered_paginate(
|
||||||
data=all_data,
|
data=all_data,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="data_list",
|
tool_name="data_list",
|
||||||
@ -100,6 +102,7 @@ class DataMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
@mcp_tool()
|
@mcp_tool()
|
||||||
@ -111,6 +114,7 @@ class DataMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""List all defined strings in the binary with pagination.
|
"""List all defined strings in the binary with pagination.
|
||||||
@ -122,6 +126,7 @@ class DataMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter results (e.g., "password|key")
|
grep: Regex pattern to filter results (e.g., "password|key")
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all strings without pagination
|
return_all: Return all strings without pagination
|
||||||
|
fields: Field names to keep (e.g. ['value', 'address']). Reduces response size.
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -157,7 +162,7 @@ class DataMixin(GhydraMixinBase):
|
|||||||
}
|
}
|
||||||
session_id = self._get_session_id(ctx)
|
session_id = self._get_session_id(ctx)
|
||||||
|
|
||||||
return self.paginate_response(
|
return self.filtered_paginate(
|
||||||
data=result_data,
|
data=result_data,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="data_list_strings",
|
tool_name="data_list_strings",
|
||||||
@ -166,6 +171,7 @@ class DataMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
@mcp_tool()
|
@mcp_tool()
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
Provides tools for function analysis, decompilation, and manipulation.
|
Provides tools for function analysis, decompilation, and manipulation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
@ -33,6 +33,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""List functions with cursor-based pagination.
|
"""List functions with cursor-based pagination.
|
||||||
@ -43,6 +44,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter function names
|
grep: Regex pattern to filter function names
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all functions without pagination
|
return_all: Return all functions without pagination
|
||||||
|
fields: Field names to keep (e.g. ['name', 'address']). Reduces response size.
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -67,7 +69,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
query_params = {"tool": "functions_list", "port": port, "grep": grep}
|
query_params = {"tool": "functions_list", "port": port, "grep": grep}
|
||||||
session_id = self._get_session_id(ctx)
|
session_id = self._get_session_id(ctx)
|
||||||
|
|
||||||
return self.paginate_response(
|
return self.filtered_paginate(
|
||||||
data=functions,
|
data=functions,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="functions_list",
|
tool_name="functions_list",
|
||||||
@ -76,6 +78,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
@mcp_tool()
|
@mcp_tool()
|
||||||
@ -129,6 +132,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Get decompiled code for a function with line pagination.
|
"""Get decompiled code for a function with line pagination.
|
||||||
@ -143,6 +147,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter lines
|
grep: Regex pattern to filter lines
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all lines without pagination
|
return_all: Return all lines without pagination
|
||||||
|
fields: Field names to keep (for structured results)
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -194,7 +199,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
}
|
}
|
||||||
session_id = self._get_session_id(ctx)
|
session_id = self._get_session_id(ctx)
|
||||||
|
|
||||||
paginated = self.paginate_response(
|
paginated = self.filtered_paginate(
|
||||||
data=lines,
|
data=lines,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="functions_decompile",
|
tool_name="functions_decompile",
|
||||||
@ -203,10 +208,11 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert lines back to text in result
|
# Convert lines back to text in result (skip if guarded)
|
||||||
if paginated.get("success"):
|
if paginated.get("success") and not paginated.get("guarded"):
|
||||||
paginated["result"] = "\n".join(paginated.get("result", []))
|
paginated["result"] = "\n".join(paginated.get("result", []))
|
||||||
paginated["function_name"] = result.get("name", name or address)
|
paginated["function_name"] = result.get("name", name or address)
|
||||||
|
|
||||||
@ -222,6 +228,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Get disassembly for a function with instruction pagination.
|
"""Get disassembly for a function with instruction pagination.
|
||||||
@ -234,6 +241,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter instructions
|
grep: Regex pattern to filter instructions
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all instructions without pagination
|
return_all: Return all instructions without pagination
|
||||||
|
fields: Field names to keep (for structured results)
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -284,7 +292,7 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
}
|
}
|
||||||
session_id = self._get_session_id(ctx)
|
session_id = self._get_session_id(ctx)
|
||||||
|
|
||||||
paginated = self.paginate_response(
|
paginated = self.filtered_paginate(
|
||||||
data=lines,
|
data=lines,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="functions_disassemble",
|
tool_name="functions_disassemble",
|
||||||
@ -293,10 +301,11 @@ class FunctionsMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert lines back to text
|
# Convert lines back to text (skip if guarded)
|
||||||
if paginated.get("success"):
|
if paginated.get("success") and not paginated.get("guarded"):
|
||||||
paginated["result"] = "\n".join(paginated.get("result", []))
|
paginated["result"] = "\n".join(paginated.get("result", []))
|
||||||
paginated["function_name"] = result.get("name", name or address)
|
paginated["function_name"] = result.get("name", name or address)
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
Provides tools for struct data type operations.
|
Provides tools for struct data type operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
|
from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
|
||||||
@ -31,6 +31,7 @@ class StructsMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""List all struct data types with cursor-based pagination.
|
"""List all struct data types with cursor-based pagination.
|
||||||
@ -42,6 +43,7 @@ class StructsMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter struct names
|
grep: Regex pattern to filter struct names
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all results without pagination
|
return_all: Return all results without pagination
|
||||||
|
fields: Field names to keep (e.g. ['name', 'size']). Reduces response size.
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -76,7 +78,7 @@ class StructsMixin(GhydraMixinBase):
|
|||||||
}
|
}
|
||||||
session_id = self._get_session_id(ctx)
|
session_id = self._get_session_id(ctx)
|
||||||
|
|
||||||
return self.paginate_response(
|
return self.filtered_paginate(
|
||||||
data=all_structs,
|
data=all_structs,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="structs_list",
|
tool_name="structs_list",
|
||||||
@ -85,6 +87,7 @@ class StructsMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
@mcp_tool()
|
@mcp_tool()
|
||||||
@ -96,6 +99,7 @@ class StructsMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
project_fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Get detailed information about a struct with field pagination.
|
"""Get detailed information about a struct with field pagination.
|
||||||
@ -107,6 +111,7 @@ class StructsMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter fields
|
grep: Regex pattern to filter fields
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all fields without pagination
|
return_all: Return all fields without pagination
|
||||||
|
project_fields: Field names to keep per struct field item. Reduces response size.
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -160,7 +165,7 @@ class StructsMixin(GhydraMixinBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Paginate fields
|
# Paginate fields
|
||||||
paginated = self.paginate_response(
|
paginated = self.filtered_paginate(
|
||||||
data=fields,
|
data=fields,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="structs_get",
|
tool_name="structs_get",
|
||||||
@ -169,10 +174,11 @@ class StructsMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=project_fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Merge struct metadata with paginated fields
|
# Merge struct metadata with paginated fields (skip if guarded)
|
||||||
if paginated.get("success"):
|
if paginated.get("success") and not paginated.get("guarded"):
|
||||||
paginated["struct_name"] = struct_info.get("name", name)
|
paginated["struct_name"] = struct_info.get("name", name)
|
||||||
paginated["struct_size"] = struct_info.get("size", struct_info.get("length"))
|
paginated["struct_size"] = struct_info.get("size", struct_info.get("length"))
|
||||||
paginated["struct_category"] = struct_info.get("category", struct_info.get("categoryPath"))
|
paginated["struct_category"] = struct_info.get("category", struct_info.get("categoryPath"))
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
Provides tools for cross-reference (xref) operations.
|
Provides tools for cross-reference (xref) operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
|
from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
|
||||||
@ -32,6 +32,7 @@ class XrefsMixin(GhydraMixinBase):
|
|||||||
grep: Optional[str] = None,
|
grep: Optional[str] = None,
|
||||||
grep_ignorecase: bool = True,
|
grep_ignorecase: bool = True,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
|
fields: Optional[List[str]] = None,
|
||||||
ctx: Optional[Context] = None,
|
ctx: Optional[Context] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""List cross-references with filtering and pagination.
|
"""List cross-references with filtering and pagination.
|
||||||
@ -45,6 +46,7 @@ class XrefsMixin(GhydraMixinBase):
|
|||||||
grep: Regex pattern to filter results
|
grep: Regex pattern to filter results
|
||||||
grep_ignorecase: Case-insensitive grep (default: True)
|
grep_ignorecase: Case-insensitive grep (default: True)
|
||||||
return_all: Return all results without pagination
|
return_all: Return all results without pagination
|
||||||
|
fields: Field names to keep (e.g. ['fromAddress', 'toAddress']). Reduces response size.
|
||||||
ctx: FastMCP context (auto-injected)
|
ctx: FastMCP context (auto-injected)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -94,7 +96,7 @@ class XrefsMixin(GhydraMixinBase):
|
|||||||
}
|
}
|
||||||
session_id = self._get_session_id(ctx)
|
session_id = self._get_session_id(ctx)
|
||||||
|
|
||||||
return self.paginate_response(
|
return self.filtered_paginate(
|
||||||
data=all_xrefs,
|
data=all_xrefs,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
tool_name="xrefs_list",
|
tool_name="xrefs_list",
|
||||||
@ -103,6 +105,7 @@ class XrefsMixin(GhydraMixinBase):
|
|||||||
grep=grep,
|
grep=grep,
|
||||||
grep_ignorecase=grep_ignorecase,
|
grep_ignorecase=grep_ignorecase,
|
||||||
return_all=return_all,
|
return_all=return_all,
|
||||||
|
fields=fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Resources
|
# Resources
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import signal
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
@ -68,6 +69,18 @@ def create_server(
|
|||||||
cursors_mixin.register_all(mcp)
|
cursors_mixin.register_all(mcp)
|
||||||
docker_mixin.register_all(mcp)
|
docker_mixin.register_all(mcp)
|
||||||
|
|
||||||
|
# Optional feedback collection
|
||||||
|
cfg = get_config()
|
||||||
|
if cfg.feedback_enabled:
|
||||||
|
try:
|
||||||
|
from fastmcp_feedback import add_feedback_tools
|
||||||
|
|
||||||
|
db_path = Path(cfg.feedback_db_path)
|
||||||
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
add_feedback_tools(mcp, database_url=f"sqlite:///{db_path}")
|
||||||
|
except ImportError:
|
||||||
|
pass # fastmcp-feedback not installed — skip silently
|
||||||
|
|
||||||
return mcp
|
return mcp
|
||||||
|
|
||||||
|
|
||||||
@ -132,6 +145,9 @@ def main():
|
|||||||
|
|
||||||
config = get_config()
|
config = get_config()
|
||||||
|
|
||||||
|
if config.feedback_enabled:
|
||||||
|
print(f" 📋 Feedback collection: {config.feedback_db_path}", file=sys.stderr)
|
||||||
|
|
||||||
# Create and configure the server
|
# Create and configure the server
|
||||||
mcp = create_server()
|
mcp = create_server()
|
||||||
|
|
||||||
|
|||||||
112
uv.lock
generated
112
uv.lock
generated
@ -400,12 +400,27 @@ 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" },
|
{ 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 = "fastmcp-feedback"
|
||||||
|
version = "2026.1.12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "fastmcp" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sqlalchemy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/73/84/cbd09919ea66ee263e7da6bf07f6389cee7a0afedacd55f9d1e210dea24b/fastmcp_feedback-2026.1.12.1.tar.gz", hash = "sha256:26f75419e7787c982204c62ce78de1f13eec8915f64acee84597389722ed902d", size = 81133, upload-time = "2026-01-16T02:09:13.543Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/ee/327a3f6c7ac5cde56c7c9449dbc6c0ab78b15c06a51ad5645ab880240120/fastmcp_feedback-2026.1.12.1-py3-none-any.whl", hash = "sha256:6a3dec71f3d3eae4eb0102eb0a86aa7853fb0419fb506a5a13d17deaf842c53c", size = 29789, upload-time = "2026-01-16T02:09:11.831Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ghydramcp"
|
name = "ghydramcp"
|
||||||
version = "2025.12.3"
|
version = "2025.12.3"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastmcp" },
|
{ name = "fastmcp" },
|
||||||
|
{ name = "fastmcp-feedback" },
|
||||||
{ name = "mcp" },
|
{ name = "mcp" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
]
|
]
|
||||||
@ -413,10 +428,58 @@ dependencies = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "fastmcp", specifier = ">=2.0.0" },
|
{ name = "fastmcp", specifier = ">=2.0.0" },
|
||||||
|
{ name = "fastmcp-feedback", specifier = ">=1.0.0" },
|
||||||
{ name = "mcp", specifier = ">=1.22.0" },
|
{ name = "mcp", specifier = ">=1.22.0" },
|
||||||
{ name = "requests", specifier = ">=2.32.3" },
|
{ name = "requests", specifier = ">=2.32.3" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greenlet"
|
||||||
|
version = "3.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@ -1061,6 +1124,55 @@ wheels = [
|
|||||||
{ 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" },
|
{ 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 = "sqlalchemy"
|
||||||
|
version = "2.0.46"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sse-starlette"
|
name = "sse-starlette"
|
||||||
version = "3.0.3"
|
version = "3.0.3"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user