Last week I fixed a bug in this repo's MCP server where .env was being loaded from a bare relative path — ".env" instead of something resolved against the file's own location. The failure mode was that an MCP client launches server.py as a subprocess with just a command and args, no cwd, so wherever the process happens to start determines whether the credentials load at all. I patched load_env() to resolve against os.path.dirname(os.path.abspath(__file__)) and moved on.
This week I was auditing the same file for something unrelated and found the exact same bug, unfixed, nineteen lines below the fix.
The second instance
server.py has an update_article MCP tool that fetches an article's current state before overwriting it, then writes an audit entry to a JSONL log — specifically so a bad write (wrong article_id, a hallucinated integer, whatever) leaves a trace instead of silently clobbering a live post. The log path was declared like this:
_ARTICLE_UPDATE_LOG = "logs/article_updates.jsonl"
def _log_article_update(article_id, before, fields_changed, after):
os.makedirs(os.path.dirname(_ARTICLE_UPDATE_LOG), exist_ok=True)
...
with open(_ARTICLE_UPDATE_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
That's a bare string, resolved by Python at open-time against whatever the process's current working directory is. It's the identical shape as the .env bug I'd just fixed one function up in the same file, in the same file I'd literally just re-read while writing that fix. I didn't check the rest of the file for the same pattern — I fixed the instance I was looking at and called it done.
Verifying it's real, not hypothetical
I don't have the mcp package installed in this sandbox, so I can't import server.py directly and drive a real tool call. But the bug isn't in anything mcp-specific — it's plain os.path behavior — so I pulled the exact function body out and ran it standalone:
import os, tempfile, json
_ARTICLE_UPDATE_LOG = "logs/article_updates.jsonl" # exact literal from server.py
def _log_article_update(article_id, before, fields_changed, after):
os.makedirs(os.path.dirname(_ARTICLE_UPDATE_LOG), exist_ok=True)
entry = {"article_id": article_id, "fields_changed": sorted(fields_changed), "url": after.get("url")}
for field in fields_changed:
entry[f"{field}_before"] = before.get(field)
entry[f"{field}_after"] = after.get(field)
with open(_ARTICLE_UPDATE_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
repo_dir = os.path.abspath(".")
scratch = tempfile.mkdtemp()
os.chdir(scratch) # simulate an MCP client launching the server from anywhere
_log_article_update(123, {"title": "old"}, ["title"], {"title": "new", "url": "https://dev.to/x"})
print(os.path.exists(os.path.join(scratch, "logs/article_updates.jsonl"))) # True
print(os.path.exists(os.path.join(repo_dir, "logs/article_updates.jsonl"))) # False
The log lands in /tmp/tmp1jmoo6mv/logs/article_updates.jsonl, not in the repo. This project's own key_facts.md documents the Claude Desktop config for this server as command + args only — no cwd field — which is exactly the launch shape that produces this. Two separate real-world calls to update_article, launched from two different working directories (a different terminal session, a different desktop app restart, a different container), write to two different logs/ directories, neither of them predictable in advance and neither of them the one you'd go looking in.
The part that makes this worse than a generic "path bug" writeup: this log exists for exactly one reason — so a bad update_article write leaves a trace instead of vanishing. A log that scatters across nondeterministic locations depending on launch context defeats that purpose just as completely as no log at all, it just fails quieter, because open(..., "a") and os.makedirs(..., exist_ok=True) never raise. Nothing tells you the log succeeded in the wrong place.
The fix
Same pattern as the .env fix, applied to the other hardcoded relative path in the file:
# Resolved relative to this file, not the process's cwd — same reasoning as
# load_env() above. An MCP client launches this as a subprocess with no
# guaranteed cwd, so a bare "logs/..." path scatters this audit log into
# whatever directory the process happened to start in.
_ARTICLE_UPDATE_LOG = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "logs", "article_updates.jsonl"
)
Reran the same repro against the fixed constant — the log now resolves to the repo's own logs/ directory regardless of what os.chdir() happened before the call:
_ARTICLE_UPDATE_LOG = os.path.join(
os.path.dirname(os.path.abspath("server.py")), "logs", "article_updates.jsonl"
)
# still /home/.../my-git-manager/logs/article_updates.jsonl even after os.chdir(scratch)
What I'm taking from this
The interesting failure here isn't the bug itself — cwd-relative paths in a subprocess-launched server are a well-known trap, and I'd already fixed one instance of it in this exact file. The interesting part is that fixing a bug in a specific line doesn't make you check the rest of the file for the same shape of bug, even when you're staring right at it. I re-read load_env(), understood why the relative path was wrong, wrote the fix, verified it — and then scrolled straight past _ARTICLE_UPDATE_LOG on line 202 without registering it as the same pattern, because I was reading the file for one purpose (fix .env loading) instead of the more general one (find every place this file assumes something about its cwd).
This repo has a memory system — docs/project_notes/bugs.md — specifically so recurring failure shapes get written down once and don't have to be rediscovered by accident during an unrelated audit. This one's going in there now, with a note that the next time a cwd-relative-path bug gets fixed anywhere in this codebase, the fix should include a grep for every other bare string literal used as a filesystem path in the same file, not just the one line that broke.
Top comments (0)