The architecture and the research are the tidy part. This is the part that actually took the time — deploying a live, internet-reachable MCP server off a home server, with all the ordinary friction that never makes it into a design doc.
Three seats, never the same chair
The method that made the messy parts tractable was set early and held throughout: oversight, build, and deployment are never the same conversation. Oversight rules on decisions from stated evidence and writes no code. Build (in Cowork) implements against a prompt that carries the ruling but not the authority to change it, and verifies against the actual running code rather than a summary of it. Deployment executes on the real hardware — I run every command and paste the output back; Claude has no direct box access and rules nothing on its own initiative there.
This wasn’t bureaucracy for its own sake. Across the build stages, the build or deployment seat caught and corrected the oversight seat’s own claims from source, not from argument, more than half a dozen times — a rule believed to work one way that, read from the actual code, worked differently; a control assumed to be enforced on read that had no reader actually checking it. That’s the seams doing their job. A seat reasoning only from its own prior write-up will confidently restate a wrong belief, because nothing forces it into contact with the ground truth. Only a seat that can actually see the running system catches that. The fuller account of why this shape, and not one long-running chat, is in the archived “Why We Built It This Way” piece.
More than once during the actual MCP setup below, a deployment thread that had been handed the wrong project instructions flagged itself: “I’m staying in the deployment-chat role: diagnose the connector, don’t touch the live front’s security config, escalate a design call rather than resolve it.” That’s the discipline holding under real, slightly tedious, conditions rather than in the abstract.
Getting the connector to actually attach
The MCP setup itself is worth recording honestly because none of it was clean on the first pass.
Node.js and the local machine. claude_desktop_config.json uses npx, which needs Node.js. I hadn’t installed it. The LTS .msi installer (not the .zip) handles PATH setup automatically — Claude Desktop launches with a minimal PATH, so avoiding a manual PATH edit mattered. node --version and npx --version confirmed the install, but npx then failed in PowerShell with a script-execution-policy error — normal Windows behaviour, not a real blocker, since npx.cmd works fine from plain cmd.exe and Claude Desktop spawns it as a subprocess rather than through an interactive PowerShell session anyway.
Windows Firewall. Node.js triggered a firewall prompt on first connection attempt. The right answer was Private network only — the connection Claude Desktop makes is local (npx/node spawned as a subprocess reaching out over HTTPS), and enabling Public would have meant allowing inbound traffic to node.exe from any untrusted network, which buys nothing and costs something.
The header substitution bug. mcp-remote does its own ${VAR} substitution on --header argument values, and the substitution regex specifically expects no spaces around the colon in "Authorization:${MEM_BEARER}" — the space has to live inside the env value instead ("MEM_BEARER": "Bearer <token>"). Getting the spacing backwards is an easy, silent way to have the header simply not populate.
The Origin gate, diagnosed from outside in. The connector still wouldn’t connect after the header fix. A bare curl against the endpoint came back 403 Forbidden from openresty (nginx). Per the front’s fail-closed design, that specific error class meant the token was fine and an Origin check was firing on a request that sent no Origin at all. A second curl with an explicit Origin header still 403’d — which meant either the gate wanted a different value, or the 403 wasn’t the Origin check at all. Reading the actual nginx config settled it: an outer-scope if block required Origin to equal https://<my-hostname> exactly, evaluated before the dedicated /mcp location block did its own Host/Origin rewrite. mcp-remote’s own probe requests weren’t sending that value. Adding a second --header "Origin:https://<my-hostname>" line to the config, once a missing comma in the JSON array was fixed (a classic case of an edit landing without a trailing comma, and Claude Desktop silently falling back to the last-good config rather than erroring visibly), closed it. A 406 Not Acceptable on the next curl attempt was, perversely, the good sign — it meant both gates had passed and the request had reached the real MCP server, which was only unhappy that curl hadn’t sent Accept: text/event-stream the way a real client does.
None of this was resolved by reasoning about MCP in the abstract. Every step was: read the actual error, read the actual config, read the actual log line, and only then propose the specific one-line fix. That’s the build/deployment discipline from the top of this page, operating at the scale of a single missing comma.
The filesystem server, added separately
A filesystem MCP server (@modelcontextprotocol/server-filesystem) was added weeks later, in early August, for an unrelated need — giving Claude Desktop’s local-agent mode access to a working folder — and it’s a genuinely separate server from mem-local, pointed at its own explicit folder argument rather than inheriting access from coworkUserFilesPath or the trusted-folders list, which are separate mechanisms entirely. Worth stating plainly since it’s easy to conflate “a filesystem connector isn’t working” with “the memory server isn’t working” — they share nothing but sitting in the same config file.
The bearer token, exposed and rotated
The mem-local bearer token ended up pasted into a chat transcript at least twice over the build — once during a config-sharing session, once during a deployment verification step where the current token was needed to complete a test. Each exposure was treated as a live compromise rather than argued away: rotate via openssl rand -hex 32, update the NPM proxy host’s advanced config, update the password-manager entry, re-confirm with a curl. By the second exposure, the pattern itself became the useful finding — the actual leak vector was transcripts and config-sharing, not the token’s storage at rest, which meant the sensible response was bounded blast radius and rotation discipline, not encrypting a secret against a threat that had never actually been the one that bit. A separate, unrelated set of long-lived Home Assistant bearer tokens was also spotted hardcoded in a Node-RED flow during an early mem-local session and flagged for rotation — an adjacent hygiene item the work surfaced rather than something mem-local itself was responsible for.
Sources: Setting up an MCP server in Claude, 2 Jul 2026; MCP server authorization header configuration issue, 2 Jul 2026; Filesystem MCP configuration troubleshooting, 4 Aug 2026; Claude configuration and MCP server setup, 24 Jul 2026; Nginx/NPM memory server breakout status, 23 Jun 2026; Memory system build stage 0 oversight, 20 Jun 2026; earlier, unpublished notes 03-how-we-built-it and 04-why-this-method; mem-local e4 (Decision R — per-chat separation).