MCP-Relational-Data/blog-entry-MCP-Relational-D...

343 lines
17 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>From VB6 Floppy Disk to Rust Daemon: A 25-Year Port Story — Jeremy Anderson</title>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--text: #e6edf3;
--muted: #8b949e;
--accent: #f0883e;
--accent-dim: rgba(240,136,62,0.12);
--code-bg: #1c2128;
--green: #3fb950;
--red: #f85149;
--blue: #58a6ff;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
--font-mono: "SF Mono", "Cascadia Code", "Fira Code", Menlo, Consolas, monospace;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-sans);
background: var(--bg);
color: var(--text);
line-height: 1.7;
font-size: 16px;
}
header {
max-width: 780px;
margin: 0 auto;
padding: 60px 24px 0;
}
header h1 {
font-size: 2rem;
font-weight: 700;
line-height: 1.25;
letter-spacing: -0.02em;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 16px;
font-size: 0.875rem;
color: var(--muted);
}
.meta span { display: flex; align-items: center; gap: 4px; }
.tag {
display: inline-block;
background: var(--accent-dim);
color: var(--accent);
padding: 2px 10px;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
}
article {
max-width: 780px;
margin: 0 auto;
padding: 40px 24px 80px;
}
article h2 {
font-size: 1.4rem;
font-weight: 600;
margin: 48px 0 16px;
letter-spacing: -0.01em;
color: #fff;
}
article h2:first-child { margin-top: 0; }
article p { margin: 0 0 18px; }
article ul, article ol { margin: 0 0 18px; padding-left: 24px; }
article li { margin-bottom: 6px; }
strong { color: #fff; }
a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }
blockquote {
border-left: 3px solid var(--accent);
background: var(--accent-dim);
padding: 14px 20px;
margin: 0 0 18px;
border-radius: 0 6px 6px 0;
font-style: italic;
color: var(--muted);
}
blockquote p { margin-bottom: 0; }
pre {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px 20px;
overflow-x: auto;
margin: 0 0 18px;
font-family: var(--font-mono);
font-size: 0.875rem;
line-height: 1.55;
color: var(--muted);
}
code {
font-family: var(--font-mono);
font-size: 0.88em;
background: var(--code-bg);
padding: 2px 6px;
border-radius: 4px;
color: var(--green);
}
pre code {
background: none;
padding: 0;
color: inherit;
}
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: var(--border);
border-radius: 8px;
overflow: hidden;
margin: 0 0 18px;
font-size: 0.9rem;
}
.comparison > div { padding: 14px 18px; background: var(--surface); }
.comparison .head {
font-weight: 700;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.comparison .old .head { color: var(--red); }
.comparison .new .head { color: var(--green); }
table {
width: 100%;
border-collapse: collapse;
margin: 0 0 18px;
font-size: 0.9rem;
}
th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--border); }
th { color: var(--muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }
hr { border: none; border-top: 1px solid var(--border); margin: 36px 0; }
.cta {
display: inline-block;
background: var(--accent);
color: #000;
font-weight: 700;
padding: 10px 22px;
border-radius: 6px;
text-decoration: none;
font-size: 0.9rem;
margin-top: 8px;
}
.cta:hover { background: #f5a060; text-decoration: none; }
footer {
max-width: 780px;
margin: 0 auto;
padding: 0 24px 60px;
text-align: center;
color: var(--muted);
font-size: 0.85rem;
}
footer a { color: var(--accent); }
@media (max-width: 600px) {
header h1 { font-size: 1.5rem; }
.comparison { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header>
<h1>From VB6 Floppy Disk to Rust Daemon: A 25-Year Port Story</h1>
<div class="meta">
<span>Jeremy Anderson</span>
<span>July 18, 2026</span>
<span>12 min read</span>
<span class="tag">Rust</span>
<span class="tag">iced</span>
<span class="tag">retrocomputing</span>
</div>
</header>
<article>
<h2>The .frm File on a Shelf</h2>
<p>In 1999, I opened Visual Basic 6 and built a desktop calculator called <strong>Project for Electronics</strong>. It solved Ohm's Law and Watts Law &mdash; enter any two of voltage, current, resistance, or power, and the program computed the other two. It worked. It was useful. Then it sat on a floppy disk for a quarter century.</p>
<p>I found the source &mdash; a single <code>Main.frm</code> file, 13 KB, packed with every classic VB6 antipattern you'd expect from that era. <code>Long</code> integers for electrical values. A <code>For</code> loop brute-forcing square roots. A 1 ms <code>Timer</code> control polling text boxes because VB6 had no event-driven input binding. <code>GoTo</code> labels scattered like confetti. Wrong in every measurable sense, but the intent was solid.</p>
<p>This is the story of porting that program to Rust, fixing every bug, expanding it from one calculator to eight, adding a headless daemon mode, and putting the whole thing under AGPL-3.0 &mdash; partly for principle, mostly for the joke.</p>
<h2>What Was Wrong (Everything)</h2>
<p>Before writing a single line of Rust, I catalogued every defect in the VB6 source. Some were bugs; others were architectural decisions that made sense in 1999 but are radioactive today.</p>
<div class="comparison">
<div class="old">
<div class="head">VB6 (1999)</div>
<p><code>Long</code> (16-bit int) for watts, volts, amps, ohms &mdash; overflows above 32,767</p>
<p>Brute-force <code>For</code> loop to approximate square roots</p>
<p>1 ms <code>Timer</code> control polling all text boxes</p>
<p>No input validation (negative resistance accepted silently)</p>
<p><code>GoTo Calculate</code>, <code>GoTo ClearFields</code> for flow control</p>
<p><code>Format(Value, "0.00")</code> &mdash; fixed 2 decimal places always</p>
<p>Ohm symbol displayed as "Ohms" (no Unicode)</p>
</div>
<div class="new">
<div class="head">Rust (2026)</div>
<p><code>f64</code> throughout &mdash; 64-bit double precision, no overflow</p>
<p><code>(x).max(0.0).sqrt()</code> &mdash; one call, hardware-accurate</p>
<p>iced's reactive <code>update()</code> &mdash; recalculate only on change</p>
<p>Negative power/resistance rejected with error output</p>
<p>Data-driven dispatch tables replace all branching</p>
<p>5-decimal precision with trailing-zero trimming via <code>trim_f()</code></p>
<p>Proper Unicode &Omega; everywhere</p>
</div>
</div>
<h2>The Eight Calculators</h2>
<p>What started as a single Ohm's Law tool grew into a suite I'm calling <strong>MCP-Relational-Data</strong> that bridges two worlds: electronics engineering and modern business/AI economics. The GUI presents all eight calculators in a two-column layout.</p>
<table>
<tr><th>Calculator</th><th>Domain</th><th>Inputs</th></tr>
<tr><td><strong>Ohm's Law &amp; Watts Law</strong></td><td>Electronics</td><td>Any 2 of P, I, R, V</td></tr>
<tr><td><strong>Margin &amp; Markup</strong></td><td>Ecommerce</td><td>Cost, Selling Price</td></tr>
<tr><td><strong>ROI</strong></td><td>Business</td><td>Investment, Revenue</td></tr>
<tr><td><strong>AI Token Cost</strong></td><td>LLM Economics</td><td>Model, Input/Output Tokens</td></tr>
<tr><td><strong>Electricity Cost</strong></td><td>Infrastructure</td><td>Watts, Hours, Days, Rate</td></tr>
<tr><td><strong>Break-Even</strong></td><td>Business</td><td>Fixed Costs, Price, Variable Cost</td></tr>
<tr><td><strong>Solar Panel Revenue</strong></td><td>Energy</td><td>Panels, Watts, Sun Hours, Rate, Cost</td></tr>
<tr><td><strong>3D Print Cost</strong></td><td>Manufacturing</td><td>Filament, Weight, Time, Power, Fail Rate</td></tr>
</table>
<p>The Ohm's Law solver uses a data-driven dispatch table &mdash; six entries, each containing a pair of non-zero checks and a solve function. The first matching entry wins. The AI Token Cost estimator cycles through six models (GPT-4o, GPT-4, GPT-3.5 Turbo, Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku) with per-million-token pricing compiled into the binary.</p>
<h2>Architecture: calc.rs Is the Soul</h2>
<p>The most important design decision was separating pure calculation logic from every presentation layer. <code>calc.rs</code> contains zero I/O, zero GUI imports, zero serde. It's just functions: numbers in, numbers out. Both the iced GUI and the JSON daemon call these same functions.</p>
<pre><code>// calc.rs — the Ohm's Law solver uses a dispatch table
pub fn ohm_calculate(p: f64, i: f64, r: f64, v: f64) -&gt; (f64, f64, f64, f64) {
let pairs: [((bool, bool), fn(...) -&gt; (...) ); 6] = [
((i != 0.0, r != 0.0), |_, i, r, _| (i * i * r, i, r, i * r)),
((i != 0.0, v != 0.0), |_, i, _, v| (i * v, i, v / i, v)),
// ... 4 more entries
];
for (check, solve) in pairs {
if check.0 &amp;&amp; check.1 { return solve(p, i, r, v); }
}
(p, i, r, v)
}</code></pre>
<p>The file structure mirrors this separation:</p>
<pre><code>src/
main.rs — CLI flag parsing, data-driven flag table
calc.rs — Pure functions (no dependencies)
gui.rs — iced 0.12 desktop app (behind #[cfg(feature = "gui")])
daemon.rs — stdin/stdout JSON API (behind #[cfg(feature = "daemon")])
— tool registry array + macro-based response builder</code></pre>
<h2>The Daemon: JSON Over Stdout</h2>
<p>The original VB6 program required a human sitting at a Windows PC. The daemon mode changes that. It reads one JSON object per line from stdin and writes one JSON object per line to stdout. Status messages go to stderr so they never pollute the pipe.</p>
<pre><code>$ echo '{"tool":"ohm","current":2,"resistance":4}' | mcp-relational-data --daemon
{"tool":"ohm","power":"16 Watts(W)","current":"2 Amps(A)",
"resistance":"4 Ohms(\u03A9)","voltage":"8 Volts(V)"}</code></pre>
<p>This makes it trivial to call from Python, Node.js, shell scripts, or any MCP-compatible server wrapper. Build a slim daemon-only binary that skips the iced GUI dependency entirely:</p>
<pre><code>cargo build --release --no-default-features --features daemon</code></pre>
<p>The daemon uses a <code>TOOLS</code> constant array as its dispatch registry. Each entry holds the tool name, description, parameter list, and a handler function pointer. Adding a new calculator means appending one entry &mdash; no match arms, no dispatch logic to touch. A <code>tool_response!</code> macro handles the repetitive BTreeMap boilerplate.</p>
<h2>The iced 0.12 Adventure</h2>
<p>Using iced 0.12 was its own challenge. The version available had API gaps compared to the documentation &mdash; no <code>Container::border()</code>, no <code>container::Style</code> or <code>container::Status</code> types, no <code>rule::horizontal()</code>. Every border, separator, and styled container I tried to add failed to compile.</p>
<p>The solution was brutal minimalism: stick to the APIs confirmed to work (<code>container</code>, <code>column</code>, <code>row</code>, <code>text</code>, <code>text_input</code>, <code>button</code>, <code>scrollable</code>) and use spacing and padding to create visual separation. It ended up looking cleaner than bordered panels would have.</p>
<p>There was also a subtle lifetime error: using <code>&amp;String::new()</code> as a static reference for the break-even calculator's empty output field caused a dangling reference at compile time. The fix was trivial &mdash; use <code>""</code> (a <code>&amp;'static str</code>) instead &mdash; but tracking it down took a few rounds of compiler error archaeology.</p>
<h2>Quality Assurance Pass</h2>
<p>After the initial port, I ran a full QA pass with production-readiness standards in mind. The goal: eliminate deep nesting, remove dead code, replace branching with data where appropriate, and make every comment and doc string sound like a deliberate decision rather than an afterthought.</p>
<p>Key changes from that pass:</p>
<ul>
<li><strong>Ohm's Law solver</strong> &mdash; replaced six nested <code>if/return</code> guards with a dispatch table array. Each entry pairs a boolean check tuple with a solve function. First match wins. Flat, data-driven, no nesting.</li>
<li><strong>GUI TokenPrev bug</strong> &mdash; both the prev and next buttons called <code>next_model()</code>. The prev button now correctly calls <code>prev_model()</code>.</li>
<li><strong>Dead code removal</strong> &mdash; <code>print3d_calculate</code> returned a <code>prints_to_payoff</code> field that was always <code>0.0</code> (printer capital cost was out of scope). The field is gone from both the function signature and the GUI.</li>
<li><strong>Parse helper extraction</strong> &mdash; eight <code>recalculate()</code> implementations each repeated <code>self.x.trim().parse().unwrap_or(default)</code>. A single <code>parse_f64()</code> helper eliminates that duplication.</li>
<li><strong>Daemon tool registry</strong> &mdash; the 8-arm <code>match</code> dispatch is now a <code>TOOLS</code> constant array. A <code>tool_response!</code> macro handles the BTreeMap construction boilerplate. Adding a calculator = one array entry + one handler function.</li>
<li><strong>CLI flag table</strong> &mdash; <code>main.rs</code> replaced four cascading <code>if args.iter().any(...)</code> checks with a <code>FLAGS</code> array evaluated top-to-bottom. First match wins. Step-down flow.</li>
</ul>
<h2>Why AGPL?</h2>
<blockquote><p>"If you host this as a network service and modify it, you owe the world the source. Consider it a 25-year-later joke at the VB6 original that was locked in a <code>.frm</code> file on a floppy disk."</p></blockquote>
<p>The original program was proprietary by default &mdash; not because I made a deliberate licensing choice, but because that's what VB6 projects were. One file, one IDE, one OS. You couldn't build on it, extend it, or integrate it into anything else.</p>
<p>AGPL-3.0 is the opposite extreme. Fork this calculator, wrap it in a web service, and charge people to use it? Publish your modified source. It's a deliberately ironic choice for a program whose original incarnation was the least open thing possible: a compiled binary I distributed on a physical disk in 1999.</p>
<p>Practically, it doesn't matter. This is a calculator. Nobody is going to build a SaaS around Ohm's Law. But the license file is 662 lines long and it's there, and that's the point.</p>
<h2>What's in the Box</h2>
<p>The final deliverable is a Rust project with everything needed to build, run, and extend it:</p>
<ul>
<li><code>Cargo.toml</code> &mdash; feature-gated dependencies (gui + daemon default, either can be disabled)</li>
<li><code>src/main.rs</code> &mdash; CLI entry point with data-driven flag table (<code>--daemon</code>, <code>--help</code>, <code>--version</code>)</li>
<li><code>src/calc.rs</code> &mdash; eight pure calculation engines + AI model pricing data</li>
<li><code>src/gui.rs</code> &mdash; iced 0.12 desktop app, two-column layout, 8 panels, reactive updates</li>
<li><code>src/daemon.rs</code> &mdash; stdin/stdout JSON protocol, tool registry array, <code>tool_response!</code> macro</li>
<li><code>README.md</code> &mdash; full documentation with protocol reference, pair-matching table, VB6 bug list</li>
<li><code>QUICKSTART.md</code> &mdash; zero-to-running in 7 steps with Python, shell, and Node.js examples</li>
<li><code>LICENSE</code> &mdash; the full GNU AGPL-3.0 text, all 662 lines</li>
</ul>
<h2>Try It</h2>
<pre><code>git clone https://git.dcos.net/dcosnet/MCP-Relational-Data.git
cd MCP-Relational-Data
cargo run # GUI mode
echo '{"tool":"ohm","voltage":12,"resistance":4}' | cargo run -- --daemon</code></pre>
<p>A 1999 VB6 calculator I wrote as a teenager, now a modern Rust application with a desktop GUI, a headless JSON daemon, eight calculators, and the most aggressively copyleft license I could find. The floppy disk would be proud.</p>
</article>
<footer>
<p><a href="https://dcos.net">Jeremy Anderson</a> &mdash; <a href="https://git.dcos.net/dcosnet/MCP-Relational-Data">MCP-Relational-Data v2.1.0</a> &mdash; AGPL-3.0</p>
</footer>
</body>
</html>