Skip to content

Commit 5373192

Browse files
dguidoclaude
andauthored
Address v2.1.0 evaluation feedback and bump to v2.2.0 (#9)
Fix four issues identified in the v2.1.0 evaluation: 1. Fix symlink false positive in path validation (Critical) - Changed os.path.abspath() to os.path.realpath() to resolve symlinks - Fixes false path traversal errors on macOS where /tmp -> /private/tmp 2. Fix exclude_paths to match subdirectories (Medium) - Added path_matches_exclusion() helper supporting both prefix and component matching - exclude_paths=["test/"] now matches both "test/foo.sol" and "src/test/foo.sol" - Updated 5 tools: list_contracts, search_contracts, search_functions, run_detectors, find_dead_code 3. Add invalid detector name validation (Low) - Added invalid_detector_names field to RunDetectorsResponse - Validates requested detector names against available detectors - Returns list of unrecognized names in response 4. Fix list_function_implementations for inherited functions (Low) - Now checks both functions_declared and functions_inherited - Updated resolve_function_implementations in types.py Also: - Update ruff-pre-commit from v0.8.0 to v0.14.13 - Rename hook id from 'ruff' to 'ruff-check' (new convention) - Modernize code with pyupgrade rules (set comprehensions, dict.fromkeys) Tests: - Added 10 tests for path_matches_exclusion helper - Added subdirectory exclusion test for list_contracts - Added 3 tests for invalid detector name validation Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ca52650 commit 5373192

24 files changed

+264
-118
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
repos:
66
- repo: https://github.com/astral-sh/ruff-pre-commit
7-
rev: v0.8.0
7+
rev: v0.14.13
88
hooks:
9-
- id: ruff
9+
- id: ruff-check
1010
args: [--fix]
1111
- id: ruff-format
1212

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "slither-mcp"
3-
version = "2.1.0"
3+
version = "2.2.0"
44
description = "MCP server for Slither static analysis of Solidity contracts"
55
readme = "README.md"
66
requires-python = ">=3.11"

slither_mcp/tools/export_call_graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def export_call_graph(
203203
truncated = len(nodes) > request.max_nodes
204204
if truncated:
205205
# Calculate node degrees (in + out edges) to prioritize connected nodes
206-
node_degrees: dict[str, int] = {n: 0 for n in nodes}
206+
node_degrees: dict[str, int] = dict.fromkeys(nodes, 0)
207207
for from_node, to_node, _ in edges:
208208
if from_node in node_degrees:
209209
node_degrees[from_node] += 1

slither_mcp/tools/find_dead_code.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
TEST_FUNCTION_PREFIX,
1111
)
1212
from slither_mcp.pagination import PaginatedRequest, apply_pagination
13-
from slither_mcp.types import ContractKey, FunctionKey, ProjectFacts
13+
from slither_mcp.types import ContractKey, FunctionKey, ProjectFacts, path_matches_exclusion
1414

1515

1616
class DeadCodeFunction(BaseModel):
@@ -185,7 +185,7 @@ def find_dead_code(
185185

186186
# Skip contracts matching exclude_paths
187187
if request.exclude_paths:
188-
if any(contract_key.path.startswith(p) for p in request.exclude_paths):
188+
if path_matches_exclusion(contract_key.path, request.exclude_paths):
189189
continue
190190

191191
# Check declared functions

slither_mcp/tools/list_contracts.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from slither_mcp.types import (
99
ContractKey,
1010
ProjectFacts,
11+
path_matches_exclusion,
1112
)
1213

1314

@@ -73,7 +74,7 @@ def list_contracts(
7374
for key, model in project_facts.contracts.items():
7475
# Apply exclude_paths filter
7576
if request.exclude_paths:
76-
if any(key.path.startswith(p) for p in request.exclude_paths):
77+
if path_matches_exclusion(key.path, request.exclude_paths):
7778
continue
7879

7980
# Apply filter_type
@@ -116,9 +117,7 @@ def list_contracts(
116117
contracts.sort(key=lambda c: c.function_count, reverse=reverse)
117118

118119
# Apply pagination
119-
contracts, total_count, has_more = apply_pagination(
120-
contracts, request.offset, request.limit
121-
)
120+
contracts, total_count, has_more = apply_pagination(contracts, request.offset, request.limit)
122121

123122
return ListContractsResponse(
124123
success=True, contracts=contracts, total_count=total_count, has_more=has_more

slither_mcp/tools/list_function_implementations.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,10 @@ def list_function_implementations(
105105
implementations = []
106106
for impl_contract in implementing_contracts:
107107
# Get the function model to extract visibility and modifiers
108+
# Check both declared and inherited functions
108109
func_model = impl_contract.functions_declared.get(request.function_signature)
110+
if not func_model:
111+
func_model = impl_contract.functions_inherited.get(request.function_signature)
109112
if func_model:
110113
implementations.append(
111114
ImplementationInfo(

slither_mcp/tools/list_functions.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ class FunctionInfo(BaseModel):
1919
visibility: str
2020
solidity_modifiers: list[str]
2121
is_declared: bool # True if declared, False if inherited
22-
line_count: Annotated[
23-
int, Field(description="Number of lines in the function")
24-
] = 0
22+
line_count: Annotated[int, Field(description="Number of lines in the function")] = 0
2523

2624

2725
class ListFunctionsRequest(PaginatedRequest):
@@ -90,9 +88,7 @@ def list_functions(
9088

9189
# Apply modifiers filter
9290
if request.has_modifiers:
93-
if not any(
94-
mod in func_model.solidity_modifiers for mod in request.has_modifiers
95-
):
91+
if not any(mod in func_model.solidity_modifiers for mod in request.has_modifiers):
9692
continue
9793

9894
# Calculate line count
@@ -120,9 +116,7 @@ def list_functions(
120116

121117
# Apply modifiers filter
122118
if request.has_modifiers:
123-
if not any(
124-
mod in func_model.solidity_modifiers for mod in request.has_modifiers
125-
):
119+
if not any(mod in func_model.solidity_modifiers for mod in request.has_modifiers):
126120
continue
127121

128122
# Calculate line count
@@ -148,9 +142,7 @@ def list_functions(
148142
# Define visibility order for sorting
149143
visibility_order = {"external": 0, "public": 1, "internal": 2, "private": 3}
150144
if request.sort_by == "name":
151-
functions.sort(
152-
key=lambda f: f.function_key.signature.lower(), reverse=reverse
153-
)
145+
functions.sort(key=lambda f: f.function_key.signature.lower(), reverse=reverse)
154146
elif request.sort_by == "visibility":
155147
functions.sort(
156148
key=lambda f: visibility_order.get(f.visibility.lower(), 4),
@@ -160,9 +152,7 @@ def list_functions(
160152
functions.sort(key=lambda f: f.line_count, reverse=reverse)
161153

162154
# Apply pagination
163-
functions, total_count, has_more = apply_pagination(
164-
functions, request.offset, request.limit
165-
)
155+
functions, total_count, has_more = apply_pagination(functions, request.offset, request.limit)
166156

167157
return ListFunctionsResponse(
168158
success=True, functions=functions, total_count=total_count, has_more=has_more

slither_mcp/tools/run_detectors.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from slither_mcp.types import (
99
DetectorResult,
1010
ProjectFacts,
11+
path_matches_exclusion,
1112
)
1213

1314

@@ -36,6 +37,10 @@ class RunDetectorsResponse(BaseModel):
3637
has_more: Annotated[
3738
bool, Field(description="True if there are more results beyond this page")
3839
] = False
40+
invalid_detector_names: Annotated[
41+
list[str] | None,
42+
Field(description="Detector names that were requested but not recognized"),
43+
] = None
3944
error_message: str | None = None
4045

4146

@@ -57,6 +62,14 @@ def run_detectors(
5762
"""
5863
try:
5964
all_results = []
65+
invalid_names: list[str] = []
66+
67+
# Validate detector names if provided
68+
if request.detector_names:
69+
available_names = {d.name.lower() for d in project_facts.available_detectors}
70+
invalid_names = [
71+
name for name in request.detector_names if name.lower() not in available_names
72+
]
6073

6174
# Collect results from all detectors or filtered detectors
6275
if request.detector_names:
@@ -91,7 +104,7 @@ def run_detectors(
91104
filtered_results.append(result)
92105
continue
93106
has_non_excluded_location = any(
94-
not any(loc.file_path.startswith(p) for p in request.exclude_paths)
107+
not path_matches_exclusion(loc.file_path, request.exclude_paths)
95108
for loc in result.source_locations
96109
)
97110
if has_non_excluded_location:
@@ -104,7 +117,11 @@ def run_detectors(
104117
)
105118

106119
return RunDetectorsResponse(
107-
success=True, results=all_results, total_count=total_count, has_more=has_more
120+
success=True,
121+
results=all_results,
122+
total_count=total_count,
123+
has_more=has_more,
124+
invalid_detector_names=invalid_names if invalid_names else None,
108125
)
109126
except Exception as e:
110127
return RunDetectorsResponse(

slither_mcp/tools/search_contracts.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66

77
from slither_mcp.pagination import apply_pagination
88
from slither_mcp.search import SearchError, compile_pattern, validate_pattern
9-
from slither_mcp.types import ContractKey, JSONStringTolerantModel, ProjectFacts
9+
from slither_mcp.types import (
10+
ContractKey,
11+
JSONStringTolerantModel,
12+
ProjectFacts,
13+
path_matches_exclusion,
14+
)
1015

1116

1217
class SearchContractsRequest(JSONStringTolerantModel):
@@ -84,9 +89,7 @@ def search_contracts(
8489
# Apply exclude_paths filter
8590
if request.exclude_paths:
8691
matches = [
87-
key
88-
for key in matches
89-
if not any(key.path.startswith(p) for p in request.exclude_paths)
92+
key for key in matches if not path_matches_exclusion(key.path, request.exclude_paths)
9093
]
9194

9295
matches, total_count, has_more = apply_pagination(matches, request.offset, request.limit)

slither_mcp/tools/search_functions.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66

77
from slither_mcp.pagination import apply_pagination
88
from slither_mcp.search import SearchError, compile_pattern, validate_pattern
9-
from slither_mcp.types import FunctionKey, JSONStringTolerantModel, ProjectFacts
9+
from slither_mcp.types import (
10+
FunctionKey,
11+
JSONStringTolerantModel,
12+
ProjectFacts,
13+
path_matches_exclusion,
14+
)
1015

1116

1217
class SearchFunctionsRequest(JSONStringTolerantModel):
@@ -98,7 +103,7 @@ def search_functions(
98103
for contract_key, contract_model in project_facts.contracts.items():
99104
# Apply exclude_paths filter
100105
if request.exclude_paths:
101-
if any(contract_key.path.startswith(p) for p in request.exclude_paths):
106+
if path_matches_exclusion(contract_key.path, request.exclude_paths):
102107
continue
103108

104109
# Search declared functions

0 commit comments

Comments
 (0)