Model Context Protocol server for Win32 systems via serial/network console (Windows 3.1 + Win32s 1.25a through Windows 11)
Model Context Protocol server for Win32 systems via serial/network console (Windows 3.1 + Win32s 1.25a through Windows 11)
Project Goal: Enable MCP clients (Claude Code, Claude Desktop, etc.) to interact with any Win32-based Windows system via a console bridge - from Windows 3.1 with Win32s 1.25a (1995) through modern Windows.
Thanks to Windows' legendary forward compatibility and strict adherence to the Win32s 1.25a API subset, the same Win32 console application can run across decades of Windows releases, making this a universal bridge for both retro and modern Windows development environments.
This project bridges modern MCP-compatible AI tools with Win32 environments across the entire Windows family. By targeting the Win32s 1.25a API subset (1995) and leveraging Windows' remarkable forward compatibility, a single Win32 console application can run unmodified from Windows 3.1 through modern Windows - spanning decades of releases.
MCP-Win32s serves both retro computing and modern Windows development scenarios:
Retro Computing & Legacy Systems:
- Maintaining legacy codebases that require period-accurate compilers (Visual C++ 1.x-6.0, Borland C++, etc.)
- Developing for retro hardware platforms and vintage systems
- Preserving and documenting vintage software
- Educational purposes (understanding historical development environments)
- Testing software compatibility across the Win32 family (Win32s 1.25a through modern Windows)
Modern Windows Development:
- Accessing Windows build environments in VMs or remote machines
- CI/CD pipelines for Windows-specific builds
- Cross-platform development where the build machine runs Windows
- Embedded Windows systems (Windows IoT, Windows Embedded)
- Air-gapped or isolated Windows environments
- Remote Windows development from Linux/macOS workstations
Universal Approach: Thanks to Win32's forward compatibility, the same console shell works everywhere - whether you're compiling 16-bit DOS programs on Windows 3.1 + Win32s 1.25a or building modern C++20 applications on Windows 11. MCP clients can't directly access these systems, so MCP-Win32s provides a universal bridge.
- ✅ Execute commands on any Win32 Windows system from modern MCP clients
- ✅ Read/write files on Windows filesystems (all versions)
- ✅ Compile C/C++ code with any Win32 compiler (VC++ 1.x-2022, Borland, Watcom, MinGW, Clang, etc.)
- ✅ List directories and manage files remotely
- ✅ Binary-safe file transfers via base64 encoding
- ✅ Simple JSON-based protocol over serial, TCP/IP, or named pipes
- ✅ Works on Windows 3.1 + Win32s 1.25a, 95, 98, ME, NT 3.x/4.0, 2000, XP, Vista, 7, 8, 10, 11, Server editions
- ✅ Single executable runs across decades of Windows releases (1995 onwards)
- ✅ Multiple transport options: serial (retro), telnet (legacy networking), TCP sockets (modern)
- ✅ Strict Win32s 1.25a API compatibility ensures maximum forward compatibility
Windows Machine (Any Win32 version):
- Minimum CPU: Intel 80386 or compatible (1986+ mass production)
- Minimum OS: Windows 3.1 + Win32s 1.25a (Feb 1995)
- Retro: Windows 95, 98, ME, NT 3.x/4.0, 2000, XP
- Modern: Vista, 7, 8, 10, 11, Server 2003-2022
- Development tools (any Win32-compatible compiler)
- Connection method:
- Serial: COM port (retro systems, embedded)
- Network: TCP/IP (modern systems, VMs)
- Named Pipes: Local or network pipes (VMs, WSL interop)
Modern Development Machine:
- Linux/Windows/macOS with Python 3.9+
- Connection hardware (if using serial):
- Serial port or USB-to-Serial adapter for retro systems
- Network connection for modern systems
- MCP-compatible client (Claude Code, Claude Desktop, etc.)
-
On Windows machine: Compile the Win32 shell
The build is defined by
CMakeLists.txt+CMakePresets.json+ the toolchain files intoolchains/. The Win32s-critical flags (/BASE:0x10000,/FIXED:NO,/G3or-march=i386, C89, no floating point) live in those toolchain files, not on the command line.# MinGW-w64 cross-compile (the build exercised on the dev host and in CI) cmake --preset mingw cmake --build --preset mingw # This binary runs on Win3.1+Win32s 1.25a → Win11!
REM Visual C++ 6.0 WITHOUT the IDE (on a Windows host with the VC6 tools REM on PATH after VCVARS32.BAT) — uses CMake's "NMake Makefiles" generator cmake --preset vc6 cmake --build --preset vc6
build.shandbuild.batstill exist but are now thin wrappers around the above (build.sh→ mingw preset,build.bat→ vc6 preset). Out-of-source build output lands inbuild/(git-ignored). -
On modern machine: Install MCP bridge
pip install pyserial mcp git clone https://github.com/yourname/mcp-win32s cd mcp-win32s -
Configure MCP client: Add to your MCP configuration
{ "mcpServers": { "win32s": { "command": "python3", "args": ["/path/to/mcp_serial_bridge.py"] } } } -
Connect:
- Serial (retro): Null-modem cable between machines
- Network (modern): TCP socket (Winsock 1.1 compatible)
- Note: Win32s has
wsock32.dll(Winsock 1.1), notws2_32.dll
- Note: Win32s has
-
Run: Start Win32 shell, then start MCP bridge
REM On Windows machine (any version from Win3.1 to Win11) mcp-w32s.exe /SERIAL:COM1 REM or mcp-w32s.exe /TCP:8932
Important: Test on Win3.1 + Win32s 1.25a to verify proper linking!
See detailed setup instructions below.
Design Philosophy: Maximum compatibility through minimum dependencies.
MCP-Win32s is designed to compile with:
- ✅ Visual C++ 6.0 (1998) - Reference compiler, retro builds
- ✅ MinGW-w64 (modern) - GitHub Actions, cross-compilation from Linux
- ✅ Visual Studio 2015+ - Modern Windows development
Single codebase, multiple toolchains - no conditional compilation required.
Strict Win32s Subset - ANSI Only:
We use ONLY APIs that existed in Win32s 1.25a (February 1995):
- ✅
CreateFileA- NOT CreateFileW (no Unicode) - ✅
CreateProcessA- ANSI process spawning - ✅
ReadFile,WriteFile- Binary I/O (no encoding issues) - ✅
WaitForSingleObject- Process synchronization - ✅
GetStdHandle- Console handles - ✅ Berkeley sockets (Windows 95+) -
socket(),bind(),listen(),accept()
Explicitly AVOIDED:
- ❌ Unicode APIs (CreateFileW, etc.) - Not in Win32s
- ❌ Shell32.dll - Not guaranteed in Win32s
- ❌ Modern networking (WinHTTP, WinINet) - Use raw sockets
- ❌ C++ STL - VC++ 6.0 STL is non-standard, MinGW differs
- ❌ Exceptions - Compatibility issues across compilers
- ❌ Templates - Except the most basic ones
C with minimal C++ features:
// YES - Works everywhere
char buffer[256];
HANDLE hFile = CreateFileA("COM1:", ...);
struct CommandData { char cmd[256]; int id; };
// NO - Compiler-dependent
std::string cmd; // STL varies between VC6 and modern
std::vector<int> items; // Template instantiation differs
try { } catch { } // Exception handling incompatibleSafe to use:
- ✅
stdio.h-fopen,fprintf,fgets(ANSI C) - ✅
stdlib.h-malloc,free,atoi(ANSI C) - ✅
string.h-strcpy,strcat,strcmp(ANSI C) - ✅
windows.h- Win32 API
Avoid:
- ❌
iostream- Vastly different between VC6 and modern C++ - ❌ STL containers - Template incompatibilities
- ❌ Modern C++ features (auto, lambdas, smart pointers)
Our code must compile cleanly with:
| Compiler | Version | Platform | Purpose |
|---|---|---|---|
| VC++ 6.0 | 1998 | Windows | Retro reference build |
| MinGW-w64 | Latest | Linux/Windows | GitHub Actions CI |
| MSVC 2015 | 14.0 | Windows | Modern development |
| MSVC 2022 | 17.x | Windows | Latest toolchain |
We explicitly DO NOT support:
- ❌ Unicode/UTF-16 - ANSI/ASCII only (keeps code simple, works everywhere)
- ❌ Internationalization - English error messages, ASCII paths
- ❌ Modern C++ features - Stay in C++98 subset that VC6 understands
- ❌ COM/OLE - Not needed, adds complexity
- ❌ MFC/ATL - Pure Win32 API only
By targeting the common subset of:
- Win32s 1.25a ANSI APIs (1995)
- ANSI C standard library
- VC++ 6.0 C++ subset
- MinGW compatibility
We get a single codebase that compiles everywhere and runs everywhere (Win95 → Win11).
Target Version: Win32s 1.25a (Build 142) - February 1995
Why 1.25a specifically?
- Most stable and well-behaved version according to testing
- Widely deployed (shipped with many applications)
- Good balance of features vs. stability
- Avoids critical bugs in earlier versions
- Avoids virtualization crashes in 1.30/1.30a/1.30c
Win32s Version History Context:
| Version | Date | Notes |
|---|---|---|
| 1.0 (Beta) | Oct 1992 | Pre-release, unstable |
| 1.10 | July 1993 | First stable release (with Windows NT 3.1) |
| 1.15/1.15a | 1994 | Minor improvements |
| 1.20 | 1994 | OLE 2.0 support added |
| 1.25a | Feb 1995 | Best stability, our target |
| 1.30/1.30a | 1995 | New features but less stable |
| 1.30c | Feb 1996 | Final release, crashes on QEMU/VMware |
Key Limitations of Win32s (All Versions):
- ❌ No multithreading - Single-threaded only
- ❌ No async I/O - Synchronous operations only
- ❌ No Unicode - ANSI APIs only (CreateFileA, not CreateFileW)
- ❌ No newer serial functions - Basic serial only
- ❌ No Winsock 2.0 - Only Winsock 1.1 (
wsock32.dll, notws2_32.dll) - ❌ Shared address space - Not isolated like true Win32
- ❌ 16MB memory limit per application
- ❌ Relocation info required - Must compile with
/FIXED:NO - ❌ Base address constraints - Can't load at 0x400000 (VC++ 5/6 default)
What IS Available in Win32s 1.25a:
- ✅ CreateFileA, ReadFile, WriteFile, CloseHandle (file I/O)
- ✅ CreateProcessA, WaitForSingleObject, ExitProcess (process control)
- ✅ CreatePipe (for output capture)
- ✅ GlobalAlloc, GlobalFree, LocalAlloc, LocalFree (memory)
- ✅ lstrlen, lstrcpy, lstrcat, lstrcmp (ANSI strings)
- ✅ GetLastError, SetLastError (error handling)
- ✅ MessageBoxA (basic UI)
- ✅ Winsock 1.1: socket, bind, listen, accept, send, recv (networking)
- ✅ WIN32_FIND_DATAA, FindFirstFileA, FindNextFileA (directory listing)
Critical Compiler Flags (these now live in the CMake toolchain files —
toolchains/vc6-nmake.cmake and toolchains/mingw-w64-i386.cmake — and are
applied automatically by the vc6 and mingw presets; they are shown here for
rationale):
REM Visual C++ 6.0 — set in toolchains/vc6-nmake.cmake
REM Default base 0x400000 is NOT available in Win32s!
REM /G3 - Target i386 (no 486+ instructions)
REM /FIXED:NO - Win32s needs relocation info
REM /BASE:0x10000 - Win32s-compatible base address
cl /W3 /O2 /TC /G3 /FIXED:NO /BASE:0x10000 ... kernel32.lib user32.lib wsock32.lib
# MinGW-w64 — set in toolchains/mingw-w64-i386.cmake
# -march=i386 - Only 386 instructions (no CPUID, CMPXCHG, etc.)
# -mtune=i386 - Optimize for 386
i686-w64-mingw32-gcc -O2 -std=c89 -march=i386 -mtune=i386 \
-Wall -Wdouble-promotion -Wfloat-equal \
-Wl,--dynamicbase -Wl,--image-base,0x10000 ...
CPU Target: i386 (80386)
Why i386 as minimum?
- Win32s requirement: Win32s 1.25a requires 386 or higher
- Maximum compatibility: Code runs on 386SX (1988) through modern CPUs
- No 486+ dependencies: Avoid CPUID, CMPXCHG, BSWAP (486+)
- No Pentium features: No RDTSC, CPUID, or Pentium-specific instructions
- Deterministic: Same instruction set across 35+ years
Prohibited CPU-Specific Features:
/* ❌ FORBIDDEN - 486+ instructions */
// CPUID instruction (486+)
// CMPXCHG (486+) - atomic compare-and-exchange
// BSWAP (486+) - byte swap
// XADD (486+) - exchange and add
/* ❌ FORBIDDEN - Pentium+ instructions */
// RDTSC (Pentium) - read timestamp counter
// CMPXCHG8B (Pentium) - 64-bit compare-exchange
// CMOVcc (Pentium Pro) - conditional move
/* ❌ FORBIDDEN - SSE/SSE2/MMX */
// Any SSE, SSE2, SSE3, MMX instructions
// MOVDQA, PADDB, etc.
/* ✅ PERMITTED - i386 instructions only */
// MOV, ADD, SUB, MUL, DIV, AND, OR, XOR, SHL, SHR
// JMP, CALL, RET, PUSH, POP
// CMP, TEST, Jcc (conditional jumps)
// LEA, LOOP, REP MOVSB/STOSBi386 vs i486 vs Pentium:
| Feature | i386 | i486 | Pentium | Our Code |
|---|---|---|---|---|
| Basic arithmetic | ✅ | ✅ | ✅ | ✅ Use |
| Memory access | ✅ | ✅ | ✅ | ✅ Use |
| CPUID | ❌ | ✅ | ✅ | ❌ Avoid |
| CMPXCHG | ❌ | ✅ | ✅ | ❌ Avoid |
| BSWAP | ❌ | ✅ | ✅ | ❌ Avoid |
| RDTSC | ❌ | ❌ | ✅ | ❌ Avoid |
| SSE/MMX | ❌ | ❌ | ❌/✅ | ❌ Avoid |
Why This Matters:
Win32s 1.25a on a 386SX (cheapest 32-bit CPU in 1995) should run our code:
- 386SX: 16-bit external bus, 32-bit internal (1988)
- 386DX: Full 32-bit (1986 - mass production, pre-production 1985)
- 486SX/DX: Adds on-chip cache, FPU (we don't use)
- Pentium: Superscalar, but we use only 386 subset
Code Examples:
/* ✅ SAFE - Pure i386 */
void MemCopy(void* dest, const void* src, size_t n) {
unsigned char* d = (unsigned char*)dest;
const unsigned char* s = (const unsigned char*)src;
size_t i;
for (i = 0; i < n; i++) {
d[i] = s[i]; /* Simple MOV instruction */
}
}
/* ❌ UNSAFE - Uses BSWAP (486+) */
unsigned long ByteSwap(unsigned long x) {
/* Compiler might emit BSWAP on 486+ */
return ((x >> 24) & 0xFF) |
((x >> 8) & 0xFF00) |
((x << 8) & 0xFF0000) |
((x << 24) & 0xFF000000);
/* Above is actually OK - pure shifts/masks */
}
/* ✅ SAFE - Manual byte swap (i386-compatible) */
unsigned long SafeByteSwap(unsigned long x) {
unsigned char* p = (unsigned char*)&x;
return ((unsigned long)p[0] << 24) |
((unsigned long)p[1] << 16) |
((unsigned long)p[2] << 8) |
((unsigned long)p[3]);
}Compiler Intrinsics to Avoid:
/* ❌ Don't use these - may emit 486+ instructions */
_InterlockedExchange() /* Uses XCHG or CMPXCHG */
_InterlockedCompareExchange() /* Uses CMPXCHG (486+) */
__rdtsc() /* RDTSC (Pentium+) */
_mm_pause() /* PAUSE (Pentium 4+) */Verification:
# Check that binary only uses i386 instructions
i686-w64-mingw32-objdump -d mcp-w32s.exe > disasm.txt
# These should NOT appear (486+ instructions):
grep -i 'cpuid' disasm.txt # Should be empty
grep -i 'cmpxchg' disasm.txt # Should be empty
grep -i 'bswap' disasm.txt # Should be empty
grep -i 'rdtsc' disasm.txt # Should be empty
# These SHOULD appear (i386 instructions):
grep -i 'mov' disasm.txt # Should have lots
grep -i 'add' disasm.txt # Should have some
grep -i 'call' disasm.txt # Should have someWhy /FIXED:NO and /BASE:0x10000?
- Win32s uses shared memory space (like Win16)
- Address 0x400000 (VC++ 5/6 default) is reserved by Win32s
- Must use lower base address (0x10000 is safe)
- Must include relocation info (hence
/FIXED:NO) - Without this, binary loads but immediately crashes
Testing Matrix:
| Platform | Compiler | Expected Behavior |
|---|---|---|
| Win3.1 + Win32s 1.25a | VC++ 6.0 | ✅ Primary target, must work |
| Win95/98/ME | Same binary | ✅ Should work (superset of Win32s) |
| WinNT 3.51/4.0 | Same binary | ✅ Should work (superset of Win32s) |
| Win2000/XP | Same binary | ✅ Should work (full Win32) |
| Win7/10/11 | Same binary | ✅ Should work (forward compat) |
| QEMU/VMware | Win32s 1.30c |
To ensure the same binary runs from Windows 3.1+Win32s 1.25a through Windows 11, we use only APIs present in Win32s 1.25a (February 1995):
Allowed APIs (Win32s-compatible, forever forward-compatible):
kernel32.dll: CreateFile, ReadFile, WriteFile, CreateProcess, CloseHandle, WaitForSingleObject, GetLastError, ExitProcess, etc.user32.dll: Basic message boxes only (MessageBox)ws2_32.dll: NOT AVAILABLE in Win32s - usewinsock.dllfor networking- String functions: lstrlen, lstrcpy, lstrcat (ANSI only)
- File I/O: CreateFile works for files, COM ports, pipes (not sockets in Win32s)
Forbidden APIs (not in Win32s):
- ❌ Unicode functions (CreateFileW, etc.) - Win32s is ANSI-only
- ❌
ws2_32.dll(Winsock 2) - use originalwinsock.dll(Winsock 1.1) - ❌ Modern CRT functions (C++11/14/17/20 features)
- ❌ SEH exceptions (not in Win32s)
- ❌ Thread pool APIs, critical sections (use basic mutexes if needed)
The codebase must compile with (targeting Win32s 1.25a):
| Compiler | Version | Platform | Notes |
|---|---|---|---|
| Visual C++ 2.0-6.0 | 1994-1998 | Windows 95/98/NT | Primary retro target |
| MinGW-w64 | Modern | Linux/GitHub Actions | Primary CI/CD target |
| Visual Studio 2022 | Modern | Windows | Modern development |
| Borland C++ 5.x | 1997-2000 | Windows | Alternative retro compiler |
Note: Visual C++ 1.x (1993) predates Win32s 1.25a and may have compatibility issues. VC++ 2.0+ recommended.
C/C++ Restrictions:
- C89/C90 compatible - no C99/C11 features
- C++ maximum: Visual C++ 6.0 dialect (pre-standard C++)
- No STL (not portable across compilers in VC6 era)
- No templates (VC6 template support is broken)
- No exceptions (not in Win32s)
- No namespaces (VC6 support is poor)
- Effectively: Write C code, compile as C++
- ABSOLUTELY NO FLOATING POINT - See rationale below
Floating Point Ban:
CRITICAL: MCP-Win32s completely prohibits all floating-point operations.
Rationale:
- FPU Compatibility Issues: Modern CPUs (Intel Ivy Bridge 2012+, AMD Zen) deprecate FCS/FDS segment registers, breaking old FPU exception handling code
- Unnecessary Complexity: File operations, command execution, JSON parsing require only integer arithmetic
- Deterministic Behavior: Integer-only code has predictable behavior across all CPU generations
- Win32s Safety: Avoid any interaction with WIN87EM.DLL (Windows 3.x FPU emulator)
- Portability: Integer code works identically on 8087, 387, modern CPUs, and even systems without FPU
Prohibited:
/* ❌ FORBIDDEN - No floating point! */
float x = 1.5;
double y = 2.0;
long double z;
f = sqrt(2.0);
d = sin(angle);Allowed:
/* ✅ PERMITTED - Integer arithmetic only */
int length = 1024;
long fileSize = 65536L;
unsigned int count = 0;
size_t bytes = sizeof(buffer);
/* Fixed-point if needed (we don't need it) */
int scaledValue = 1500; /* Represents 1.500 scaled by 1000 */What if we need division/percentages?
/* Integer division is fine */
int half = total / 2;
int percent = (value * 100) / total; /* Percentage calculation */
/* For more precision, scale first */
int permille = (value * 1000) / total; /* Parts per thousand */Compiler Enforcement (the MinGW toolchain at toolchains/mingw-w64-i386.cmake
sets -Wall -Werror -pedantic -Wdouble-promotion -Wfloat-equal, turning any
floating-point usage into a build error):
# MinGW - Error on FP usage
i686-w64-mingw32-gcc -O2 -std=c89 -Wall -Werror -Wdouble-promotion -Wfloat-equal ...
Verification:
# Check binary for FPU instructions
objdump -d mcp-w32s.exe | grep -E 'fld|fst|fadd|fmul|fsqrt'
# Should return EMPTY - no FPU instructions!Why This Is Easy:
MCP-Win32s operations are purely integer-based:
- File sizes: integers (bytes)
- Command execution: no math needed
- JSON parsing: string manipulation (no floats)
- Base64 encoding: integer arithmetic only
- Buffer management: integer sizes and offsets
- Serial/network I/O: byte counts (integers)
There is literally no need for floating point in this project.
Concrete Rules:
// ✅ ALLOWED - Integer operations
int i;
long l;
unsigned int ui;
size_t sz;
DWORD dw; /* Win32 - unsigned long */
int result = a * b / c;
unsigned char byte = (unsigned char)(value & 0xFF);
// ❌ FORBIDDEN - Floating point
float f; /* BANNED */
double d; /* BANNED */
long double ld; /* BANNED */
sqrt(x); /* BANNED */
pow(x, y); /* BANNED */
sin(x); /* BANNED */
atof(str); /* BANNED - converts to double */Concrete Rules:
// ✅ ALLOWED - C89 style
HANDLE hFile;
DWORD bytesRead;
char buffer[1024];
int i;
// ❌ FORBIDDEN - C99/modern
// HANDLE hFile = CreateFile(...); // No declaration+initialization mix
// for (int i = 0; i < 10; i++) // No loop variable declaration
// char buffer[] = {...}; // No dynamic initializersDesign Decision: UTF-8 everywhere on modern side, ANSI APIs on Win32s side.
Challenge: Win32s 1.25a has no Unicode support - only ANSI APIs (CreateFileA, not CreateFileW). How do we preserve UTF-8 fidelity bidirectionally?
Solution: Careful UTF-8/ANSI code page handling with explicit conversion.
Modern Side (MCP Bridge):
- All strings are UTF-8 (Python default)
- JSON protocol uses UTF-8 encoding
- File contents transmitted as base64 (binary safe, encoding agnostic)
Win32s Side (C89 Shell):
- All Win32 APIs are ANSI (
CreateFileA,ReadFileA, etc.) - Windows ANSI code page determines character interpretation
- Default code page: Windows-1252 (Western Europe/US)
Bidirectional Strategy:
Modern (UTF-8) ←→ [Wire Protocol] ←→ Win32s (ANSI/Code Page)
↓ ↓
base64 encode base64 decode
(binary safe) (binary safe)
↓ ↓
Preserve exact bytes Preserve exact bytes
For file read/write operations, use base64 encoding:
/* Win32s side - Reading a file */
void ReadFileContent(const char* path, char* base64Output) {
HANDLE hFile;
DWORD bytesRead;
unsigned char buffer[65536];
hFile = CreateFileA(path, GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
/* Read raw bytes (encoding agnostic) */
ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL);
CloseHandle(hFile);
/* Convert to base64 (preserves exact byte sequence) */
Base64Encode(buffer, bytesRead, base64Output);
/* JSON response: {"status":"ok","data":"<base64>"} */
/* Modern side decodes base64 → gets exact UTF-8 bytes */
}Result: File contents are binary safe - UTF-8, UTF-16, Shift-JIS, anything works!
Problem: Filenames may contain non-ASCII characters.
Win32s Limitations:
- ANSI APIs interpret bytes according to system code page
- Common code pages:
- Windows-1252: Western Europe (é, ñ, ü work)
- Windows-1251: Cyrillic
- Windows-932: Japanese (Shift-JIS)
- Windows-936: Simplified Chinese (GBK)
Strategy for Filenames:
# Modern side (MCP Bridge)
def send_read_file_command(utf8_filename):
"""
Send filename to Win32s - handle encoding carefully
"""
# Option 1: Try Windows-1252 encoding (works for Western languages)
try:
ansi_bytes = utf8_filename.encode('cp1252')
# If no exception, filename is Windows-1252 compatible
json_cmd = {
"cmd": "read",
"path": ansi_bytes.decode('cp1252'), # Valid in Win32s
"encoding": "cp1252"
}
except UnicodeEncodeError:
# Option 2: Filename has chars not in Windows-1252
# Fall back to 8.3 short names or error
raise ValueError(f"Filename '{utf8_filename}' not compatible with Windows-1252")
return json_cmdOn Win32s side (receives filename as ANSI):
/* Filename received from MCP bridge */
void ProcessReadCommand(const char* ansiPath) {
HANDLE hFile;
/* CreateFileA interprets ansiPath according to system code page */
/* If system is Windows-1252, works for Western European chars */
hFile = CreateFileA(ansiPath, GENERIC_READ, ...);
/* Rest of file reading... */
}What Works Well (Windows-1252):
- ✅ ASCII:
A-Z a-z 0-9 _ - . - ✅ Western European:
café.txt,Müller.c,résumé.doc - ✅ Symbols:
€,£,©
What Doesn't Work (not in Windows-1252):
- ❌ Cyrillic:
файл.txt(need Windows-1251) - ❌ CJK:
文件.txt(need Windows-936/932/950) - ❌ Emoji:
📁file.txt(need Unicode, not available in Win32s)
Recommended Approach:
-
Detect code page on Win32s startup:
UINT codePage = GetACP(); /* Get ANSI code page */ /* Send to MCP bridge: {"shell_ready":true,"codepage":1252} */
-
MCP bridge validates filenames against code page:
def validate_filename(filename, codepage): """Check if filename can be encoded in Win32s code page""" try: filename.encode(f'cp{codepage}') return True except UnicodeEncodeError: return False
-
Restrict to ASCII for maximum compatibility:
- Safest: Only use
[A-Za-z0-9_-.]in filenames - Reasonably safe: Use Windows-1252 (Western European)
- Complex: Detect and match system code page
- Safest: Only use
Challenge: Compiler output (cl.exe, gcc) may contain non-ASCII.
Win32s Behavior:
/* Executing a compiler that outputs non-ASCII */
CreateProcessA(NULL, "cl /c file.c", ...);
/* Compiler outputs in system code page (e.g., Windows-1252) */
/* Our shell captures this via ReadFile on pipe */Strategy:
/* Option 1: Pass through raw bytes (let modern side handle) */
void CaptureProcessOutput(HANDLE hPipe, char* output) {
DWORD bytesRead;
char buffer[4096];
/* Read raw bytes - don't try to interpret encoding */
ReadFile(hPipe, buffer, sizeof(buffer)-1, &bytesRead, NULL);
buffer[bytesRead] = '\0';
/* Send as-is in JSON (will be UTF-8 on modern Windows,
code page ANSI on Win32s) */
/* Modern side: attempts UTF-8 decode, falls back to cp1252 */
}Modern side handling:
def decode_output(raw_bytes):
"""Decode command output, handling encoding ambiguity"""
# Try UTF-8 first (modern Windows)
try:
return raw_bytes.decode('utf-8')
except UnicodeDecodeError:
# Fall back to Windows-1252 (Win32s/Win9x default)
return raw_bytes.decode('cp1252', errors='replace')
# 'replace' puts � for undecodable charsWire format is always UTF-8:
/* Win32s side - Building JSON response */
void BuildJsonResponse(const char* output, char* json) {
/* output contains ANSI bytes (code page dependent) */
/* Strategy 1: Base64 encode (safest for non-ASCII) */
char base64[8192];
Base64Encode((unsigned char*)output, lstrlen(output), base64);
lstrcpy(json, "{\"status\":\"ok\",\"output_b64\":\"");
lstrcat(json, base64);
lstrcat(json, "\"}");
/* Modern side: base64 decode, then UTF-8/cp1252 decode */
/* Strategy 2: JSON escape sequences (limited) */
/* Only works for ASCII + basic Latin-1 */
EscapeJsonString(output, json);
}For Maximum Compatibility:
-
File Contents: ALWAYS use base64
- ✅ Preserves exact bytes
- ✅ Works with any encoding (UTF-8, Shift-JIS, binary files)
- ✅ No ambiguity
-
Filenames: Restrict to ASCII or Windows-1252
- ✅ Use
[A-Za-z0-9_-.]for universal compatibility ⚠️ Western European chars (é, ñ, ü) work on most systems- ❌ Avoid Cyrillic, CJK, emoji
- ✅ Use
-
Command Output: Base64 or accept code page ambiguity
- ✅ Base64: Always safe, always correct
⚠️ Raw ANSI: Works on same-locale systems, may garble on different locales- 🔧 Include code page in shell startup message
-
JSON Keys/Commands: ASCII only
- Commands:
exec,read,write(all ASCII) - Status:
ok,error(all ASCII)
- Commands:
# Modern side: Read a UTF-8 file from Win32s
filename = "café.txt" # UTF-8 string with accented char
# 1. Encode filename to Windows-1252
ansi_filename = filename.encode('cp1252').decode('cp1252')
# Result: "café.txt" (valid in Windows-1252)
# 2. Send JSON command
cmd = {"cmd":"read", "path":"C:\\TEMP\\café.txt"}
send_json(cmd) # Serialized as UTF-8 over wire
# 3. Win32s receives, interprets path as ANSI
# If system code page is Windows-1252, "café" works correctly
# CreateFileA opens C:\TEMP\café.txt
# 4. Win32s reads file, base64 encodes
response = {"status":"ok","data":"Y2Fmw6kgdGV4dA=="} # base64
# 5. Modern side: base64 decode
raw_bytes = base64.b64decode("Y2Fmw6kgdGV4dA==")
# Result: b'caf\xc3\xa9 text' (UTF-8 bytes)
# 6. Decode as UTF-8
content = raw_bytes.decode('utf-8')
# Result: "café text" ✅| Data Type | Win32s Side | Wire Protocol | Modern Side | Fidelity |
|---|---|---|---|---|
| File content | Raw bytes | Base64 | Raw bytes | ✅ Perfect |
| Filenames (ASCII) | ANSI API | UTF-8 JSON | UTF-8 | ✅ Perfect |
| Filenames (Western) | ANSI cp1252 | UTF-8 JSON | UTF-8 | ✅ Good* |
| Filenames (CJK) | ANSI cp936 | UTF-8 JSON | UTF-8 | |
| Command output | ANSI/Code page | Base64/Raw | UTF-8/cp1252 | ✅ Good*** |
| JSON structure | ASCII | UTF-8 | UTF-8 | ✅ Perfect |
* Works if Win32s system is Windows-1252
** Works if Win32s system has matching code page
*** Use base64 for guaranteed fidelity, or accept decode ambiguity
/* Send code page info on startup */
void SendReadyMessage(HANDLE hComm) {
char json[256];
UINT codePage;
char cpStr[8];
codePage = GetACP(); /* Get system ANSI code page */
/* Convert to string (no sprintf in strict C89) */
wsprintf(cpStr, "%u", codePage);
lstrcpy(json, "{\"status\":\"ready\",\"codepage\":");
lstrcat(json, cpStr);
lstrcat(json, "}\n");
WriteFile(hComm, json, lstrlen(json), NULL, NULL);
}Modern side stores code page:
class Win32SerialBridge:
def __init__(self):
self.codepage = None
def handle_ready(self, msg):
self.codepage = msg.get('codepage', 1252) # Default Windows-1252
print(f"Win32s system code page: {self.codepage}")Explicit Non-Goal: We do NOT attempt to provide full Unicode support on Win32s 1.25a.
Rationale:
- Win32s has no Unicode APIs (no CreateFileW, MessageBoxW)
- Attempting Unicode would break Win32s compatibility
- Code page ANSI is the Win32s reality
- Base64 provides perfect byte fidelity where needed
Instead: We embrace the ANSI limitations and work within them safely.
Question: What about Japanese, Chinese, Korean filenames/content?
Win32s DBCS Reality:
- Win32s 1.25a DOES support DBCS for CJK languages
- Special DBCS code pages exist:
- Windows-932: Japanese (Shift-JIS)
- Windows-936: Simplified Chinese (GBK)
- Windows-949: Korean
- Windows-950: Traditional Chinese (Big5)
- These code pages use variable-width encoding (1 or 2 bytes per character)
DBCS vs Unicode:
- DBCS is NOT Unicode - it's a legacy multi-byte encoding
- Each CJK region has its own incompatible DBCS code page
- Win32s ANSI APIs work with DBCS IF the system code page matches
- Example: Japanese Windows 3.1 + Win32s uses cp932 (Shift-JIS)
Complexity: DBCS String Handling
DBCS strings require special care because some bytes can be both:
- Lead bytes (start of 2-byte character)
- Trail bytes (second half of 2-byte character)
/* INCORRECT - Naive ANSI assumes 1 byte = 1 char */
void BadBackslashSearch(const char* path) {
char* p = strrchr(path, '\\'); /* WRONG for DBCS! */
/* If '\\' byte appears as trail byte, this breaks! */
}
/* CORRECT - DBCS-aware string handling */
void GoodBackslashSearch(const char* path) {
const char* p = path;
const char* lastBackslash = NULL;
while (*p) {
if (IsDBCSLeadByte(*p)) {
p += 2; /* Skip 2-byte character */
} else {
if (*p == '\\') {
lastBackslash = p;
}
p++;
}
}
}Win32s DBCS APIs (Available in 1.25a):
IsDBCSLeadByte(BYTE)- Check if byte is DBCS lead byteCharNext()/CharPrev()- DBCS-aware string navigationlstrlen()- DBCS-safe (counts bytes, not characters)
Strategy for DBCS Support:
-
Detection: Win32s shell reports code page on startup
UINT codePage = GetACP(); /* e.g., 932 for Japanese */ /* Send: {"codepage":932,"dbcs":true} */
-
MCP Bridge Validation:
DBCS_CODEPAGES = {932, 936, 949, 950} def is_dbcs_codepage(cp): return cp in DBCS_CODEPAGES def validate_filename_dbcs(filename, codepage): """Validate filename for DBCS compatibility""" try: # Attempt to encode in target code page filename.encode(f'cp{codepage}') return True except UnicodeEncodeError: return False
-
File Content: Base64 handles DBCS perfectly
- Shift-JIS, GBK, Big5 text files → base64 → perfect round-trip
- Modern UTF-8 editing tools can transcode if needed
Recommendation for DBCS:
- ✅ Support it - Win32s 1.25a can handle DBCS
- ✅ Detect code page and validate filenames
- ✅ Use base64 for file contents (encoding-agnostic)
⚠️ Warn users that DBCS is region-specific (Japanese Win32s can't handle Chinese filenames)- ✅ Use
CharNext()/CharPrev()for path parsing on Win32s side
Issue: Very old segmented code (Windows 3.x) may be affected by modern CPU changes.
Background:
- Old x87 FPU (387 and earlier) had FCS and FDS registers
- These stored CS:DS segments at time of last FPU instruction
- Used for asynchronous FPU exception handling on 386
The Problem:
- 486+: FCS/FDS became unnecessary but kept for compatibility
- AMD64 (2003): 64-bit FPU instructions couldn't save/restore FCS/FDS properly
- Modern CPUs (Intel Ivy Bridge+, 2012): Reading FCS/FDS returns zero
- Impact: Breaks very old software that relies on FCS/FDS values
MCP-Win32s Mitigation: Complete Floating Point Ban
Our Solution: NO FLOATING POINT WHATSOEVER
We completely prohibit all floating-point operations in MCP-Win32s. See "Language Subset" section for full details.
Why This Solves The Problem:
- ✅ No FPU instructions = No FCS/FDS registers touched
- ✅ Integer-only code = Works identically on all CPUs (8087 through modern)
- ✅ No FPU state = No FSAVE/FRSTOR, no exception handling
- ✅ Deterministic = Same behavior on Win3.1+Win32s and Windows 11
- ✅ Simple = File I/O, JSON, commands don't need floating point anyway
Verification:
# Binary should contain ZERO FPU instructions
objdump -d mcp-w32s.exe | grep -E 'fld|fst|fadd|fmul'
# Empty output = Success!Who IS Still Affected (Not Us):
- ❌ WIN87EM.DLL (Windows 3.x FPU emulator) - uses FCS/FDS
- ❌ Very old 16-bit Windows apps with custom FPU exception handlers
- ❌ Code that uses FSAVE and examines segment fields
MCP-Win32s:
- ✅ Zero FPU usage = Zero FPU compatibility issues
- ✅ Integer-only arithmetic = Runs everywhere, always
- ✅ No floating point = Can't be broken by FCS/FDS deprecation
Reference: If running actual Windows 3.1 in a VM on modern CPU and encountering FPU issues, that's WIN87EM.DLL hitting the FCS/FDS=0 issue. Our Win32s code uses NO floating point, so we're completely immune.
Requirements:
- Must compile with Visual C++ 6.0 (via the
vc6CMake preset) - Must compile with MinGW on GitHub Actions (via the
mingwCMake preset) - CMake is the single source of truth:
CMakeLists.txt+CMakePresets.json+toolchains/. The VC6 toolchain uses CMake's "NMake Makefiles" generator (CMake removed its "Visual Studio 6" project generator in 3.6), so no IDE is needed.
Example:
# MinGW cross-compile on Linux / GitHub Actions
cmake --preset mingw
cmake --build --preset mingw
# Visual C++ 6.0, no IDE (NMake Makefiles generator, VC6 tools on PATH)
cmake --preset vc6
cmake --build --preset vc6build.sh and build.bat remain as thin wrappers (build.sh → mingw preset,
build.bat → vc6 preset). Build output is out-of-source in build/.
Must verify on:
- ✅ Windows 98 SE with VC++ 6.0 compiled binary
- ✅ Windows 11 with same VC++ 6.0 binary (forward compatibility)
- ✅ Windows 11 with MinGW-compiled binary (modern toolchain)
- ✅ GitHub Actions CI (MinGW cross-compile from Ubuntu)
Success = Same binary behavior across all platforms
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ MCP Client │ ◄─MCP──►│ Modern Machine │◄────────►│ Any Windows │
│ (Claude Code, │ │ MCP Bridge │ Serial │ Win32s → 11 │
│ Claude Desktop) │ │ Server │ TCP/IP │ + Dev Tools │
│ │ │ │ Pipes │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
Transport Options:
- Serial (RS-232): Win3.1, Win95/98/ME, embedded systems, retro hardware
- TCP Sockets: Modern Windows, VMs, containers, WSL
- Named Pipes: Local Windows, network shares, VM host-guest
- Telnet: Legacy networking, terminal servers
Flow:
- MCP client makes requests (file operations, command execution)
- MCP bridge translates to protocol (JSON over chosen transport)
- Win32 shell receives commands, executes in native Windows context
- Results returned via same transport
- MCP bridge formats response back to client
Why This Works: The same Win32 executable can run on any Windows version thanks to Microsoft's commitment to API compatibility. CreateFile, CreateProcess, and other Win32 APIs work identically from Windows 95 to Windows 11.
| Transport | Practical Minimum | Runtime Detection | Graceful Fallback |
|---|---|---|---|
| Serial | Win3.1 + Win32s 1.25a | Always available | N/A (baseline) |
| TCP/IP | WfW 3.11 + TCP/IP-32 | wsock32.dll check | → Serial |
| Named Pipes | Windows 95 | CreateNamedPipeA test | → TCP or Serial |
Design Philosophy: Serial is universal baseline. TCP requires WfW 3.11 as practical minimum, but we detect at runtime and gracefully fall back to serial if unavailable.
Supported: Windows 3.1 + Win32s 1.25a through Windows 11
The serial transport works on every Win32 platform because:
- COM ports existed before Windows
- ANSI CreateFileA("COM1:", ...) works universally
- No network stack required
- No additional drivers needed
Use Cases:
- Windows 3.1 + Win32s 1.25a (pre-WfW)
- Retro hardware without network cards
- Air-gapped systems
- Maximum compatibility testing
Practical Minimum: Windows for Workgroups 3.11 + Win32s 1.25a + Microsoft TCP/IP-32
Why WfW 3.11 as Practical Minimum?
- Microsoft TCP/IP-32 officially supports WfW 3.11
- Most stable Win32s networking platform
- 32-bit wsock32.dll included with TCP/IP-32
- Well-tested, widely deployed (1994-1995)
- Excellent retro hardware compatibility
Runtime Detection Strategy:
The current implementation (Phase 1) parses command-line flags and defaults to serial. Full runtime detection with LoadLibraryA probing arrives with the TCP backend (Phase 3); named pipe support is a later backend (Phase 5+).
/* serial.h - Transport configuration (actual implementation) */
#define TRANSPORT_SERIAL 1
#define TRANSPORT_TCP 2
#define TRANSPORT_PIPE 3
typedef struct {
int transport; /* TRANSPORT_SERIAL, TRANSPORT_TCP, etc. */
char port[32]; /* "COM1", "COM2", etc. */
DWORD baudRate; /* CBR_115200, CBR_57600, etc. */
int tcpPort; /* TCP port number (for TRANSPORT_TCP) */
char pipeName[260]; /* Named pipe path (for TRANSPORT_PIPE) */
} TransportConfig;
/* ParseCommandLine - extract transport from /SERIAL:COMx, /TCP:port, etc. */
int ParseCommandLine(const char *cmdLine, TransportConfig *config);
/* BuildSerialDCB - populate DCB with 8N1, no flow control */
void BuildSerialDCB(DWORD baudRate, DCB *dcb);
/* OpenSerialPort - CreateFileA + SetCommState + SetCommTimeouts */
HANDLE OpenSerialPort(const char *portName, DWORD baudRate);Planned runtime detection (not yet implemented):
TCP/IP availability will be detected at runtime by probing for wsock32.dll
with LoadLibraryA("wsock32.dll") and GetProcAddress. Named pipe support
will be detected via GetVersion() (Win95+ = version 4.0+). Fallback order:
TCP > Pipes > Serial. TCP lands in Phase 3 (transport); pipes in Phase 5+.
Command Line Options:
REM Auto-detect (tries TCP, falls back to serial)
mcp-w32s.exe
REM Force TCP (shows error if unavailable, falls back to serial)
mcp-w32s.exe /TCP:8932
REM Force serial (always works)
mcp-w32s.exe /SERIAL:COM1
REM Force named pipe (Win95+, falls back if unavailable)
mcp-w32s.exe /PIPE:\\.\\pipe\\mcpWinsock 1.1 APIs Used (Win32s Compatible):
/* All from wsock32.dll - available IF TCP/IP-32 installed */
WSAStartup(MAKEWORD(1,1), ...) /* Initialize Winsock 1.1 */
socket(AF_INET, SOCK_STREAM, 0) /* Create TCP socket */
bind(sock, &addr, sizeof(addr)) /* Bind to port */
listen(sock, backlog) /* Listen for connections */
accept(sock, &addr, &addrlen) /* Accept connection */
send(sock, buffer, len, 0) /* Send data */
recv(sock, buffer, len, 0) /* Receive data */
closesocket(sock) /* Close socket */
WSACleanup() /* Cleanup Winsock */
WSAGetLastError() /* Get error code */No Winsock 2.0 Dependencies:
- ❌ ws2_32.dll (Winsock 2)
- ❌ WSASocket, WSARecv, WSASend (Winsock 2)
- ❌ WSAEventSelect (Winsock 2)
- ❌ getaddrinfo (Winsock 2)
- ✅ Only Winsock 1.1 APIs (wsock32.dll)
TCP/IP Stack Availability by Platform:
| Platform | TCP/IP Status | Notes |
|---|---|---|
| Win3.1 + Win32s 1.25a | Complex setup, use serial instead | |
| WfW 3.11 + Win32s + TCP/IP-32 | ✅ Recommended minimum | Stable, well-supported |
| Win95/98/ME | ✅ Built-in | Native TCP/IP, no setup needed |
| WinNT 3.x/4.0 | ✅ Built-in | Native TCP/IP, no setup needed |
| Win2000+ | ✅ Built-in | Native TCP/IP, no setup needed |
Installing TCP/IP-32 on WfW 3.11:
- Download
tcp32b.exe(Microsoft TCP/IP-32 3.11b) - Run installer on WfW 3.11
- Configure network adapter (NE2000, 3C509, etc.)
- Set IP address, subnet mask, gateway, DNS
- Reboot Windows
- Verify: Check for
C:\WINDOWS\SYSTEM\WSOCK32.DLL - Our shell will auto-detect and use TCP!
Minimum OS: Windows 95
Named pipes use the same CreateFileA() API:
HANDLE hPipe = CreateFileA("\\\\.\\pipe\\mcp_win32s",
GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, 0, NULL);Runtime Detection:
/* Named pipes not in Win32s - detect by attempting to create */
int HasNamedPipeSupport(void) {
HANDLE hPipe;
hPipe = CreateNamedPipeA("\\\\.\\pipe\\test_detect",
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_BYTE | PIPE_WAIT,
1, 4096, 4096, 0, NULL);
if (hPipe == INVALID_HANDLE_VALUE) {
return 0; /* Not available (Win32s/Win3.x) */
}
CloseHandle(hPipe);
return 1; /* Available (Win95+) */
}Use Cases:
- VMs (VMware, VirtualBox, QEMU with pipe support)
- WSL ↔ Windows interop
- Local IPC testing
- Network pipes:
\\server\pipe\name
Not Available: Win32s 1.25a and Win3.x lack CreateNamedPipeA.
Design: All three transports share identical I/O via ReadFile/WriteFile. Phase 1 implements serial only; TCP and pipe support will follow this pattern:
/* Planned unified transport abstraction (not yet implemented) */
HANDLE OpenTransport(const char* spec) {
HANDLE hTransport = INVALID_HANDLE_VALUE;
if (strncmp(spec, "COM", 3) == 0) {
/* Serial: Always available */
hTransport = CreateFileA(spec,
GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, 0, NULL);
if (hTransport == INVALID_HANDLE_VALUE) {
MessageBoxA(NULL, "Failed to open serial port",
"Error", MB_OK | MB_ICONERROR);
}
}
else if (strncmp(spec, "TCP:", 4) == 0) {
/* TCP: Requires runtime detection */
if (!HasTcpIpStack()) {
MessageBoxA(NULL,
"TCP/IP stack not found (wsock32.dll missing)\n\n"
"Requires:\n"
" - WfW 3.11 + TCP/IP-32, or\n"
" - Windows 95 or later\n\n"
"Falling back to serial (COM1)",
"No TCP/IP", MB_OK | MB_ICONWARNING);
return OpenTransport("COM1");
}
hTransport = (HANDLE)CreateTcpListener(atoi(spec + 4));
}
else if (strncmp(spec, "PIPE:", 5) == 0) {
/* Named pipe: Win95+ only */
if (!HasNamedPipeSupport()) {
MessageBoxA(NULL,
"Named pipes not supported\n\n"
"Requires Windows 95 or later\n\n"
"Trying TCP/IP...",
"No Pipes", MB_OK | MB_ICONWARNING);
/* Try TCP, then serial */
if (HasTcpIpStack()) {
return OpenTransport("TCP:8932");
} else {
return OpenTransport("COM1");
}
}
hTransport = CreateFileA(spec + 5,
GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, 0, NULL);
}
return hTransport;
}
/* Same I/O functions for ALL transports! */
BOOL ReadTransport(HANDLE h, void* buf, DWORD size, DWORD* read) {
return ReadFile(h, buf, size, read, NULL);
}
BOOL WriteTransport(HANDLE h, const void* buf, DWORD size, DWORD* written) {
return WriteFile(h, buf, size, written, NULL);
}
BOOL CloseTransport(HANDLE h) {
return CloseHandle(h);
}Socket ↔ HANDLE Interoperability:
/* Windows allows using SOCKET as HANDLE for ReadFile/WriteFile */
SOCKET CreateTcpListener(int port) {
SOCKET sock;
struct sockaddr_in addr;
WSADATA wsaData;
int addrlen;
/* Initialize Winsock 1.1 */
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
return INVALID_SOCKET;
}
/* Create socket */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
WSACleanup();
return INVALID_SOCKET;
}
/* Bind to port */
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons((unsigned short)port);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
closesocket(sock);
WSACleanup();
return INVALID_SOCKET;
}
/* Listen for connections */
if (listen(sock, 1) != 0) {
closesocket(sock);
WSACleanup();
return INVALID_SOCKET;
}
/* Can now use as HANDLE with ReadFile/WriteFile! */
return sock;
}| Platform | Available Transports | Auto-Detect | Setup Required |
|---|---|---|---|
| Win3.1 + Win32s 1.25a | Serial only | ✅ Works | ✅ None |
| WfW 3.11 + Win32s + TCP/IP-32 | Serial + TCP | ✅ Detects both | |
| Win95/98/ME | All three | ✅ Detects all | ✅ None |
| WinNT 3.x/4.0 | All three | ✅ Detects all | ✅ None |
| Win2000/XP | All three | ✅ Detects all | ✅ None |
| Win7/10/11 | TCP + Pipe | ✅ Detects both | |
| VMs (VMware/QEMU) | TCP + Pipe preferred | ✅ Detects | ✅ None (bridged networking) |
User Experience Example:
C:\WINDOWS> mcp-w32s.exe
MCP-Win32s Bridge v1.0
═══════════════════════════════════════
Detecting available transports...
[✓] Serial ports: Available (COM1, COM2)
[✓] TCP/IP stack: Found (wsock32.dll v1.1)
Platform: WfW 3.11 + TCP/IP-32 3.11b
[✗] Named pipes: Not available (requires Win95+)
Starting TCP server on port 8932...
Fallback: COM1 (9600 8N1)
Waiting for MCP client connection...
Bottom Line:
- Win3.1 + Win32s: Serial only (baseline, always works)
- WfW 3.11 + TCP/IP-32: Serial or TCP (runtime detect, graceful fallback)
- Win95+: All three transports (runtime detect, auto-select best)
- Graceful degradation: Always functional, uses best available transport
Location: Any Windows machine (Win32s 1.25a through Windows 11)
Language: C89 (strict ANSI C)
Compilers: Visual C++ 1.x-2022, Borland C++, Watcom, MinGW, Clang
Purpose: Universal Windows command shell accessible via multiple transports
The Win32 shell uses only stable, forward-compatible Win32 APIs from Win32s 1.25a:
CreateFileA- Works identically Win32s 1.25a → Win11 (file I/O, COM ports, pipes, sockets)CreateProcessA- Process spawning unchanged across Windows versionsReadFile/WriteFile- Universal I/O across all handle typesWaitForSingleObject- Process synchronization
Key Point: An executable targeting Win32s 1.25a (1995) will run unmodified on modern Windows. This is the power we're leveraging.
Core APIs (present in Win32s 1.0):
-
kernel32.dllbasics:- File I/O: CreateFileA, ReadFile, WriteFile, CloseHandle
- Process: CreateProcessA, WaitForSingleObject, ExitProcess
- Memory: GlobalAlloc, GlobalFree, LocalAlloc, LocalFree
- Strings: lstrlen, lstrcpy, lstrcat, lstrcmp (ANSI only)
- Errors: GetLastError, SetLastError
-
user32.dllbasics:- MessageBoxA (for error reporting)
-
wsock32.dll(Winsock 1.1):- socket, bind, listen, accept, connect, send, recv, closesocket
- WSAStartup, WSACleanup, WSAGetLastError
- Note: Use
wsock32.dllNOTws2_32.dll(Winsock 2 came later)
Forbidden Modern APIs:
- ❌ Unicode: CreateFileW, MessageBoxW (not in Win32s)
- ❌ Winsock 2: ws2_32.dll, WSASocket, etc. (use Winsock 1.1)
- ❌ Overlapped I/O: OVERLAPPED structures (Win32s limitation)
- ❌ Named pipes for networking (file pipes OK)
- ❌ Security APIs: Much of advapi32.dll (not in Win32s)
Result: An executable compiled with these constraints in 1998 runs identically on modern Windows decades later.
Compiler Independence: By using only stable C89 and Win32s APIs, the code compiles with:
- Visual C++ 6.0 (1998) → produces 30-year-compatible binary
- Visual Studio 2022 → same binary compatibility
- MinGW-w64 → same binary compatibility
- All produce functionally identical executables
- Transport Abstraction: Single codebase supports serial, TCP, and named pipes
- Command Parser: Parse incoming protocol messages
- Command Executor: Execute commands via
CreateProcess()orsystem() - Output Capture: Capture stdout/stderr from child processes
- File Operations: Read/write/list files on Windows filesystem
- Error Handling: Return error codes and messages
The Win32 shell auto-detects or accepts a transport mode flag:
REM Serial mode (default on Win95/98/ME if COM port available)
mcp-w32s.exe /SERIAL:COM1
REM TCP server mode (modern Windows)
mcp-w32s.exe /TCP:8932
REM Named pipe mode (VMs, WSL)
mcp-w32s.exe /PIPE:\\.\pipe\mcp-win32s
REM Auto-detect (tries TCP, then serial, then pipe)
mcp-w32s.exeImplementation Note: CreateFile works for ALL these transports on Windows:
CreateFile("COM1:", ...)- Serial portCreateFile("\\\\.\\pipe\\name", ...)- Named pipe- For TCP: Use Winsock (
WSASocket,bind,listen,accept)- ws2_32.lib available since Windows 95
Command Format (JSON-like, newline-delimited):
{"cmd":"exec","id":"123","argv":["cl","/c","test.c"],"timeout_ms":30000}
{"cmd":"exec","id":"123b","line":"cl /c test.c"}
{"cmd":"ptyExec","id":"123c","argv":["cmd"],"cols":80,"rows":25}
{"cmd":"read","id":"124","path":"C:\\PROJECTS\\test.c"}
{"cmd":"write","id":"125","path":"C:\\PROJECTS\\new.c","data":"...base64..."}
{"cmd":"list","id":"126","path":"C:\\PROJECTS"}
{"cmd":"delete","id":"127","path":"C:\\PROJECTS\\old.obj"}exec accepts argv (preferred) or legacy line (if both, argv wins),
plus cwd, shell, timeout_ms (0/omitted = server default 55000ms,
ceiling 600000ms), stdin_b64 (max 4096 decoded bytes), max_output
(0/omitted = 65536 per stream, larger clamps), unsafe (per-request
catalog bypass), mem_cap_bytes/cpu_time_ms (job-object limits,
NT 4.0+ only). ptyExec adds cols/rows and requires Windows 10
1809+ (features.pty in the ready message).
Response Format:
{"id":"123","status":"ok","exit_code":0,"stdout_b64":"...","stderr_b64":"",
"stdout_truncated":false,"stderr_truncated":false,"duration_ms":47,
"exec_method":"direct","binary_type":"pe32","killed_by":""}
{"id":"123c","status":"ok","exit_code":0,"output_b64":"...","output_kind":"ansi",
"output_truncated":false,"duration_ms":123}
{"id":"123","status":"error","error":"timed out"}
{"id":"123","status":"error","error":"busy","blocking_cmd_line":"...","elapsed_ms":1500}
{"id":"124","status":"ok","data":"...base64..."}
{"id":"126","status":"ok","files":"test.c\nmain.c"}exec stdout/stderr are ALWAYS base64 (raw OEM-codepage bytes; decode
using codepage from the ready message). exec_method is one of
direct|shell|vdm-best-effort; binary_type one of
pe32|ne16|mz|shell-builtin|unknown; killed_by one of
""|timeout|ctrl_break|memory_cap|cpu_cap. A response with
"unsafe_used":true records a per-request whitelist bypass. Error
reasons include: busy (a deliberately-unkilled 16-bit child is still
running; the response names it and its elapsed ms), stdin too large,
command not in catalog, argument not allowed, command line too long, invalid base64, timed out, spawn failed: <code>, and
pty not available on this Windows (ptyExec only).
catalog/win32-commands.json ships next to the binary (override with
/CATALOG:<path>): 30 era-tagged commands with typed options that map
1:1 onto MCP tool definitions (catalog/MCP-MAPPING.md). With the
catalog loaded, exec only runs catalogued commands with catalogued
flags; shell built-ins (dir, copy, ...) auto-route through the
era-correct shell (cmd.exe / command.com) and report
exec_method:"shell". /UNSAFE disables enforcement server-wide; a
per-request "unsafe":true bypasses it for one exec and is reported
back as unsafe_used. The catalog is fixed per session (restart to
reload). A missing catalog adds "warning":"catalog not loaded" to the
ready message and disables enforcement.
The binary's baseline is Win32s 1.25a; at startup src/feat.c probes
newer APIs via GetProcAddress (never the import table - CI enforces
this) and uplifts at runtime:
| Capability | Min OS | Used for | Baseline fallback |
|---|---|---|---|
| Threads | Win 95 / NT | reader threads in the capture loop | PeekNamedPipe polling |
| Job objects | NT 4.0 | kill-on-close containment + mem_cap_bytes/cpu_time_ms |
children survive a server crash; no caps |
GenerateConsoleCtrlEvent |
NT 4.0 | graceful Ctrl-Break before TerminateProcess on timeout |
direct terminate |
GetBinaryTypeA |
NT / 95 | binary_type classification |
manual MZ/NE/PE header read |
CreatePseudoConsole |
Win 10 1809 | the ptyExec command |
explicit pty not available error |
The per-connection ready message advertises the live feature set:
{"status":"ready","codepage":437,"version":"Windows 10.0.19045 (NT)",
"transport":"tcp","features":{"is_win32s":false,"is_win9x":false,
"is_nt":true,"is_wow64":false,"threads":true,"job_objects":true,
"ctrl_events":true,"pty":true,"binary_classify":"GetBinaryTypeA",
"process_mitigation":false}}WaitForSingleObject(hProcess)returns immediately on Win32s (KB Q125213): the capture loop pollsGetExitCodeProcessuntil it stops returningSTILL_ACTIVE.- The era shell is
COMMAND.COMon Win 3.x,cmd.exeon NT/95+; the catalog records both forms per built-in. - A timed-out 16-bit child is never terminated: it shares the VDM
with unrelated processes (killing it can take siblings down). The
child is left running ("still_active", after
GetExitCodeProcess's own sentinel), the request gets an in-bandtimed outerror, and subsequent exec/ptyExec requests getbusy- naming the blocking command line and its elapsed ms - until a later request's re-poll observes it exited (implicit reap).
The actual implementation splits into three source files: serial.c (port init + config),
json_parser.c (protocol parsing), and mcp-w32s.c (main loop + dispatch). Key excerpts:
/* serial.c - Serial port configuration (actual implementation) */
HANDLE OpenSerialPort(const char *portName, DWORD baudRate)
{
HANDLE hPort;
DCB dcb;
COMMTIMEOUTS timeouts;
hPort = CreateFileA(portName,
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hPort == INVALID_HANDLE_VALUE) {
return INVALID_HANDLE_VALUE;
}
BuildSerialDCB(baudRate, &dcb); /* 8N1, no flow control */
SetCommState(hPort, &dcb);
BuildSerialTimeouts(&timeouts); /* 50ms interval, 10ms multiplier */
SetCommTimeouts(hPort, &timeouts);
return hPort;
}/* mcp-w32s.c - Main executable (actual implementation) */
#include <windows.h>
#include <string.h>
#include "common.h"
#include "json_parser.h"
#include "serial.h"
#define READY_MESSAGE "MCP_WIN32S_READY\n"
/* ProcessBuffer accumulates bytes and splits on newline boundaries */
int ProcessBuffer(LineBuffer *buf, const char *input, int inputLen,
void (*handler)(const char *line, HANDLE hOutput),
HANDLE hOutput)
{
int i;
int lines;
lines = 0;
for (i = 0; i < inputLen; i++) {
if (input[i] == '\n') {
buf->data[buf->pos] = '\0';
if (handler != NULL) {
handler(buf->data, hOutput);
}
buf->pos = 0;
lines++;
} else if (buf->pos < CMD_BUF_SIZE - 1) {
buf->data[buf->pos++] = input[i];
}
}
return lines;
}
/* ProcessCommand parses JSON, dispatches by cmd, writes response */
void ProcessCommand(const char *line, HANDLE hOutput)
{
JsonCommand cmd;
char response[MCP_MAX_RESPONSE];
int responseLen;
DWORD bytesWritten;
if (line == NULL || line[0] == '\0') {
return;
}
if (!ParseJsonCommand(line, &cmd)) {
responseLen = BuildJsonResponse("", "error", "error",
"invalid JSON", response,
sizeof(response));
if (responseLen > 0 && hOutput != INVALID_HANDLE_VALUE) {
WriteFile(hOutput, response, (DWORD)responseLen,
&bytesWritten, NULL);
}
return;
}
/* read/write/list/delete handlers landed in Phase 2; exec arrives in Phase 4 */
/* For now, acknowledge known commands */
if (strcmp(cmd.cmd, "exec") == 0 || strcmp(cmd.cmd, "read") == 0 ||
strcmp(cmd.cmd, "write") == 0 || strcmp(cmd.cmd, "list") == 0 ||
strcmp(cmd.cmd, "delete") == 0) {
responseLen = BuildJsonResponse(cmd.id, "ok", "message",
"command received",
response, sizeof(response));
} else {
responseLen = BuildJsonResponse(cmd.id, "error", "error",
"unknown command",
response, sizeof(response));
}
if (responseLen > 0 && hOutput != INVALID_HANDLE_VALUE) {
WriteFile(hOutput, response, (DWORD)responseLen,
&bytesWritten, NULL);
}
}
int main(void)
{
TransportConfig config;
HANDLE hTransport;
ParseCommandLine(GetCommandLineA(), &config);
hTransport = OpenSerialPort(config.port, config.baudRate);
/* ... send MCP_WIN32S_READY, enter MainLoop ... */
return 0;
}Key Points:
- Pure C89 - no C99/C11 features
- ANSI APIs only (CreateFileA, not CreateFileW)
- No STL, exceptions, or modern C++
- Old-style declarations (all vars at top of block)
- Win32s-era string functions (lstrlen, lstrcat)
- Compiles with VC++ 6.0 and modern MinGW
Strict Requirements:
- C89-compatible source code only
- ANSI Win32 APIs (no Unicode)
- Win32s-era APIs only (kernel32, user32, wsock32)
- No modern CRT features
- ABSOLUTELY NO FLOATING POINT (integer arithmetic only)
- i386 CPU target only (no 486+ instructions)
# MinGW-w64 (cross-compile from Linux for GitHub Actions)
cmake --preset mingw
cmake --build --preset mingw
# Visual C++ 6.0, no IDE (VC6 tools on PATH via VCVARS32.BAT)
cmake --preset vc6
cmake --build --preset vc6The presets pull their Win32s-critical flags from the toolchain files:
toolchains/mingw-w64-i386.cmake (-std=c89 -march=i386 -mtune=i386 -Wall -Wdouble-promotion -Wfloat-equal, image base 0x10000) and
toolchains/vc6-nmake.cmake (/TC /G3 /FIXED:NO /BASE:0x10000).
Critical Flags (set in the toolchain files):
/TCor-std=c89: Force C89 compilation/G3or-march=i386: Target 386 CPU (no 486+)- No
/D_UNICODE: ANSI only for Win32s wsock32.libnotws2_32.lib: Win32s has Winsock 1.1 only
Testing the binary:
REM The SAME .exe should run on:
REM - Windows 3.1 + Win32s 1.25a (1995)
REM - Windows 11 - decades later!
mcp-w32s.exe /SERIAL:COM1Verification Checklist:
- Compiles with VC++ 6.0 (or earlier)
- Compiles with MinGW-w64 (modern CI)
- Uses
/BASE:0x10000and/FIXED:NO(or equivalent) - Links only against kernel32.lib, user32.lib, wsock32.lib
- No Unicode APIs (all CreateFileA, not CreateFileW)
- Runs on Windows 3.1 + Win32s 1.25a (primary test!)
- Runs on Windows 11 (forward compatibility test!)
Challenge: Win32s has no JSON libraries. Modern C libraries may not compile with VC++ 6.0.
Solution: Minimal hand-coded JSON parser in C89.
/* Simple JSON parser - C89 compatible, no dependencies */
typedef struct {
char cmd[32];
char id[32];
char path[MAX_PATH];
char line[1024];
char data[65536]; /* base64 encoded data */
} JsonCommand;
/* Parse: {"cmd":"exec","id":"123","line":"dir"} */
int ParseJsonCommand(const char* json, JsonCommand* out) {
const char* p;
char key[32];
char value[1024];
memset(out, 0, sizeof(JsonCommand));
/* Very simple parser - find "key":"value" pairs */
p = json;
while (*p) {
/* Skip to opening quote of key */
while (*p && *p != '"') p++;
if (!*p) break;
p++; /* skip " */
/* Extract key and value... */
/* Assign to struct based on key */
}
return 1; /* success */
}
/* Build: {"id":"123","status":"ok","output":"..."} */
void BuildJsonResponse(const JsonCommand* cmd, const char* status,
const char* output, char* json) {
/* Simple string concatenation - C89 safe */
lstrcpy(json, "{\"id\":\"");
lstrcat(json, cmd->id);
lstrcat(json, "\",\"status\":\"");
lstrcat(json, status);
lstrcat(json, "\"");
if (output && *output) {
lstrcat(json, ",\"output\":\"");
/* Escape special chars in output */
lstrcat(json, "\"");
}
lstrcat(json, "}\n");
}Rationale:
- No external dependencies (cJSON, jansson won't compile on VC++ 6.0)
- Simple subset of JSON (no nested objects, arrays)
- Hand-coded parser is ~200 lines of C89
- Compiles everywhere (VC++ 6.0, MinGW, VS2022)
- Sufficient for our protocol needs
Challenge: No test frameworks work on VC++ 6.0. Unity, CUnit, etc. require modern C features.
Solution: ~100 line test framework in pure C89.
/* test_framework.h - Minimal C89 test framework */
#ifndef TEST_FRAMEWORK_H
#define TEST_FRAMEWORK_H
#include <stdio.h>
#include <string.h>
/* Global test counters */
static int g_tests_run = 0;
static int g_tests_failed = 0;
static const char* g_current_test = NULL;
/* Test assertion macros */
#define TEST_ASSERT(condition, message) \
do { \
if (!(condition)) { \
printf(" FAIL: %s - %s\n", g_current_test, message); \
g_tests_failed++; \
return; \
} \
} while(0)
#define TEST_ASSERT_EQUAL(expected, actual, message) \
TEST_ASSERT((expected) == (actual), message)
#define TEST_ASSERT_STR_EQUAL(expected, actual, message) \
TEST_ASSERT(lstrcmp(expected, actual) == 0, message)
/* Test runner macros */
#define TEST_CASE(name) \
void test_##name(void); \
void run_##name(void) { \
g_current_test = #name; \
g_tests_run++; \
test_##name(); \
} \
void test_##name(void)
#define RUN_TEST(name) \
do { \
printf("Running: %s\n", #name); \
run_##name(); \
} while(0)
/* Test suite reporting */
void print_test_summary(void) {
printf("\n========================================\n");
printf("Tests run: %d\n", g_tests_run);
printf("Tests failed: %d\n", g_tests_failed);
printf("Tests passed: %d\n", g_tests_run - g_tests_failed);
printf("========================================\n");
if (g_tests_failed == 0) {
printf("ALL TESTS PASSED\n");
} else {
printf("SOME TESTS FAILED\n");
}
}
int get_test_failures(void) {
return g_tests_failed;
}
#endif /* TEST_FRAMEWORK_H *//* test_json.c - JSON parser tests */
#include "test_framework.h"
#include "json_parser.h"
TEST_CASE(json_parse_simple_command) {
JsonCommand cmd;
const char* json = "{\"cmd\":\"exec\",\"id\":\"123\",\"line\":\"dir\"}";
int result;
result = ParseJsonCommand(json, &cmd);
TEST_ASSERT_EQUAL(1, result, "Parse should succeed");
TEST_ASSERT_STR_EQUAL("exec", cmd.cmd, "Command type");
TEST_ASSERT_STR_EQUAL("123", cmd.id, "Command ID");
TEST_ASSERT_STR_EQUAL("dir", cmd.line, "Command line");
}
TEST_CASE(json_parse_with_path) {
JsonCommand cmd;
const char* json = "{\"cmd\":\"read\",\"id\":\"456\",\"path\":\"C:\\\\test.txt\"}";
int result;
result = ParseJsonCommand(json, &cmd);
TEST_ASSERT_EQUAL(1, result, "Parse should succeed");
TEST_ASSERT_STR_EQUAL("read", cmd.cmd, "Command type");
TEST_ASSERT_STR_EQUAL("C:\\test.txt", cmd.path, "File path");
}
TEST_CASE(json_build_simple_response) {
JsonCommand cmd;
char json[1024];
lstrcpy(cmd.id, "789");
BuildJsonResponse(&cmd, "ok", "test output", json);
/* Basic validation - contains expected substrings */
TEST_ASSERT(strstr(json, "\"id\":\"789\"") != NULL, "ID in response");
TEST_ASSERT(strstr(json, "\"status\":\"ok\"") != NULL, "Status in response");
TEST_ASSERT(strstr(json, "test output") != NULL, "Output in response");
}
TEST_CASE(json_build_error_response) {
JsonCommand cmd;
char json[1024];
lstrcpy(cmd.id, "999");
BuildJsonResponse(&cmd, "error", NULL, json);
TEST_ASSERT(strstr(json, "\"status\":\"error\"") != NULL, "Error status");
}
/* Test runner */
int main(void) {
printf("JSON Parser Test Suite\n");
printf("========================================\n");
RUN_TEST(json_parse_simple_command);
RUN_TEST(json_parse_with_path);
RUN_TEST(json_build_simple_response);
RUN_TEST(json_build_error_response);
print_test_summary();
return get_test_failures();
}/* test_file_ops.c - File operation tests */
#include "test_framework.h"
#include "mcp-w32s.h"
#include <windows.h>
TEST_CASE(file_read_success) {
const char* testFile = "C:\\TEMP\\test_read.txt";
char output[1024];
HANDLE hFile;
DWORD written;
int result;
/* Create test file */
hFile = CreateFileA(testFile, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
TEST_ASSERT(hFile != INVALID_HANDLE_VALUE, "Create test file");
WriteFile(hFile, "test content", 12, &written, NULL);
CloseHandle(hFile);
/* Test reading */
result = ReadFileContent(testFile, output, sizeof(output));
TEST_ASSERT_EQUAL(1, result, "Read should succeed");
TEST_ASSERT_STR_EQUAL("test content", output, "Content matches");
/* Cleanup */
DeleteFileA(testFile);
}
TEST_CASE(file_write_success) {
const char* testFile = "C:\\TEMP\\test_write.txt";
const char* content = "new content";
char verify[1024];
HANDLE hFile;
DWORD read;
int result;
/* Write file */
result = WriteFileContent(testFile, content, lstrlen(content));
TEST_ASSERT_EQUAL(1, result, "Write should succeed");
/* Verify content */
hFile = CreateFileA(testFile, GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
TEST_ASSERT(hFile != INVALID_HANDLE_VALUE, "Open for verification");
ReadFile(hFile, verify, sizeof(verify)-1, &read, NULL);
verify[read] = '\0';
CloseHandle(hFile);
TEST_ASSERT_STR_EQUAL(content, verify, "Written content matches");
/* Cleanup */
DeleteFileA(testFile);
}
TEST_CASE(file_list_directory) {
WIN32_FIND_DATAA findData;
HANDLE hFind;
int fileCount;
/* List C:\WINDOWS directory (always exists) */
hFind = FindFirstFileA("C:\\WINDOWS\\*.*", &findData);
TEST_ASSERT(hFind != INVALID_HANDLE_VALUE, "FindFirstFile succeeds");
fileCount = 0;
do {
fileCount++;
} while (FindNextFileA(hFind, &findData));
FindClose(hFind);
TEST_ASSERT(fileCount > 0, "Directory contains files");
}
int main(void) {
printf("File Operations Test Suite\n");
printf("========================================\n");
RUN_TEST(file_read_success);
RUN_TEST(file_write_success);
RUN_TEST(file_list_directory);
print_test_summary();
return get_test_failures();
}# Build and run the test binaries via the mingw preset
cmake --preset mingw
cmake --build --preset mingw
ctest --preset mingw # runs the 7 test binariesctest --preset mingw runs the test PEs natively via WSL interop on the dev
host (real kernel32/wsock32), and under Wine in CI. build.sh test is a
thin wrapper that does the same. The VC6 toolchain builds the same test
binaries via cmake --preset vc6 && cmake --build --preset vc6.
# .github/workflows/build-and-test.yml (actual, simplified)
name: Build and Test MCP-Win32s
on: [push, pull_request]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install MinGW-w64, Wine, CMake and Ninja
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y mingw-w64 wine wine32 cmake ninja-build
# CMakeLists.txt is the single source of truth; the "mingw" preset carries
# the strict C89/i386/Win32s flags (toolchains/mingw-w64-i386.cmake).
- name: Configure, build, test
run: |
cmake --preset mingw
cmake --build --preset mingw
ctest --preset mingw # all 7 suites; Wine on the runner
- name: Verify no static Winsock import (TCP is runtime-loaded)
run: |
! i686-w64-mingw32-objdump -p build/mingw/mcp-w32s.exe \
| grep -i 'wsock32\|ws2_32'
- name: Verify no FPU/486+ instructions in application code
run: |
# Disassemble the app objects CMake built, grep for forbidden insns.
i686-w64-mingw32-objdump -d build/mingw/CMakeFiles/mcp-w32s.dir/src/*.obj \
> app_disasm.txt
! grep -E 'fld|fst[^r]|fadd|fmul|fdiv|fsqrt' app_disasm.txt
! grep -iE 'cpuid|cmpxchg|bswap|xadd|rdtsc' app_disasm.txtMCP-Win32s/
├── src/
│ ├── common.h # Shared types (JsonCommand struct)
│ ├── json_parser.c # JSON parsing + response building
│ ├── json_parser.h # Parser/builder public API
│ ├── mcp-w32s.c # Main executable (protocol loop, dispatch)
│ ├── serial.c # Serial port init + config builders
│ └── serial.h # Serial/transport API
├── tests/
│ ├── test_framework.h # Minimal C89 test framework (header-only)
│ ├── test_json.c # 31 JSON parser unit tests
│ └── test_serial.c # 28 serial + main loop tests
├── toolchains/
│ ├── mingw-w64-i386.cmake # MinGW cross-compile toolchain (flags + image base)
│ └── vc6-nmake.cmake # VC++ 6.0 toolchain (NMake Makefiles generator)
├── CMakeLists.txt # Single source of truth for the build
├── CMakePresets.json # mingw + vc6 presets
├── build.bat # Thin wrapper → vc6 preset
├── build.sh # Thin wrapper → mingw preset
└── README.md
Unit Tests:
- ✅ JSON parsing (valid/invalid input)
- ✅ JSON response building
- ✅ File read/write operations
- ✅ Directory listing
- ✅ Command execution
- ✅ Serial port initialization (requires hardware or mock)
- ✅ Base64 encoding/decoding
Integration Tests:
- ✅ End-to-end command execution
- ✅ File transfer accuracy
- ✅ Protocol compliance
- ✅ Error handling paths
Platform Tests:
- ✅ Windows 3.1 + Win32s 1.25a (VC++ 6.0 binary) - PRIMARY TARGET
- ✅ Windows 95 (same VC++ 6.0 binary)
- ✅ Windows 98 SE (same VC++ 6.0 binary)
- ✅ Windows ME (same VC++ 6.0 binary)
- ✅ Windows NT 3.51 (same VC++ 6.0 binary)
- ✅ Windows NT 4.0 (same VC++ 6.0 binary)
- ✅ Windows 2000 (same VC++ 6.0 binary)
- ✅ Windows XP (same VC++ 6.0 binary)
- ✅ Windows 7 (same VC++ 6.0 binary)
- ✅ Windows 10 (same VC++ 6.0 binary)
- ✅ Windows 11 (VC++ 6.0 and MinGW binaries) - FORWARD COMPAT TEST
REM Build with the VC6 toolchain (VC6 tools on PATH via VCVARS32.BAT),
REM or cross-compile elsewhere with the mingw preset and copy the .exe over.
cmake --preset vc6
cmake --build --preset vc6
REM Run a test binary (output lands in build\)
build\test_json.exe
echo Test result: %ERRORLEVEL%
REM On Windows 3.1+Win32s 1.25a through Win 11 - same binary!
REM Just copy the .exe built above
test_json.exeBenefits:
- ✅ No external dependencies (no CUnit, Unity, etc.)
- ✅ Compiles with VC++ 6.0 (1998) and earlier
- ✅ Compiles with MinGW (modern CI)
- ✅ Runs on Windows 3.1 + Win32s 1.25a through Windows 11
- ✅ Simple, readable test code
- ✅ ~100 lines for framework, easy to understand
- ✅ Compatible with Wine for Linux CI testing
Critical: Always test on actual Windows 3.1 + Win32s 1.25a hardware/VM to verify true compatibility!
# .github/workflows/build.yml
name: Build MCP-Win32s
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install MinGW, CMake and Ninja
run: |
sudo apt-get update
sudo apt-get install -y mingw-w64 cmake ninja-build
- name: Build Win32 Shell (C89 strict, i386 target)
run: |
cmake --preset mingw
cmake --build --preset mingw
- name: Verify i386-only instructions (no 486+)
run: |
# Application objects only (CRT startup legitimately has FPU/atomics).
i686-w64-mingw32-objdump -d build/mingw/CMakeFiles/mcp-w32s.dir/src/*.obj > disasm.txt
# Check for forbidden 486+ instructions
if grep -iE 'cpuid|cmpxchg|bswap|xadd' disasm.txt; then
echo "ERROR: 486+ instructions found!"
exit 1
fi
# Check for forbidden Pentium+ instructions
if grep -iE 'rdtsc|cmov|cmpxchg8b' disasm.txt; then
echo "ERROR: Pentium+ instructions found!"
exit 1
fi
echo "✓ i386-only instructions verified - no 486+ detected"
- name: Verify no floating point usage
run: |
# Binary must contain ZERO FPU instructions
if grep -E 'fld|fst|fadd|fsub|fmul|fdiv|fsqrt|fsin|fcos' disasm.txt; then
echo "ERROR: Floating point instructions found!"
exit 1
fi
echo "✓ No floating point instructions - integer-only verified"
- name: Verify no Unicode APIs
run: |
# Check that binary uses only ANSI functions
i686-w64-mingw32-objdump -p build/mingw/mcp-w32s.exe | grep -v "CreateFileW"
i686-w64-mingw32-objdump -p build/mingw/mcp-w32s.exe | grep -v "MessageBoxW"
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: mcp-w32s-ci-build
path: mcp-w32s.exeTesting on Windows:
test-windows:
needs: build
runs-on: windows-latest
steps:
- uses: actions/download-artifact@v3
with:
name: mcp-w32s-ci-build
- name: Test on Windows 11
run: |
# MinGW binary should run on Windows 11
./mcp-w32s.exe --versionThis ensures every commit produces a Win32s-compatible binary that works on Windows 11!
Location: Modern Linux/Windows/macOS machine with serial port
Language: Python 3.9+
Purpose: Translate MCP protocol requests to serial commands for Win32 systems
- MCP Server Implementation: Implement MCP protocol handlers
- Serial Communication: pySerial for COM port access
- Command Translation: Map MCP file/exec operations to serial protocol
- Path Translation: Convert Unix paths to Win98 paths (C:\PROJECTS...)
- Response Parsing: Parse JSON responses from Win98 shell
- Timeout Handling: Handle serial timeouts and retries
pip install pyserial mcp# mcp_serial_bridge.py
import serial
import json
import base64
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# Serial port configuration
SERIAL_PORT = "/dev/ttyUSB0" # Adjust for your system
BAUD_RATE = 115200
class Win32SerialBridge:
def __init__(self, port, baud):
self.serial = serial.Serial(port, baud, timeout=2)
self.cmd_id = 0
def send_command(self, cmd_type, **kwargs):
self.cmd_id += 1
cmd = {"cmd": cmd_type, "id": str(self.cmd_id)}
cmd.update(kwargs)
json_cmd = json.dumps(cmd) + "\n"
self.serial.write(json_cmd.encode('utf-8'))
# Read response (wait for newline)
response_line = self.serial.readline().decode('utf-8')
return json.loads(response_line)
def exec_command(self, cmdline):
resp = self.send_command("exec", line=cmdline)
if resp.get("status") == "ok":
return resp.get("output", "")
else:
raise Exception(resp.get("error", "Command failed"))
def read_file(self, path):
resp = self.send_command("read", path=path)
if resp.get("status") == "ok":
return base64.b64decode(resp["data"])
else:
raise Exception(resp.get("error", "Read failed"))
def write_file(self, path, content):
b64_data = base64.b64encode(content).decode('utf-8')
resp = self.send_command("write", path=path, data=b64_data)
if resp.get("status") != "ok":
raise Exception(resp.get("error", "Write failed"))
def list_dir(self, path):
resp = self.send_command("list", path=path)
if resp.get("status") == "ok":
return resp.get("files", [])
else:
raise Exception(resp.get("error", "List failed"))
# MCP Server implementation
app = Server("mcp-win32s")
bridge = None
@app.list_tools()
async def list_tools():
return [
Tool(
name="win32_exec",
description="Execute a command on legacy Win32 machine",
inputSchema={
"type": "object",
"properties": {
"command": {"type": "string", "description": "Command to execute"}
},
"required": ["command"]
}
),
Tool(
name="win32_read_file",
description="Read a file from Win32 filesystem",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "description": "Windows path (e.g., C:\\PROJECTS\\file.c)"}
},
"required": ["path"]
}
),
Tool(
name="win32_write_file",
description="Write a file to Win32 filesystem",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"}
},
"required": ["path", "content"]
}
),
Tool(
name="win32_list_dir",
description="List directory contents on Win32 machine",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
global bridge
if name == "win32_exec":
output = bridge.exec_command(arguments["command"])
return [TextContent(type="text", text=output)]
elif name == "win32_read_file":
content = bridge.read_file(arguments["path"])
return [TextContent(type="text", text=content.decode('utf-8', errors='replace'))]
elif name == "win32_write_file":
bridge.write_file(arguments["path"], arguments["content"].encode('utf-8'))
return [TextContent(type="text", text=f"File written: {arguments['path']}")]
elif name == "win32_list_dir":
files = bridge.list_dir(arguments["path"])
return [TextContent(type="text", text="\n".join(files))]
async def main():
global bridge
bridge = Win32SerialBridge(SERIAL_PORT, BAUD_RATE)
# Wait for Win32 shell ready message
ready_msg = bridge.serial.readline().decode('utf-8')
if "MCP_WIN32S_READY" not in ready_msg:
raise Exception("Win32 shell not ready")
async with stdio_server() as (read_stream, write_stream):
await app.run(read_stream, write_stream, app.create_initialization_options())
if __name__ == "__main__":
import asyncio
asyncio.run(main())MCP Client Config (e.g., Claude Code ~/.config/claude-code/mcp.json or Claude Desktop config):
{
"mcpServers": {
"win32s": {
"command": "python3",
"args": ["/path/to/mcp-win32s/mcp_serial_bridge.py"],
"env": {}
}
}
}Option A: Direct Serial Cable
- Null-modem serial cable (DB9 female-to-female with crossover)
- Connect modern PC serial port to Win98 COM1/COM2
- If modern PC lacks serial port, use USB-to-Serial adapter
Option B: Serial over Network (ser2net)
- Run
ser2neton intermediate machine with serial port - MCP bridge connects via telnet to ser2net
- More flexible for remote Win98 machines
Wiring for Null-Modem Cable:
Modern PC Win98
Pin 2 (TX) <---> Pin 3 (RX)
Pin 3 (RX) <---> Pin 2 (TX)
Pin 5 (GND) <---> Pin 5 (GND)
Pin 7 (RTS) <---> Pin 8 (CTS) [Optional for hardware flow control]
Pin 8 (CTS) <---> Pin 7 (RTS) [Optional for hardware flow control]
Recommended: 115200 baud (reliable on short cables <6ft)
Alternative: 57600 baud (more stable on longer cables)
Flow Control: Software (XON/XOFF) or None
Delivered:
- Minimal C89 test framework (
tests/test_framework.h, header-only) - Hand-coded JSON parser (
src/json_parser.c+src/json_parser.h, ~334 lines C89) - Shared types header (
src/common.h, JsonCommand struct) - Serial port initialization + transport config (
src/serial.c+src/serial.h) - Main executable with protocol loop and command dispatch stub (
src/mcp-w32s.c) - CMake build (
CMakeLists.txt+CMakePresets.json+toolchains/); VC6 via thevc6preset (NMake Makefiles generator, no IDE) - 31 JSON parser unit tests (
tests/test_json.c) - 28 serial + main loop unit tests (
tests/test_serial.c) - MinGW cross-compile wrapper (
build.sh→ mingw preset) - VC++ 6.0 wrapper (
build.bat→ vc6 preset) - GitHub Actions CI: C89/i386 compilation, Wine test execution, FPU instruction verification, 486+ instruction verification
- All 59 tests pass under Wine on Linux CI
- Echo command: echo back command data
- Base64 encode/decode (
base64.c/.h) - Implement file read/write/list/delete operations (
file_ops.c/.h) - Unit tests for base64 (14 tests) and file operations (10 tests)
- Test with HyperTerminal or equivalent
Make the network a first-class peer of the serial port. See plan/PHASE3.md in the agentic host repo (Agentic-MCP-Win32s) for the fully worked plan.
- Transport abstraction: vtable interface (
read/write/close/optionalaccept) + runtime backend registry (src/transport.c/.h) - Refactor serial onto the abstraction (
SerialBackendOpen); moveTransportConfig/ParseCommandLinetotransport.c - TCP backend over Winsock 1.1, runtime-probed via
LoadLibraryA("wsock32.dll")(src/tcp.c/.h); single-client-sequential server - Mock transport backend for asserting response bytes in tests (
tests/mock_transport.c/.h) -
specs/transport.allium; retarget protocol core off rawHANDLEontoTransport * - Tests:
test_transport(≥10),test_tcp(≥6, loopback), mock-backedtest_serial - CI assertion: binary does not statically import
wsock32/ws2_32(still loads on bare Win32s)
- Add
execcommand handler (process execution, stdout/stderr capture) - Implement full JSON command/response protocol loop
- Implement command/response protocol with timeout and retry logic
- Unit tests for command execution
- Test executing simple commands (
dir,cl /?)
- Implement MCP server skeleton (Python bridge)
- Map MCP tool calls to serial/TCP protocol
- Test with Claude Code / Claude Desktop
- Create helper prompts for Claude
- Integration tests: Claude Code reads C source, modifies, writes back, compiles, reads output
- Verify VC++ 6.0 compiled binary runs on Windows 3.1 + Win32s 1.25a
- Verify MinGW compiled binary runs on Windows 11
- Test across Win9x, NT, XP, modern Windows
- Verify serial transport on retro hardware
- Verify TCP transport on modern Windows
- Verify binary file integrity (compile .c files, transfer .obj)
- Error handling for serial disconnects
- Win32 shell auto-restart on crash
- Path translation helpers (Unix → Windows)
- Logging and debugging tools
- Performance optimization (command batching?)
- Complete user guide and code cleanup
Currently implemented:
- JSON parser tests (31 tests in
tests/test_json.c): command parsing, escape handling, edge cases, response building
Planned — Win32 Shell:
- Test serial port open/close
- Test command parsing (valid/invalid JSON)
- Test process execution and output capture
- Test file operations (read/write/list/delete)
Planned — MCP Bridge:
- Test serial communication with mock Win32 shell
- Test MCP tool registration
- Test command translation
- Test error handling
- Echo Test: Send string, verify echo
- Command Test: Execute
dir, verify output - Compile Test: Transfer
.cfile, compile withcl.exe, retrieve.obj - Full Workflow: Claude Code edits source, compiles, checks errors
Scenario: "Compile a test project on Win98 via MCP"
- MCP Client calls
win98_list_dir("C:\\PROJECTS") - MCP Client calls
win98_read_file("C:\\PROJECTS\\main.c") - MCP Client modifies code based on analysis
- MCP Client calls
win98_write_file("C:\\PROJECTS\\main.c", new_content) - MCP Client calls
win98_exec("cl /c main.c") - MCP Client reads compilation output
- MCP Client reports success/errors to user
Once the MCP server is running, any MCP-compatible client (Claude Code, Claude Desktop, etc.) can interact with the Win32 machine:
Example 1: List projects
User: "List the contents of C:\PROJECTS on the Win32s machine"
MCP Client calls: win32_list_dir("C:\\PROJECTS")
Example 2: Compile a file
User: "Compile main.c on the legacy Windows machine with optimization"
MCP Client calls: win32_exec("cl /O2 /c C:\\PROJECTS\\main.c")
Example 3: Debug build errors
User: "Fix the compilation errors in the Win32 project"
MCP Client calls: win32_exec("cl /c C:\\PROJECTS\\main.c")
MCP Client reads: error messages
MCP Client calls: win32_read_file("C:\\PROJECTS\\main.c")
MCP Client fixes: syntax errors
MCP Client calls: win32_write_file("C:\\PROJECTS\\main.c", fixed_content)
MCP Client calls: win32_exec("cl /c C:\\PROJECTS\\main.c")
Since MCP clients may run on Unix and Win32 uses DOS paths:
Helper Function (in MCP bridge):
def unix_to_win32(unix_path):
"""Convert /mnt/legacy/projects/main.c -> C:\\PROJECTS\\main.c"""
# Define mapping
mappings = {
"/mnt/legacy": "C:",
"/projects": "\\PROJECTS"
}
# Apply transformations
win_path = unix_path
for unix_prefix, win_prefix in mappings.items():
if win_path.startswith(unix_prefix):
win_path = win_path.replace(unix_prefix, win_prefix)
return win_path.replace("/", "\\")- No Authentication: Serial protocol has no auth - physical security only
- Command Injection: Win32 shell must sanitize commands (though limited risk on isolated retro machine)
- File Access: No sandboxing - Win32 shell has full filesystem access
- Network Exposure: If using ser2net over TCP, ensure firewall rules
Mitigation: Treat Win32s/Win9x machine as isolated retro dev environment, not production.
- Baud Rate: 115200 bps ≈ 11.5 KB/s theoretical
- Actual Throughput: ~8-10 KB/s with protocol overhead
- Latency: ~50-100ms per command round-trip
- File Transfer: 100 KB source file ≈ 10-15 seconds
Optimization Ideas:
- Compress large files before transfer (zlib in VC++ 6.0, zlib in Python)
- Batch multiple commands in single message
- Use binary protocol instead of JSON for large transfers
- Check COM port settings in Device Manager (Win95/98/ME) or Control Panel (Win3.1)
- Verify no other app (HyperTerminal, mouse driver) is using the port
- Test with simple loopback (jumper TX/RX pins)
- Check BIOS serial port settings (enabled, correct IRQ)
- For Win3.1+Win32s: Ensure Win32s subsystem is properly installed
- Verify baud rate matches on both sides
- Check cable wiring (null-modem vs straight-through)
- Test with HyperTerminal echo test
- Try lower baud rate (9600) for initial testing
- Disable hardware flow control if not wired
- Increase timeout in Python serial config
- Check for buffer overflow (add delays between commands)
- Verify Win32 shell is still running (no crash)
- Check for serial driver issues on modern OS
- Win3.1+Win32s may be slower - increase timeouts for these systems
- Verify base64 encoding/decoding
- Check for line ending issues (CRLF vs LF)
- Test with small text files first
- Add checksums to protocol
- Compression: Implement gzip compression for large file transfers
- Multiplexing: Allow multiple concurrent commands (if Win98 can handle)
- Auto-Reconnect: Detect and recover from serial disconnects
- Build Automation: Higher-level "build project" command that runs nmake
- Debugging Support: Integrate with VC++ 6.0 debugger over serial
- Telnet: Replace serial with lightweight telnet server on Win98
- Named Pipes: If running Win98 in VM, use virtual serial/pipes
- Network Bridge: Use TCP sockets over parallel port Ethernet adapter
- Project Templates: Pre-configured Win98 project structures
- Custom System Prompts: Documentation for AI assistants about Win98 constraints
- Build Scripts: Automation wrapper scripts for common workflows
- Error Pattern Matching: Knowledge bases for VC++ 6.0 error messages
- MSDN: CreateFile for COM ports
- MSDN: DCB structure (baud rate, parity configuration)
- MSDN: CreateProcess with redirected I/O
- Anthropic MCP Documentation: https://docs.anthropic.com/mcp
- MCP Python SDK: https://github.com/modelcontextprotocol/python-sdk
- pySerial documentation: https://pyserial.readthedocs.io/
- Null-modem wiring diagrams
- RS-232 specification
- Compiler switches reference
- NMAKE documentation
- Debugging techniques
Total Estimated Time: 5-6 weeks
| Week | Phase | Focus | Status |
|---|---|---|---|
| 1 | Phase 1 | Test framework + JSON parser + CI | Complete |
| 2 | Phase 2 | File operations + base64 + echo + 87 tests incl. PBT | Complete |
| 3 | Phase 3 | Network & transport: vtable backends, serial refactor + TCP/Winsock peer, /AUTO + /BIND, SO_KEEPALIVE | Complete |
| 4 | Phase 4 | Command execution + protocol | Spec'd |
| 5 | Phase 5 | MCP integration | Not started |
| 6 | Phase 6 | Cross-platform testing | Not started |
| 7 | Phase 7 | Documentation & polish | Not started |
Note: GitHub Actions CI was integrated into Phase 1. All subsequent phases inherit CI validation automatically.
Functionality:
- MCP client can list directories on any Windows version
- MCP client can read/write files across Win32s through Win11
- MCP client can compile code with any Win32 compiler (VC++ 1.x-2022, Borland, MinGW, etc.)
- MCP client can read compilation errors and suggest fixes
- System is stable over hours-long development sessions
- File transfers are reliable (no corruption)
- Response latency is acceptable (<2 seconds per command)
Compatibility:
- Single executable works across full Windows range (Win32s 1.25a → Win11)
- Binary runs on Windows 3.1 + Win32s 1.25a (primary compatibility test)
- Binary runs on Windows 95/98/ME (Win9x family)
- Binary runs on Windows NT 3.51/4.0 (NT family)
- Binary runs on Windows 2000/XP (NT5 family)
- Binary runs on Windows 7/8/10/11 (modern Windows)
- Serial transport works on retro hardware (Win3.1, Win95/98)
- TCP transport works on modern Windows (Win7 → Win11)
- Named pipe transport works for VMs and WSL scenarios
- Binary compiled with VC++ 6.0 runs on Windows 11
- Binary compiled with MinGW runs on Windows 3.1 + Win32s 1.25a
- Same binary behavior across all platforms
Testing:
- All unit tests pass on VC++ 6.0 compiled binary
- All unit tests pass on MinGW compiled binary
- Unit tests run on Windows 3.1 + Win32s 1.25a (with VC++ 6.0 binary)
- Unit tests run on Windows 95 (with same binary)
- Unit tests run on Windows 98 SE (with same binary)
- Unit tests run on Windows XP (with same binary)
- Unit tests run on Windows 11 (with same binary) - 30 year forward compat!
- GitHub Actions CI passes on every commit
- Integration tests verify end-to-end protocol
- No external test dependencies (framework is in-tree)
- Test coverage >80% for core components
Current Phase: Phase 3 complete (transport vtable, serial refactored onto it, TCP/Winsock peer runtime-loaded, /AUTO fallback + /BIND scope, SO_KEEPALIVE, 115 tests incl. PBT). Command execution follows as Phase 4.
This is a technical design specification for bridging MCP clients with Win32 systems across the full Windows family (Win32s 1.25a through Windows 11). The project emphasizes maximum compatibility through strict adherence to the Win32s 1.25a API subset and i386 instruction set.
Key Design Constraints:
- Win32s 1.25a API compatibility (1995 baseline)
- i386 CPU target (no 486+ instructions)
- C89 language subset
- No floating point operations
- ANSI-only character handling with DBCS support
- Base address 0x10000 for Win32s compatibility
Development Environment Requirements:
- Windows machine (any Win32 version) with development tools
- Modern development machine (Linux/Windows/macOS) with Python 3.9+
- Connection method matching your scenario (serial/network/pipe)
- Basic knowledge of Win32 API and your chosen transport
Testing Matrix: We're particularly interested in testing across Windows versions:
- Win32s era: Windows 3.1 + Win32s 1.25a (PRIMARY TARGET)
- Win9x era: 95, 98, ME
- NT era: NT 3.51, NT 4.0, 2000, XP
- Modern era: Vista, 7, 8, 10, 11
- Server editions: 2003, 2008, 2012, 2016, 2019, 2022
Note: Win32s 1.30/1.30a/1.30c have known issues with QEMU/VMware. For virtualization, use Win32s 1.25a.
This project is released into the public domain. You are free to use, modify, and distribute this code for any purpose, commercial or non-commercial, without any restrictions.
For jurisdictions that do not recognize public domain dedications, this code is available under the Unlicense or CC0 1.0 Universal.
Rationale: Given the AI-assisted nature of this project's development and evolving copyright questions around AI-generated code, we've chosen the most permissive option to ensure maximum accessibility and legal clarity.
- Anthropic for the Model Context Protocol specification
- The retro computing community for preserving and documenting legacy systems
- Open source serial communication libraries and tools
- Model Context Protocol (MCP) - The protocol specification
- Claude Code - AI-assisted command-line development tool
- pySerial - Python serial port access library