Skip to content

Commit 334783d

Browse files
committed
Use encoding auto-detection for mcp tools
1 parent 96955f5 commit 334783d

7 files changed

Lines changed: 89 additions & 88 deletions

File tree

docs/plans/encoding.md

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,121 +3,145 @@
33
## Current Encoding Usage
44

55
### APIs (15+ functions with encoding parameter)
6+
67
- `get_overview(absolute_file_path, encoding="utf-8")`
78
- `search_content(..., encoding="utf-8", ...)`
89
- `read_content(..., encoding="utf-8", ...)`
910
- `edit_content(..., encoding="utf-8", ...)`
1011

1112
### Internal Functions
13+
1214
- `src/file_access.py`: 6 functions with encoding
13-
- `src/search_engine.py`: `search_file()`
15+
- `src/search_engine.py`: `search_file()`
1416
- `src/editor.py`: 2 functions with encoding
1517
- `src/data_models.py`: `FileOverview.encoding`
1618

1719
## Implementation Plan
1820

1921
#### Phase 1: Add chardet and detection
20-
- [x] Add chardet dependency
22+
23+
- [x] Add chardet dependency
2124
- [x] Create `detect_file_encoding(file_path: str) -> str` in `file_access.py`
2225
- [x] Fallback to utf-8 on detection failure
2326

2427
**Implementation Notes:**
28+
2529
- Added 0.7 confidence threshold to prevent poor chardet guesses
2630
- Detection function handles empty files, exceptions, and low confidence cases
2731
- Test coverage for UTF-8, Latin-1, UTF-16, empty files, and non-existent files
2832

2933
#### Phase 2: Update file access layer
34+
3035
- [x] Remove encoding parameter from `read_file_content()`
31-
- [x] Remove encoding parameter from `read_file_lines()`
36+
- [x] Remove encoding parameter from `read_file_lines()`
3237
- [x] Remove encoding parameter from `write_file_content()`
3338
- [x] Update internal strategy functions
3439
- [x] Add encoding cache per file session
3540

3641
**Implementation Notes:**
42+
3743
- `read_file_content()` and `read_file_lines()` now call `detect_file_encoding()` internally
3844
- `write_file_content()` preserves existing file encoding or defaults to utf-8 for new files
3945
- Internal strategy functions (`_read_file_memory`, etc.) still accept encoding parameter
4046
- **Skipped caching**: Detection is fast (~1-5ms), avoiding session management complexity
4147
- All existing tests pass with auto-detection
4248

4349
#### Phase 3: Update search and edit layers
50+
4451
- [x] Remove encoding parameter from `search_file()`
4552
- [x] Remove encoding parameter from `atomic_edit_file()`
4653
- [x] Remove encoding parameter from `_preview_edit()`
4754

4855
**Implementation Notes:**
56+
4957
- `search_file()` now uses `read_file_lines()` without encoding parameter
5058
- `atomic_edit_file()` and `replace_content()` updated to use auto-detection
5159
- **No `_preview_edit()` function**: Preview functionality is built into `replace_content()`
5260
- All edit operations now preserve original file encoding automatically
5361

5462
#### Phase 4: Update public APIs
55-
- [ ] Remove encoding parameter from all 4 MCP tools
56-
- [ ] Update MCP schema definitions
57-
- [ ] Update `FileOverview.encoding` to show detected value
63+
64+
- [x] Remove encoding parameter from all 4 MCP tools
65+
- [x] Update MCP schema definitions
66+
- [x] Update `FileOverview.encoding` to show detected value
67+
68+
**Implementation Notes:**
69+
- All 4 MCP tools (`get_overview`, `search_content`, `read_content`, `edit_content`) no longer accept encoding
70+
- MCP schema definitions updated to remove encoding parameter from all tools
71+
- `FileOverview.encoding` now shows the actual detected encoding value
72+
- **Breaking change**: All tool signatures simplified, auto-detection is now transparent to users
5873

5974
#### Phase 5: Documentation and testing
75+
6076
- [ ] Update API.md, design.md, examples
6177
- [ ] Add encoding detection tests
6278
- [ ] Test various encodings and edge cases
6379

6480
#### Phase 6: Quality assurance
81+
6582
- [ ] Run pre-push script
6683
- [ ] Verify all tests pass
6784
- [ ] Update plan as complete
6885

6986
## Technical Implementation
7087

7188
### Detection Function
89+
7290
```python
7391
def detect_file_encoding(file_path: str) -> str:
7492
"""Detect file encoding using chardet with utf-8 fallback."""
7593
try:
7694
with open(file_path, 'rb') as f:
7795
sample = f.read(65536) # 64KB sample
78-
96+
7997
if not sample:
8098
return 'utf-8'
81-
99+
82100
result = chardet.detect(sample)
83-
101+
84102
# Use detection only if confidence is reasonable (>= 0.7)
85103
if result and result.get('confidence', 0) >= 0.7:
86104
return result['encoding'] or 'utf-8'
87105
else:
88106
return 'utf-8'
89-
107+
90108
except Exception:
91109
return 'utf-8'
92110
```
93111

94112
## Testing
95113

96114
### Encodings to Test
115+
97116
- UTF-8, UTF-16 LE/BE, Latin-1, ASCII, Windows-1252
98117

99-
### Edge Cases
118+
### Edge Cases
119+
100120
- Empty files, binary files, small files, BOM markers
101121

102122
## Code Quality
103123

104124
### Requirements
125+
105126
- Simple, clear, concise code
106127
- Avoid complexity and premature optimization
107128
- Self-documenting functions
108129
- Minimal abstraction layers
109130
- Direct error handling
110131

111132
### Implementation Guidance
112-
- Document scope changes and decisions made during each phase
113-
- Note what was skipped and why (e.g., complexity vs benefit trade-offs)
133+
134+
- Document scope changes and decisions made during each phase as Implementation Notes below the phase
114135
- Keep implementation notes concise but informative for future reference
136+
- Note what was skipped and why (e.g., complexity vs benefit trade-offs)
137+
- ALWAYS use a todo when implementing a phase, the last step of the todo being to mark checklist items as complete when done.
115138

116139
### API After Implementation
140+
117141
```python
118142
# Simplified APIs
119143
get_overview("/path/to/file.py")
120144
search_content("/path/to/file.py", "pattern")
121145
read_content("/path/to/file.py", target)
122146
edit_content("/path/to/file.py", search_text, replace_text)
123-
```
147+
```

src/editor.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,4 @@ def atomic_edit_file(
248248
raise EditError(f"File is not writable: {file_path}")
249249

250250
# Perform the edit operation
251-
return replace_content(
252-
canonical_path, search_text, replace_text, fuzzy, preview
253-
)
251+
return replace_content(canonical_path, search_text, replace_text, fuzzy, preview)

src/file_access.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,28 @@ def choose_file_strategy(file_size: int) -> str:
2727
def detect_file_encoding(file_path: str) -> str:
2828
"""Detect file encoding using chardet with utf-8 fallback."""
2929
try:
30-
with open(file_path, 'rb') as f:
30+
with open(file_path, "rb") as f:
3131
sample = f.read(65536) # 64KB sample
32-
32+
3333
if not sample:
34-
return 'utf-8'
35-
34+
return "utf-8"
35+
3636
result = chardet.detect(sample)
37-
37+
38+
# If chardet detects ASCII, try UTF-8 first since ASCII is subset of UTF-8
39+
# and UTF-8 is more robust for files that might have non-ASCII later
40+
encoding = result.get("encoding") if result else None
41+
if encoding and encoding.lower() == "ascii":
42+
return "utf-8"
43+
3844
# Use detection only if confidence is reasonable (>= 0.7)
39-
if result and result.get('confidence', 0) >= 0.7:
40-
return result['encoding'] or 'utf-8'
45+
if result and result.get("confidence", 0) >= 0.7:
46+
return result["encoding"] or "utf-8"
4147
else:
42-
return 'utf-8'
43-
48+
return "utf-8"
49+
4450
except Exception:
45-
return 'utf-8'
51+
return "utf-8"
4652

4753

4854
def get_file_info(file_path: str) -> dict:
@@ -208,12 +214,12 @@ def write_file_content(file_path: str, content: str) -> None:
208214
"""Write content to file atomically using temp file + rename with auto-detected encoding."""
209215
canonical_path = normalize_path(file_path)
210216
temp_path = f"{canonical_path}.tmp"
211-
217+
212218
# Detect encoding from existing file, or default to utf-8 for new files
213219
if os.path.exists(canonical_path):
214220
encoding = detect_file_encoding(canonical_path)
215221
else:
216-
encoding = 'utf-8'
222+
encoding = "utf-8"
217223

218224
try:
219225
# Clean up any existing temp file first

src/mcp_schemas.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ def get_tool_schemas() -> list[types.Tool]:
3232
"type": "string",
3333
"description": "Absolute path to the file",
3434
},
35-
"encoding": {
36-
"type": "string",
37-
"description": "File encoding",
38-
"default": "utf-8",
39-
},
4035
},
4136
"required": ["absolute_file_path"],
4237
},
@@ -52,11 +47,6 @@ def get_tool_schemas() -> list[types.Tool]:
5247
"description": "Absolute path to the file",
5348
},
5449
"pattern": {"type": "string", "description": "Search pattern"},
55-
"encoding": {
56-
"type": "string",
57-
"description": "File encoding",
58-
"default": "utf-8",
59-
},
6050
"max_results": {
6151
"type": "integer",
6252
"description": "Maximum number of results",
@@ -90,11 +80,6 @@ def get_tool_schemas() -> list[types.Tool]:
9080
"oneOf": [{"type": "integer"}, {"type": "string"}],
9181
"description": "Line number or search pattern",
9282
},
93-
"encoding": {
94-
"type": "string",
95-
"description": "File encoding",
96-
"default": "utf-8",
97-
},
9883
"mode": {
9984
"type": "string",
10085
"description": "Reading mode",
@@ -123,11 +108,6 @@ def get_tool_schemas() -> list[types.Tool]:
123108
"type": "string",
124109
"description": "Replacement text",
125110
},
126-
"encoding": {
127-
"type": "string",
128-
"description": "File encoding",
129-
"default": "utf-8",
130-
},
131111
"fuzzy": {
132112
"type": "boolean",
133113
"description": "Enable fuzzy matching",

src/search_engine.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ def combine_results(
7171
return sorted(combined, key=lambda x: (x.line_number, -x.similarity_score))
7272

7373

74-
def search_file(
75-
file_path: str, pattern: str, fuzzy: bool = True
76-
) -> list[SearchMatch]:
74+
def search_file(file_path: str, pattern: str, fuzzy: bool = True) -> list[SearchMatch]:
7775
"""Search file content using auto-detected encoding. Returns clear results or clear errors."""
7876

7977
try:

0 commit comments

Comments
 (0)