Compare commits

..

No commits in common. "290252c0dba49834d0f30eb9f69e07bb19d4825c" and "2d837d95fce3fa415fd5f900a3c153ed6b627c2a" have entirely different histories.

15 changed files with 231 additions and 2729 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,197 +0,0 @@
# 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
```

View File

@ -11,7 +11,6 @@ dependencies = [
"mcp>=1.22.0",
"requests>=2.32.3",
"fastmcp>=2.0.0",
"fastmcp-feedback>=1.0.0",
]
[project.scripts]

View File

@ -72,24 +72,9 @@ class GhydraConfig:
cursor_ttl_seconds: int = 300 # 5 minutes
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: 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: dict = field(default_factory=lambda: {
"functions": 1000,

View File

@ -24,11 +24,6 @@ from .progress import (
report_progress,
report_step,
)
from .filtering import (
project_fields,
apply_grep,
estimate_and_guard,
)
from .logging import (
log_info,
log_debug,
@ -55,10 +50,6 @@ __all__ = [
"ProgressReporter",
"report_progress",
"report_step",
# Filtering
"project_fields",
"apply_grep",
"estimate_and_guard",
# Logging
"log_info",
"log_debug",

View File

@ -1,208 +0,0 @@
"""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)

View File

@ -14,7 +14,6 @@ from threading import Lock
from typing import Any, Dict, List, Optional, Tuple
from ..config import get_config
from .filtering import project_fields, estimate_and_guard
# ReDoS Protection Configuration
@ -394,9 +393,8 @@ def paginate_response(
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Create a paginated response with optional grep filtering and field projection.
"""Create a paginated response with optional grep filtering.
Args:
data: Full result list to paginate
@ -406,8 +404,7 @@ def paginate_response(
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 budget guard)
fields: Optional list of field names to project (jq-style)
return_all: Bypass pagination and return all results (with warning)
Returns:
dict with pagination metadata and results
@ -434,19 +431,6 @@ def paginate_response(
"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)
warning = None
@ -454,7 +438,7 @@ def paginate_response(
warning = f"EXTREMELY LARGE response (~{estimated_tokens:,} tokens)"
elif estimated_tokens > 20000:
warning = f"VERY LARGE response (~{estimated_tokens:,} tokens)"
elif estimated_tokens > config.large_response_threshold:
elif estimated_tokens > 8000:
warning = f"Large response (~{estimated_tokens:,} tokens)"
return {
@ -465,19 +449,16 @@ def paginate_response(
"total_count": len(data),
"filtered_count": len(filtered_data),
"grep_pattern": grep,
"fields_projected": fields,
"estimated_tokens": estimated_tokens,
"warning": warning,
},
"timestamp": int(time.time() * 1000),
}
# Normal pagination flow — apply field projection before cursoring
paginated_data = project_fields(data, fields) if fields else data
# Normal pagination flow
try:
cursor_id, state = cursor_manager.create_cursor(
data=paginated_data,
data=data,
query_params=query_params,
tool_name=tool_name,
session_id=session_id,

View File

@ -3,7 +3,7 @@
Provides tools for program analysis operations.
"""
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional
from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_tool
@ -57,7 +57,6 @@ class AnalysisMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Get function call graph with edge pagination.
@ -71,7 +70,6 @@ class AnalysisMixin(GhydraMixinBase):
grep: Regex pattern to filter edges
grep_ignorecase: Case-insensitive grep (default: True)
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)
Returns:
@ -117,7 +115,7 @@ class AnalysisMixin(GhydraMixinBase):
}
session_id = self._get_session_id(ctx)
paginated = self.filtered_paginate(
paginated = self.paginate_response(
data=edges,
query_params=query_params,
tool_name="analysis_get_callgraph",
@ -126,10 +124,9 @@ class AnalysisMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
)
if paginated.get("success") and not paginated.get("guarded"):
if paginated.get("success"):
paginated["result"] = {
"root_function": func_id,
"max_depth": max_depth,
@ -151,7 +148,6 @@ class AnalysisMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Perform data flow analysis with step pagination.
@ -165,7 +161,6 @@ class AnalysisMixin(GhydraMixinBase):
grep: Regex pattern to filter steps
grep_ignorecase: Case-insensitive grep (default: True)
return_all: Return all steps without pagination
fields: Field names to keep per step. Reduces response size.
ctx: FastMCP context (auto-injected)
Returns:
@ -215,7 +210,7 @@ class AnalysisMixin(GhydraMixinBase):
}
session_id = self._get_session_id(ctx)
paginated = self.filtered_paginate(
paginated = self.paginate_response(
data=steps,
query_params=query_params,
tool_name="analysis_get_dataflow",
@ -224,11 +219,9 @@ class AnalysisMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
)
# Merge metadata into result (skip if guarded)
if paginated.get("success") and not paginated.get("guarded"):
if paginated.get("success"):
paginated["result"] = {
"start_address": address,
"direction": direction,

View File

@ -12,7 +12,7 @@ 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 paginate_response
from ..core.pagination import get_cursor_manager, paginate_response
from ..core.logging import log_info, log_debug, log_warning, log_error
@ -209,9 +209,8 @@ class GhydraMixinBase(MCPMixin):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[list] = None,
) -> Dict:
"""Create paginated response with optional field projection."""
"""Create paginated response."""
return paginate_response(
data=data,
query_params=query_params,
@ -221,37 +220,6 @@ class GhydraMixinBase(MCPMixin):
grep=grep,
grep_ignorecase=grep_ignorecase,
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

View File

@ -3,7 +3,7 @@
Provides tools for data items and strings operations.
"""
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional
from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
@ -34,7 +34,6 @@ class DataMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""List defined data items with filtering and cursor-based pagination.
@ -49,7 +48,6 @@ class DataMixin(GhydraMixinBase):
grep: Regex pattern to filter results
grep_ignorecase: Case-insensitive grep (default: True)
return_all: Return all results without pagination
fields: Field names to keep (e.g. ['address', 'name']). Reduces response size.
ctx: FastMCP context (auto-injected)
Returns:
@ -93,7 +91,7 @@ class DataMixin(GhydraMixinBase):
}
session_id = self._get_session_id(ctx)
return self.filtered_paginate(
return self.paginate_response(
data=all_data,
query_params=query_params,
tool_name="data_list",
@ -102,7 +100,6 @@ class DataMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
)
@mcp_tool()
@ -114,7 +111,6 @@ class DataMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""List all defined strings in the binary with pagination.
@ -126,7 +122,6 @@ class DataMixin(GhydraMixinBase):
grep: Regex pattern to filter results (e.g., "password|key")
grep_ignorecase: Case-insensitive grep (default: True)
return_all: Return all strings without pagination
fields: Field names to keep (e.g. ['value', 'address']). Reduces response size.
ctx: FastMCP context (auto-injected)
Returns:
@ -162,7 +157,7 @@ class DataMixin(GhydraMixinBase):
}
session_id = self._get_session_id(ctx)
return self.filtered_paginate(
return self.paginate_response(
data=result_data,
query_params=query_params,
tool_name="data_list_strings",
@ -171,7 +166,6 @@ class DataMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
)
@mcp_tool()

View File

@ -3,7 +3,7 @@
Provides tools for function analysis, decompilation, and manipulation.
"""
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional
from urllib.parse import quote
from fastmcp import Context
@ -33,7 +33,6 @@ class FunctionsMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""List functions with cursor-based pagination.
@ -44,7 +43,6 @@ class FunctionsMixin(GhydraMixinBase):
grep: Regex pattern to filter function names
grep_ignorecase: Case-insensitive grep (default: True)
return_all: Return all functions without pagination
fields: Field names to keep (e.g. ['name', 'address']). Reduces response size.
ctx: FastMCP context (auto-injected)
Returns:
@ -69,7 +67,7 @@ class FunctionsMixin(GhydraMixinBase):
query_params = {"tool": "functions_list", "port": port, "grep": grep}
session_id = self._get_session_id(ctx)
return self.filtered_paginate(
return self.paginate_response(
data=functions,
query_params=query_params,
tool_name="functions_list",
@ -78,7 +76,6 @@ class FunctionsMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
)
@mcp_tool()
@ -132,7 +129,6 @@ class FunctionsMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Get decompiled code for a function with line pagination.
@ -147,7 +143,6 @@ class FunctionsMixin(GhydraMixinBase):
grep: Regex pattern to filter lines
grep_ignorecase: Case-insensitive grep (default: True)
return_all: Return all lines without pagination
fields: Field names to keep (for structured results)
ctx: FastMCP context (auto-injected)
Returns:
@ -199,7 +194,7 @@ class FunctionsMixin(GhydraMixinBase):
}
session_id = self._get_session_id(ctx)
paginated = self.filtered_paginate(
paginated = self.paginate_response(
data=lines,
query_params=query_params,
tool_name="functions_decompile",
@ -208,11 +203,10 @@ class FunctionsMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
)
# Convert lines back to text in result (skip if guarded)
if paginated.get("success") and not paginated.get("guarded"):
# 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)
@ -228,7 +222,6 @@ class FunctionsMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Get disassembly for a function with instruction pagination.
@ -241,7 +234,6 @@ class FunctionsMixin(GhydraMixinBase):
grep: Regex pattern to filter instructions
grep_ignorecase: Case-insensitive grep (default: True)
return_all: Return all instructions without pagination
fields: Field names to keep (for structured results)
ctx: FastMCP context (auto-injected)
Returns:
@ -292,7 +284,7 @@ class FunctionsMixin(GhydraMixinBase):
}
session_id = self._get_session_id(ctx)
paginated = self.filtered_paginate(
paginated = self.paginate_response(
data=lines,
query_params=query_params,
tool_name="functions_disassemble",
@ -301,11 +293,10 @@ class FunctionsMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
)
# Convert lines back to text (skip if guarded)
if paginated.get("success") and not paginated.get("guarded"):
# 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)

View File

@ -3,7 +3,7 @@
Provides tools for struct data type operations.
"""
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional
from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
@ -31,7 +31,6 @@ class StructsMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""List all struct data types with cursor-based pagination.
@ -43,7 +42,6 @@ class StructsMixin(GhydraMixinBase):
grep: Regex pattern to filter struct names
grep_ignorecase: Case-insensitive grep (default: True)
return_all: Return all results without pagination
fields: Field names to keep (e.g. ['name', 'size']). Reduces response size.
ctx: FastMCP context (auto-injected)
Returns:
@ -78,7 +76,7 @@ class StructsMixin(GhydraMixinBase):
}
session_id = self._get_session_id(ctx)
return self.filtered_paginate(
return self.paginate_response(
data=all_structs,
query_params=query_params,
tool_name="structs_list",
@ -87,7 +85,6 @@ class StructsMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
)
@mcp_tool()
@ -99,7 +96,6 @@ class StructsMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
project_fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Get detailed information about a struct with field pagination.
@ -111,7 +107,6 @@ class StructsMixin(GhydraMixinBase):
grep: Regex pattern to filter fields
grep_ignorecase: Case-insensitive grep (default: True)
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)
Returns:
@ -165,7 +160,7 @@ class StructsMixin(GhydraMixinBase):
}
# Paginate fields
paginated = self.filtered_paginate(
paginated = self.paginate_response(
data=fields,
query_params=query_params,
tool_name="structs_get",
@ -174,11 +169,10 @@ class StructsMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=project_fields,
)
# Merge struct metadata with paginated fields (skip if guarded)
if paginated.get("success") and not paginated.get("guarded"):
# 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"))

View File

@ -3,7 +3,7 @@
Provides tools for cross-reference (xref) operations.
"""
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional
from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_tool, mcp_resource
@ -32,7 +32,6 @@ class XrefsMixin(GhydraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""List cross-references with filtering and pagination.
@ -46,7 +45,6 @@ class XrefsMixin(GhydraMixinBase):
grep: Regex pattern to filter results
grep_ignorecase: Case-insensitive grep (default: True)
return_all: Return all results without pagination
fields: Field names to keep (e.g. ['fromAddress', 'toAddress']). Reduces response size.
ctx: FastMCP context (auto-injected)
Returns:
@ -96,7 +94,7 @@ class XrefsMixin(GhydraMixinBase):
}
session_id = self._get_session_id(ctx)
return self.filtered_paginate(
return self.paginate_response(
data=all_xrefs,
query_params=query_params,
tool_name="xrefs_list",
@ -105,7 +103,6 @@ class XrefsMixin(GhydraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
)
# Resources

View File

@ -8,7 +8,6 @@ import signal
import sys
import threading
import time
from pathlib import Path
from typing import Optional
from fastmcp import FastMCP
@ -69,18 +68,6 @@ def create_server(
cursors_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
@ -145,9 +132,6 @@ def main():
config = get_config()
if config.feedback_enabled:
print(f" 📋 Feedback collection: {config.feedback_db_path}", file=sys.stderr)
# Create and configure the server
mcp = create_server()

112
uv.lock generated
View File

@ -400,27 +400,12 @@ 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 = "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]]
name = "ghydramcp"
version = "2025.12.3"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },
{ name = "fastmcp-feedback" },
{ name = "mcp" },
{ name = "requests" },
]
@ -428,58 +413,10 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "fastmcp", specifier = ">=2.0.0" },
{ name = "fastmcp-feedback", specifier = ">=1.0.0" },
{ name = "mcp", specifier = ">=1.22.0" },
{ 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]]
name = "h11"
version = "0.16.0"
@ -1124,55 +1061,6 @@ 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" },
]
[[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]]
name = "sse-starlette"
version = "3.0.3"