← All writing

Portable Is a Claim, Not a Property

Bearing, a new MCP context server, was built with a real, defensible claim: no database, no COM, no Win32 — markdown in, markdown out, so it should run anywhere. That claim had only ever been tested by building on macOS, then verified further by driving a live MCP protocol exchange against it there — correct retrieval, correct staleness flags, zero stray bytes on stdout. None of that found what a three-OS CI matrix found on its first run: 26 passed, 1 failed on Windows, because Path.GetRelativePath returns backslashes there and the tool's own docs told a model to pass forward slashes. The fix was two lines. The gap it closed was between a claim about the code and a claim about the world.

Michael Shatny··6 min read

26 Passed, 1 Failed

The push queued three jobs, one per operating system, all running the same commit against the same test suite: ubuntu-latest, macos-latest, windows-latest. Macos came back green in seventeen seconds. Ubuntu came back green in twenty-six. Windows took over a minute and came back red.

windows-latest — Test
Bearing.Core.Tests.CorpusTests.Find_matches_by_label_then_relative_path_then_contains [FAIL]
  Assert.NotNull() Failure: Value is null
  at CorpusTests.cs:line 130

Total tests: 27
     Passed: 26
     Failed: 1

Nothing about the test had changed between the two runs. Nothing about the code being tested had changed either — this was the project's first CI run, exercising the exact commit already sitting, unmodified, under a green checkmark on macOS. The only variable that moved was the operating system underneath it.

A Claim Already Defended Once

The project is Bearing, a small MCP server that answers “what does this schema look like, what does this table mean, why is it built this way” by indexing a folder of markdown with BM25 — no database connection, no COM, no HTTP client, no live integration of any kind. One input type, by design. That constraint is real, and by the time the CI matrix ran it had already caught one platform problem on its own.

Bearing.Mcp's project file had <RuntimeIdentifier>win-x64</RuntimeIdentifier> sitting in its base property group — almost certainly carried over by convention from a sibling WPF project that genuinely does need to target Windows. Nothing in Bearing.Core or Bearing.Mcp touches a Windows API. But the pin alone was enough: attempting to run the built assembly on macOS failed before a single line of application code executed —

Unhandled exception. System.IO.FileLoadException: Could not load file or assembly
'bearing-mcp.dll'. The assembly architecture is not compatible with the current
process architecture.

Two lines removed from the csproj, and the same build ran clean, portable, framework- dependent, no RID baked in. That fix felt complete. It wasn't the last platform assumption sitting unexamined in the project — it was just the one visible from a build error, which is the easy kind to find.

As Real As Verification Gets, on One Machine

What followed wasn't a hopeful dotnet build and a shrug. The server's own documentation names one rule as non-negotiable: stdout carries JSON-RPC frames, and anything else written there corrupts the protocol. So it got run for real — piped a live handshake over stdin, stdout and stderr captured to separate files — and the response came back exactly as a client would need it:

Real stdout, from a running process
{"result":{"protocolVersion":"2024-11-05","capabilities":{"logging":{},"tools":
{"listChanged":true}},"serverInfo":{"name":"bearing-mcp","version":"1.0.0.0"}},
"id":1,"jsonrpc":"2.0"}

Byte count on stdout outside those frames: zero, confirmed with wc -c, not assumed from reading Program.cs. Beyond the handshake, real tool calls against a real corpus: a search for what “availability” means came back ranked correctly, a freshness check correctly flagged one origin as stale at fifty-four days against its thirty-day threshold and left a two-day-old origin alone, a document fetch returned full text. Twenty-seven unit tests, all green. Nothing in that list was hand-waved.

!

None of that scrutiny was capable of finding what came next, because none of it involved a second operating system.

A verification that only ever runs on one machine isn't testing “this works everywhere.” It's testing “this works here” — as many times, and as rigorously, as you like.

What One Machine Can't Show You

The CI matrix existed for exactly this reason: Bearing has no OS-specific code, and the only way to keep that true is to stop asserting it and start running it somewhere else. Linux passed instantly — no surprise, it's the closest cousin to macOS in this context. Windows found something real.

Corpus.Find() looks up a document by matching a caller-supplied label against, among other things, the document's relative path. That path comes from Path.GetRelativePath, which on Windows returns backslashes. The tool's own description of the get_document parameter — the text a model reads before deciding what string to pass — says exactly this:

ContextTools.cs — the instruction a model actually follows
[Description("Document label or relative path, e.g. 'BookCopy' or 'schema/BookCopy.md'.")]

Forward slashes. Always forward slashes — it's the only form a model would ever produce, because it's the universal convention in prose and in every other label this same server returns. On Windows, comparing that string against a backslash-separated RelativePath fails silently. Not an exception. Not a warning. A null, indistinguishable from “that document doesn't exist.”

Two Lines, Normalized Once

The fix doesn't chase the mismatch at every comparison site. It removes the reason a mismatch is possible, once, at the one place the path is first read off disk:

Corpus.cs — normalised at load time, not compared at lookup time
// Normalised to forward slashes regardless of OS: this is the form shown in
// get_document's own examples, and it must round-trip through Find() the
// same way on Windows as anywhere else, not just on whichever OS indexed
// the corpus.
var relative = Path.GetRelativePath(_options.CorpusRoot, file).Replace('\\', '/');

The origin-defaulting fallback, which had been splitting that same string on Path.DirectorySeparatorChar, moved to split on the character the string is now guaranteed to contain. Pushed, watched the matrix again: ubuntu in thirty seconds, windows in a minute ten, macos in seventeen. Twenty-seven tests, three operating systems, all green, for the first time since the project existed.

The General Shape

“No platform-specific code” is a claim about what a program does. “Runs on every platform” is a claim about what happens when someone actually runs it somewhere. They sound like the same sentence. They are only the same claim once you've closed the distance between them by hand — and nothing closes it faster than confidence, or code review, or even a correct, live, end-to-end protocol exchange, if all of it happened on the one machine you were already sitting at.

The three checkmarks at the end aren't the interesting part. The interesting part is that they replaced two green ones and a red one — and the red one is the only reason the other two are worth anything now.

Related

Michael Shatny is a software developer and methodology engineer and founding contributor to .netTiers (2005–2010), one of the earliest schema-driven code generation frameworks for .NET. His work spans 28 years of the same architectural pattern: structured input, generated output, auditable artifacts. Bearing, an MCP context server built the same week as this piece, applies that instinct to a different question: what a claim about “anywhere” actually costs to make good on, one operating system at a time.

ORCID: 0009-0006-2011-3258