MCP server (Model Context Protocol) that solves the problem of AI coding tools (Windsurf, Claude Code, Claude Desktop, Cursor, etc.) expecting text files to be UTF-8 encoded.
Many projects — particularly Delphi, C++Builder, and legacy Windows applications —
have source files in Windows-1252, ISO-8859-1, or other 8-bit encodings. Without
proper handling, special characters (æ, ø, å, €, smart quotes, etc.)
get corrupted when an LLM reads or writes these files.
The server automatically detects encoding (BOM, UTF-8 validation, or heuristic
codepage scoring) and translates between file encoding and UTF-8 on read and
write. Encoding is remembered per file in a .windsurf-encoding.json sidecar in
the workspace root.
- Go to Releases
- Download
EncodingMCP.exe - Place it somewhere accessible (e.g.
C:\Tools\EncodingMCP.exe) - Configure your MCP client (see Installation)
Note (Windows): After downloading you may need to right-click the file → Properties → "Unblock", as Windows marks files downloaded from the internet. Alternatively:
Unblock-File -Path <path>in PowerShell.
git clone https://github.com/thvedel/EncodingMCP.git
cd EncodingMCP
build.batThe result is a standalone .exe under build\Win64\Release\.
| Path | Description |
|---|---|
EncodingMCP.dpr |
Main program (console application) |
src/MCP.*.pas |
Stdio transport, JSON-RPC 2.0, server dispatcher |
src/Encoding.*.pas |
Detection, heuristics, workspace, cache |
src/FileIO.*.pas |
Encoding-aware file reading/writing |
src/Tools.*.pas |
MCP tools (read/write/detect/override) |
tests/ |
DUnitX test suite |
build.bat |
Build script (main program + tests) |
- Delphi 12.3 (Studio 23.0) or Delphi 13.1 (Studio 37.0) — uses only RTL
(
System.JSON,System.SysUtils,System.Classes,System.IOUtils,System.Generics.*) - Windows
- No runtime dependencies — it is a single standalone
.exe
build.bat automatically searches for Delphi in standard locations and selects
the newest available version:
| Delphi version | Studio number | Path to rsvars.bat |
|---|---|---|
| 13.1 | 37.0 | C:\Program Files (x86)\Embarcadero\Studio\37.0\bin\rsvars.bat |
| 12.3 | 23.0 | C:\Program Files (x86)\Embarcadero\Studio\23.0\bin\rsvars.bat |
If your installation is elsewhere, set the RSVARS environment variable
before running build.bat.
build.batBuilding is done via msbuild against the two .dproj files. Output goes to
build\$(Platform)\$(Config)\ — by default build\Win64\Release\. Change
PLATFORM/CONFIG in build.bat to build differently (e.g. Win32
or Debug).
You can also open EncodingMCP.dproj directly in RAD Studio and build from there.
The server communicates via stdio (stdin/stdout JSON-RPC). Any MCP client that supports stdio transport can use it. Below are examples for the most popular clients.
In all examples, replace <PATH> with the absolute path to
EncodingMCP.exe (either downloaded from Releases or built locally).
Backslashes in JSON must be escaped as \\.
Edit %USERPROFILE%\.codeium\windsurf\mcp_config.json:
{
"mcpServers": {
"encoding-bridge": {
"command": "<PATH>",
"args": [],
"disabled": false
}
}
}Restart Windsurf after the change.
claude mcp add encoding-bridge "<PATH>"Edit the config file:
- Windows:
%APPDATA%\Claude\claude_desktop_config.json - macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"encoding-bridge": {
"command": "<PATH>",
"args": []
}
}
}Edit .cursor/mcp.json in your project (or the global config):
{
"mcpServers": {
"encoding-bridge": {
"command": "<PATH>",
"args": []
}
}
}Any client that supports MCP stdio transport can launch the EXE directly.
Protocol: JSON-RPC 2.0 over stdin/stdout. Server name: encoding-bridge.
Set the environment variable ENCODING_MCP_LOG_LEVEL=debug for detailed logging
on stderr. Supported levels: debug, info (default), warning, error.
Reads a text file with automatic encoding detection and returns the content as UTF-8 along with metadata.
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute path to the file |
head |
integer | Optional: only first N lines |
tail |
integer | Optional: only last N lines |
startLine |
integer | Optional: 1-based line number to start reading from. Use with endLine for a specific range. Takes priority over head/tail. |
endLine |
integer | Optional: 1-based line number to stop at (inclusive). Use with startLine for a specific range. Takes priority over head/tail. |
contextLines |
integer | Optional: extra lines to include before and after the startLine/endLine range. Only applies when a line range is specified. |
metadataOnly |
boolean | Optional: if true, return only metadata (encoding, lineEnding, totalLines, etc.) without file content. |
searchText |
string | Optional: search for lines containing this text (case-insensitive). Returns matching lines with line-number prefixes, separated by ... between non-contiguous regions. Use contextLines for surrounding context. Takes priority over head/tail/startLine/endLine. |
Output (JSON in content[0].text):
{
"path": "...",
"encoding": "Windows-1252",
"hasBom": false,
"lineEnding": "CRLF",
"confidence": 0.95,
"fromCache": false,
"bytesRead": 1234,
"totalLines": 42,
"lineNumberStart": 1,
"returnedLines": 42,
"matchCount": 3,
"content": "unit MainForm; ..."
}Writes UTF-8 content to a file in the correct encoding. If the file exists,
its original encoding is preserved (unless an encoding override is given).
New files default to UTF-8 with BOM.
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute path |
content |
string | UTF-8 content |
encoding |
string | Optional override (UTF-8, Windows-1252, ...) |
lineEnding |
string | Optional (CRLF/LF/CR) |
hasBom |
boolean | Optional |
createIfMissing |
boolean | Default true |
Edits a text file by search/replace or line-range replacement, preserving the file's original encoding, BOM, and line-ending style.
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute path to the file (must exist) |
oldText |
string | Text to find and replace. Leave empty for range mode. |
newText |
string | Replacement text (required) |
startLine |
integer | Optional: 1-based start line for range replacement |
endLine |
integer | Optional: 1-based end line (inclusive) for range replacement |
maxReplacements |
integer | Optional: max replacements (default 1). 0 = unlimited. |
dryRun |
boolean | Optional: if true, compute the result without writing to disk. |
edits |
array | Optional: array of atomic edits (see below). Overrides top-level oldText/newText. |
Modes:
- Search/replace: provide
oldText+newText. IfmaxReplacementsis 1 (default) and multiple matches exist, an error is returned with the match count. - Range replacement: provide
startLine+endLine+newTextwith emptyoldText. The specified line range is replaced entirely. - Multi-edit (atomic): provide
editsarray. Each edit is applied sequentially. If any edit fails, no changes are written. Each item supports:oldText,newText,startLine,endLine,maxReplacements.
Output (JSON in content[0].text):
{
"path": "...",
"encoding": "Windows-1252",
"hasBom": false,
"lineEnding": "CRLF",
"bytesWritten": 1234,
"replacements": 1,
"changed": true,
"diff": "@@ -1,3 +1,3 @@\n context\n-old line\n+new line\n context\n"
}The diff field is only present when changed is true. It contains a unified
diff snippet showing removed (-) and added (+) lines with context.
Reads multiple files in a single call. Reduces MCP round-trips during
cross-file refactoring. Each file entry supports the same parameters as
read_text_file. Errors for individual files are reported inline without
aborting the batch.
| Parameter | Type | Description |
|---|---|---|
files |
array | Array of file specifications (see below) |
Each entry in files:
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute path to the file (required) |
head |
integer | Optional: first N lines |
tail |
integer | Optional: last N lines |
startLine |
integer | Optional: 1-based start line |
endLine |
integer | Optional: 1-based end line (inclusive) |
contextLines |
integer | Optional: extra context lines |
metadataOnly |
boolean | Optional: skip content |
searchText |
string | Optional: case-insensitive line search |
Output (JSON in content[0].text):
{
"totalFiles": 3,
"succeeded": 2,
"failed": 1,
"results": [
{ "path": "...", "encoding": "UTF-8", "content": "...", ... },
{ "path": "...", "encoding": "Windows-1252", "content": "...", ... },
{ "path": "...", "error": "File not found: ..." }
]
}Writes multiple files in a single call with encoding-aware conversion. Each file entry can specify its own encoding, lineEnding, and hasBom options. Errors for individual files are reported inline without aborting the batch.
| Parameter | Type | Description |
|---|---|---|
files |
array | Array of file specifications (see below) |
Each entry in files:
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute path to the file (required) |
content |
string | UTF-8 content to write (required) |
encoding |
string | Optional: target encoding (UTF-8, Windows-1252, etc.) |
lineEnding |
string | Optional: CRLF, LF, or CR |
hasBom |
boolean | Optional: write a BOM |
createIfMissing |
boolean | Optional (default true): create if not exists |
Output (JSON in content[0].text):
{
"totalFiles": 2,
"succeeded": 2,
"failed": 0,
"results": [
{ "path": "...", "encoding": "UTF-8", "bytesWritten": 123, "created": true, ... },
{ "path": "...", "encoding": "Windows-1252", "bytesWritten": 456, "created": false, ... }
]
}Lists files in a directory recursively, optionally filtered by glob pattern. Returns relative paths from the specified directory.
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute path to the directory to list (must be within workspace) |
pattern |
string | Optional: glob pattern to filter files (e.g. *.pas, *.dfm) |
Output (JSON in content[0].text):
{
"path": "C:/Projects/MyApp",
"totalFiles": 12,
"files": ["src/Main.pas", "src/Utils.pas", ...]
}Returns detected encoding + candidate scores without reading the full content.
| Parameter | Type |
|---|---|
path |
string |
Manually set encoding for a specific file or for an extension pattern.
| Parameter | Type | Description |
|---|---|---|
path |
string | Either path or pattern |
pattern |
string | E.g. *.pas |
encoding |
string | Encoding to apply |
All tools validate that the requested path resides within an allowed
workspace root. Paths are fully resolved first, so .. traversal cannot be
used to escape. Requests outside the allowed roots are rejected with an error.
The allowed roots are determined at startup, in this order:
- Command-line arguments — each non-switch argument is a workspace root:
EncodingMCP.exe C:\Projects\MyApp C:\Projects\Shared ENCODING_MCP_WORKSPACEenvironment variable — semicolon-separated list- Fallback: the workspace containing the current directory
In addition, roots advertised by the MCP client during initialize
(rootUri, rootPath or the MCP roots array) are added to the allowed set.
Configuring the root explicitly is recommended, since the working directory the MCP client uses to launch the server is not guaranteed:
{
"mcpServers": {
"encoding-bridge": {
"command": "<PATH>",
"args": ["C:\\Projects\\MyApp"]
}
}
}If no root can be determined at all, the server falls back to requiring a real
workspace marker (.git, .windsurf, *.dproj, *.groupproj, .svn, .hg)
at or above the requested path.
Note: Containment is checked on the resolved path string. Symlinks and junctions that point out of the workspace are not resolved and can therefore still reach outside it.
- BOM check (UTF-8 / UTF-16 LE+BE / UTF-32 LE+BE) — 100% certain
- Strict UTF-8 validation — rejects overlong, surrogates, out-of-range
- UTF-16 without BOM — null-byte distribution heuristic
- 8-bit codepage scoring:
- Windows-1252: preferred if printable C1 characters (€, smart quotes, ...)
- ISO-8859-1: preferred if no C1 bytes
- ISO-8859-15: if 0xA4 (€) seen without other C1 characters
- Fallback: Windows-1252 (Delphi/Windows default)
.windsurf-encoding.json is placed in the workspace root (the directory
containing .git, .windsurf, *.dproj, *.groupproj, .svn, or .hg).
Format:
{
"version": 1,
"files": {
"src/MainForm.pas": {
"encoding": "Windows-1252",
"hasBom": false,
"lineEnding": "CRLF",
"detectedAt": "2026-04-30T16:21:06+02:00"
}
},
"overrides": {
"*.pas": "Windows-1252"
}
}manual: true marks entries set explicitly via set_encoding_override —
these are not overwritten by auto-detection.
File before (Windows-1252):
75 6E 69 74 20 54 65 73 74 3B 0D 0A 2F 2F 20 E6 F8 E5
Read via read_text_file:
unit Test;\r\n// æøå
Written back via write_text_file with the same content:
75 6E 69 74 20 54 65 73 74 3B 0D 0A 2F 2F 20 E6 F8 E5
Bytes are identical — encoding is preserved.
- Client integration: There is no guarantee that the AI tool will actually
call
write_text_fileinstead of its native file writing. It depends on how the tool descriptions cause the LLM to choose the tool. Test empirically. - MacRoman: Not actively detected yet — the code has the structure for it but scoring is minimal.
- Large files: Heuristics run only on the first 64 KB. This is fast and sufficient for typical source files.
- Concurrent writes: File writing (both user files and sidecar cache) is atomic via temp file + rename, so a crash never leaves a half-written file. The sidecar cache merges with disk content before writing, so entries from other instances are preserved. However, there is no file-locking layer — with truly concurrent writes to the same file from two instances, the last one wins.
Two ways to run tests:
build.batBuilds and runs the DUnitX console runner. Output in terminal, exit code 0 on success.
tests\EncodingMCPTests.dpr has a {$IFDEF TESTINSIGHT} that selects between
two runners:
| Build config | Define | Runner |
|---|---|---|
| Debug (IDE) | TESTINSIGHT defined |
TestInsight (results to View → TestInsight) |
| Release (build.bat) | not defined | DUnitX console with stdout output |
TESTINSIGHT is set in tests\EncodingMCPTests.dproj's Debug configuration —
no extra setup required after the first time you install TestInsight.
- Install TestInsight: github.com/Stefan-Glienke/TestInsight — free open source by Stefan Glienke.
- Library Path: In the IDE, Tools → Options → Language → Delphi → Library
→ add the TestInsight
Sourcefolder to Library Path for Win64. On Windows it is typically located at%LOCALAPPDATA%\Programs\TestInsight\Sourceafter installation.
- Open
tests\EncodingMCPTests.dprojin RAD Studio. - Select the Debug configuration (default). Compile (Ctrl+F9).
- Open View → TestInsight (or View → Other Windows → TestInsight).
- Click Run in the TestInsight panel — results appear live, and you can double-click a failure to jump directly to the line in the editor.
Tests cover:
- BOM detection (UTF-8/16 LE/BE)
- Strict UTF-8 validation (incl. overlong, surrogates, lone start bytes)
- Codepage heuristics (Windows-1252 vs ISO-8859-1 vs ISO-8859-15)
- Line-ending detection (CRLF/LF/Mixed)
- End-to-end round-trip (read Windows-1252 → write back, byte comparison)
- BOM writing and default UTF-8 BOM for new files
- Atomic file writing (no .tmp file left behind)
- Cache merge (two instances preserve each other's entries)
- Line-range reading (startLine/endLine interval, clamping, priority over head/tail)
- Edit tool: search/replace (single, multi, unlimited, max-N, deletion)
- Edit tool: range replacement (middle, first, last, entire, clamp, invalid)
- Edit tool: encoding preservation, no-change detection, error handling
- lineNumberStart output for head, tail, startLine/endLine, and full file
- contextLines expansion and clamping for line-range reads
- dryRun mode for edit_text_file (no-write verification)
- Cache invalidation on file change (size/timestamp staleness check)
- Manual override survives cache invalidation
- Search-in-file: matching, context, case-insensitivity, region merging, match count
- Optimistic lock: external modification detection, successful edit without conflict
- Workspace restriction: allowed roots, traversal escape, sibling prefix directory, root directory itself, multiple roots, marker-discovery fallback
- list_files tool: recursive listing, glob filtering, empty directory, subdirectories, missing directory
- read_text_files (batch-read): single/multiple files, inline error handling, metadataOnly, head param, searchText, empty array
- Multi-edit (atomic): two edits applied, second-fails-nothing-written, dryRun, empty array, mixed modes
- write_text_files (batch-write): single/multiple files, inline error handling, creates new, empty array, encoding override
- Diff output: contains changed lines, empty when no change, multi-edit shows all changes
Pull requests and issues are welcome. Run build.bat before submitting a PR to
ensure the test suite still passes.
Released under the MIT License — Copyright (c) 2026 Thomas Vedel.