Skip to content

Repository files navigation

MCP-Win32s

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.

Why This Exists

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.

Features

  • ✅ 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

Quick Start

Prerequisites

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.)

Installation

  1. On Windows machine: Compile the Win32 shell

    The build is defined by CMakeLists.txt + CMakePresets.json + the toolchain files in toolchains/. The Win32s-critical flags (/BASE:0x10000, /FIXED:NO, /G3 or -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.sh and build.bat still exist but are now thin wrappers around the above (build.sh → mingw preset, build.bat → vc6 preset). Out-of-source build output lands in build/ (git-ignored).

  2. On modern machine: Install MCP bridge

    pip install pyserial mcp
    git clone https://github.com/yourname/mcp-win32s
    cd mcp-win32s
  3. Configure MCP client: Add to your MCP configuration

    {
      "mcpServers": {
        "win32s": {
          "command": "python3",
          "args": ["/path/to/mcp_serial_bridge.py"]
        }
      }
    }
  4. Connect:

    • Serial (retro): Null-modem cable between machines
    • Network (modern): TCP socket (Winsock 1.1 compatible)
      • Note: Win32s has wsock32.dll (Winsock 1.1), not ws2_32.dll
  5. 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.


Technical Constraints & Compatibility

Design Philosophy: Maximum compatibility through minimum dependencies.

Compilation Targets

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.

API Restrictions

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

Language Subset

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 incompatible

Standard Library

Safe 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)

Build Matrix

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

Non-Goals

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

Why This Works

By targeting the common subset of:

  1. Win32s 1.25a ANSI APIs (1995)
  2. ANSI C standard library
  3. VC++ 6.0 C++ subset
  4. MinGW compatibility

We get a single codebase that compiles everywhere and runs everywhere (Win95 → Win11).


Technical Constraints & Compatibility

Win32s Version Targeting

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, not ws2_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)

Compiler Settings for Win32s 1.25a Compatibility

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?

  1. Win32s requirement: Win32s 1.25a requires 386 or higher
  2. Maximum compatibility: Code runs on 386SX (1988) through modern CPUs
  3. No 486+ dependencies: Avoid CPUID, CMPXCHG, BSWAP (486+)
  4. No Pentium features: No RDTSC, CPUID, or Pentium-specific instructions
  5. 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/STOSB

i386 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 some

Why /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 ⚠️ Known crashes, use 1.25a

API Compatibility Strategy

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 - use winsock.dll for 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 original winsock.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)

Compiler Compatibility Matrix

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.

Language Subset

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:

  1. FPU Compatibility Issues: Modern CPUs (Intel Ivy Bridge 2012+, AMD Zen) deprecate FCS/FDS segment registers, breaking old FPU exception handling code
  2. Unnecessary Complexity: File operations, command execution, JSON parsing require only integer arithmetic
  3. Deterministic Behavior: Integer-only code has predictable behavior across all CPU generations
  4. Win32s Safety: Avoid any interaction with WIN87EM.DLL (Windows 3.x FPU emulator)
  5. 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 initializers

Character Encoding Strategy

Design 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.

UTF-8 Through ANSI APIs (The Strategy)

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

File Content: Base64 Everything (SAFE)

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!

Filenames: Code Page Aware Handling

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_cmd

On 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... */
}

Practical Filename Guidelines

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:

  1. Detect code page on Win32s startup:

    UINT codePage = GetACP();  /* Get ANSI code page */
    /* Send to MCP bridge: {"shell_ready":true,"codepage":1252} */
  2. 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
  3. 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

Command Output: UTF-8 on Modern Windows, ANSI on Win32s

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 chars

JSON Protocol Encoding

Wire 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);
}

Recommendations

For Maximum Compatibility:

  1. File Contents: ALWAYS use base64

    • ✅ Preserves exact bytes
    • ✅ Works with any encoding (UTF-8, Shift-JIS, binary files)
    • ✅ No ambiguity
  2. 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
  3. 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
  4. JSON Keys/Commands: ASCII only

    • Commands: exec, read, write (all ASCII)
    • Status: ok, error (all ASCII)

Example: Full Round-Trip

# 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" ✅

Summary Table

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 ⚠️ Fragile**
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

Code Page Detection (Win32s Side)

/* 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}")

Non-Goal: Full Unicode Support

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.


DBCS (Double-Byte Character Set) Support

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 byte
  • CharNext() / CharPrev() - DBCS-aware string navigation
  • lstrlen() - DBCS-safe (counts bytes, not characters)

Strategy for DBCS Support:

  1. Detection: Win32s shell reports code page on startup

    UINT codePage = GetACP();  /* e.g., 932 for Japanese */
    /* Send: {"codepage":932,"dbcs":true} */
  2. 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
  3. 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

Modern CPU Compatibility Note: FPU Segment Registers

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:

  1. 486+: FCS/FDS became unnecessary but kept for compatibility
  2. AMD64 (2003): 64-bit FPU instructions couldn't save/restore FCS/FDS properly
  3. Modern CPUs (Intel Ivy Bridge+, 2012): Reading FCS/FDS returns zero
  4. 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:

  1. No FPU instructions = No FCS/FDS registers touched
  2. Integer-only code = Works identically on all CPUs (8087 through modern)
  3. No FPU state = No FSAVE/FRSTOR, no exception handling
  4. Deterministic = Same behavior on Win3.1+Win32s and Windows 11
  5. 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.

Build System

Requirements:

  • Must compile with Visual C++ 6.0 (via the vc6 CMake preset)
  • Must compile with MinGW on GitHub Actions (via the mingw CMake 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 vc6

build.sh and build.bat remain as thin wrappers (build.sh → mingw preset, build.bat → vc6 preset). Build output is out-of-source in build/.

Testing Requirements

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:

  1. MCP client makes requests (file operations, command execution)
  2. MCP bridge translates to protocol (JSON over chosen transport)
  3. Win32 shell receives commands, executes in native Windows context
  4. Results returned via same transport
  5. 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 Layer Options

Transport Support Matrix

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.


Option 1: Serial (Universal Baseline)

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

Option 2: TCP/IP via Winsock 1.1 (WfW 3.11+ Recommended)

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\\mcp

Winsock 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 ⚠️ Possible but not recommended 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:

  1. Download tcp32b.exe (Microsoft TCP/IP-32 3.11b)
  2. Run installer on WfW 3.11
  3. Configure network adapter (NE2000, 3C509, etc.)
  4. Set IP address, subnet mask, gateway, DNS
  5. Reboot Windows
  6. Verify: Check for C:\WINDOWS\SYSTEM\WSOCK32.DLL
  7. Our shell will auto-detect and use TCP!

Option 3: Named Pipes (Windows 95+)

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.


Unified Transport Implementation (Implemented - Phase 3)

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;
}

Practical Deployment Matrix

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 ⚠️ Install TCP/IP-32
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 ⚠️ Serial needs USB adapter
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

Forward Compatibility Design

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 versions
  • ReadFile/WriteFile - Universal I/O across all handle types
  • WaitForSingleObject - 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.dll basics:

    • 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.dll basics:

    • MessageBoxA (for error reporting)
  • wsock32.dll (Winsock 1.1):

    • socket, bind, listen, accept, connect, send, recv, closesocket
    • WSAStartup, WSACleanup, WSAGetLastError
    • Note: Use wsock32.dll NOT ws2_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

Core Features

  • Transport Abstraction: Single codebase supports serial, TCP, and named pipes
  • Command Parser: Parse incoming protocol messages
  • Command Executor: Execute commands via CreateProcess() or system()
  • Output Capture: Capture stdout/stderr from child processes
  • File Operations: Read/write/list files on Windows filesystem
  • Error Handling: Return error codes and messages

Transport Selection

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.exe

Implementation Note: CreateFile works for ALL these transports on Windows:

  • CreateFile("COM1:", ...) - Serial port
  • CreateFile("\\\\.\\pipe\\name", ...) - Named pipe
  • For TCP: Use Winsock (WSASocket, bind, listen, accept)
    • ws2_32.lib available since Windows 95

Protocol Design

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).

Command Catalog (whitelist)

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.

Feature Detection & Graceful Uplift

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}}

Command Execution: Win32s caveats

  • WaitForSingleObject(hProcess) returns immediately on Win32s (KB Q125213): the capture loop polls GetExitCodeProcess until it stops returning STILL_ACTIVE.
  • The era shell is COMMAND.COM on Win 3.x, cmd.exe on 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-band timed out error, and subsequent exec/ptyExec requests get busy - naming the blocking command line and its elapsed ms - until a later request's re-poll observes it exited (implicit reap).

Implementation (Win32s-Compatible C)

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

Build Instructions

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 vc6

The 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):

  • /TC or -std=c89: Force C89 compilation
  • /G3 or -march=i386: Target 386 CPU (no 486+)
  • No /D_UNICODE: ANSI only for Win32s
  • wsock32.lib not ws2_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:COM1

Verification Checklist:

  • Compiles with VC++ 6.0 (or earlier)
  • Compiles with MinGW-w64 (modern CI)
  • Uses /BASE:0x10000 and /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!)

JSON Parsing Strategy (No External Libraries)

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

Unit Testing Strategy

Minimal Test Framework (No Dependencies)

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 */

Example Test Suite

/* 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();
}

Example Test Suite for File Operations (Planned - Phase 2)

/* 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 Tests

# Build and run the test binaries via the mingw preset
cmake --preset mingw
cmake --build --preset mingw
ctest --preset mingw          # runs the 7 test binaries

ctest --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 Actions CI with Tests

# .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.txt

Test Organization

MCP-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

Test Coverage Goals

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

Running Tests on Real Windows

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.exe

Benefits:

  • ✅ 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 Actions CI Example

# .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.exe

Testing 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 --version

This 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

Core Features

  • 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

Dependencies

pip install pyserial mcp

Implementation Sketch

# 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 Server Configuration

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": {}
    }
  }
}

Component 3: Serial Cable / Connection

Physical Serial Connection

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 ser2net on 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]

Baud Rate Configuration

Recommended: 115200 baud (reliable on short cables <6ft)
Alternative: 57600 baud (more stable on longer cables)
Flow Control: Software (XON/XOFF) or None


Implementation Phases

Phase 1: Test Framework + JSON Parser + CI + Serial Init + Main Exe (Complete)

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 the vc6 preset (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

Phase 2: File Operations + Serial Echo Test

  • 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

Phase 3: Network & Transport (serial + TCP/Winsock)

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/optional accept) + runtime backend registry (src/transport.c/.h)
  • Refactor serial onto the abstraction (SerialBackendOpen); move TransportConfig/ParseCommandLine to transport.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 raw HANDLE onto Transport *
  • Tests: test_transport (≥10), test_tcp (≥6, loopback), mock-backed test_serial
  • CI assertion: binary does not statically import wsock32/ws2_32 (still loads on bare Win32s)

Phase 4: Command Execution + Protocol

  • Add exec command 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 /?)

Phase 5: MCP Integration

  • 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

Phase 6: Cross-Platform Testing

  • 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)

Phase 7: Documentation & Polish

  • 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

Testing Strategy

Unit Tests

Currently implemented:

  • JSON parser tests (31 tests in tests/test_json.c): command parsing, escape handling, edge cases, response building

Planned — Win32 Shell:

  1. Test serial port open/close
  2. Test command parsing (valid/invalid JSON)
  3. Test process execution and output capture
  4. Test file operations (read/write/list/delete)

Planned — MCP Bridge:

  1. Test serial communication with mock Win32 shell
  2. Test MCP tool registration
  3. Test command translation
  4. Test error handling

Integration Tests

  1. Echo Test: Send string, verify echo
  2. Command Test: Execute dir, verify output
  3. Compile Test: Transfer .c file, compile with cl.exe, retrieve .obj
  4. Full Workflow: Claude Code edits source, compiles, checks errors

End-to-End Test

Scenario: "Compile a test project on Win98 via MCP"

  1. MCP Client calls win98_list_dir("C:\\PROJECTS")
  2. MCP Client calls win98_read_file("C:\\PROJECTS\\main.c")
  3. MCP Client modifies code based on analysis
  4. MCP Client calls win98_write_file("C:\\PROJECTS\\main.c", new_content)
  5. MCP Client calls win98_exec("cl /c main.c")
  6. MCP Client reads compilation output
  7. MCP Client reports success/errors to user

Usage with MCP Clients

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")

Path Translation Helpers

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("/", "\\")

Security Considerations

  1. No Authentication: Serial protocol has no auth - physical security only
  2. Command Injection: Win32 shell must sanitize commands (though limited risk on isolated retro machine)
  3. File Access: No sandboxing - Win32 shell has full filesystem access
  4. Network Exposure: If using ser2net over TCP, ensure firewall rules

Mitigation: Treat Win32s/Win9x machine as isolated retro dev environment, not production.


Performance Expectations

  • 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

Troubleshooting Guide

Win32 Shell Won't Start

  1. Check COM port settings in Device Manager (Win95/98/ME) or Control Panel (Win3.1)
  2. Verify no other app (HyperTerminal, mouse driver) is using the port
  3. Test with simple loopback (jumper TX/RX pins)
  4. Check BIOS serial port settings (enabled, correct IRQ)
  5. For Win3.1+Win32s: Ensure Win32s subsystem is properly installed

Serial Communication Fails

  1. Verify baud rate matches on both sides
  2. Check cable wiring (null-modem vs straight-through)
  3. Test with HyperTerminal echo test
  4. Try lower baud rate (9600) for initial testing
  5. Disable hardware flow control if not wired

Commands Time Out

  1. Increase timeout in Python serial config
  2. Check for buffer overflow (add delays between commands)
  3. Verify Win32 shell is still running (no crash)
  4. Check for serial driver issues on modern OS
  5. Win3.1+Win32s may be slower - increase timeouts for these systems

File Transfers Corrupted

  1. Verify base64 encoding/decoding
  2. Check for line ending issues (CRLF vs LF)
  3. Test with small text files first
  4. Add checksums to protocol

Future Enhancements

Advanced Features

  1. Compression: Implement gzip compression for large file transfers
  2. Multiplexing: Allow multiple concurrent commands (if Win98 can handle)
  3. Auto-Reconnect: Detect and recover from serial disconnects
  4. Build Automation: Higher-level "build project" command that runs nmake
  5. Debugging Support: Integrate with VC++ 6.0 debugger over serial

Alternative Transports

  1. Telnet: Replace serial with lightweight telnet server on Win98
  2. Named Pipes: If running Win98 in VM, use virtual serial/pipes
  3. Network Bridge: Use TCP sockets over parallel port Ethernet adapter

MCP Client Integration

  1. Project Templates: Pre-configured Win98 project structures
  2. Custom System Prompts: Documentation for AI assistants about Win98 constraints
  3. Build Scripts: Automation wrapper scripts for common workflows
  4. Error Pattern Matching: Knowledge bases for VC++ 6.0 error messages

Resources

Win32 Serial Programming

  • MSDN: CreateFile for COM ports
  • MSDN: DCB structure (baud rate, parity configuration)
  • MSDN: CreateProcess with redirected I/O

MCP Protocol

Serial Communication

Visual C++ 6.0

  • Compiler switches reference
  • NMAKE documentation
  • Debugging techniques

Project Timeline

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.


Success Criteria

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

Project Status

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.

License

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.

Acknowledgments

  • Anthropic for the Model Context Protocol specification
  • The retro computing community for preserving and documenting legacy systems
  • Open source serial communication libraries and tools

Related Projects

About

Model Context Protocol server for Win32 systems via serial/network console (Windows 3.1 + Win32s 1.25a through Windows 11). Agentic and Specification Driven Software Development example.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages