I removed an MCP server from my config. It kept coming back. Here's a debugging story about hidden config merge layers.
The Problem
Text
ENOENT: no such file or directory, posix_spawn '/opt/homebrew/bin/mcp-obsidian'
The Search
| Location | Result |
|---|---|
| ~/.config/opencode/opencode.json | No obsidian entry |
| opencode.jsonc | Doesn't exist |
| Project-local configs | Don't exist |
| ~/.opencode/opencode.json | Doesn't exist |
| SQLite database (all 11 tables) | No MCP storage |
| mcp-auth.json | Only notion |
| ~/.claude/settings.json | mcpServers: {} (empty) |
| oh-my-opencode profiles (5 files) | No MCP config |
| OpenCode binary (strings scan) | No obsidian |
| WAL journal | Only in message content |
| OPENCODE_CONFIG env var | Not set |
The Breakthrough
Bash
curl -s "http://127.0.0.1:4096/mcp?directory=$PWD" | python3 -m json.tool
Json
{
"obsidian": {
"status": "failed",
"error": "ENOENT: no such file or directory, posix_spawn '/opt/homebrew/bin/mcp-obsidian'"
}
}
The Root Cause
Javascript
const merged = {
...createBuiltinMcps(disabledMcps, pluginConfig), // websearch, grep_app
...userMcp ?? {}, // from opencode.json
...mcpResult.servers, // from Claude Code configs (!)
...pluginComponents.mcpServers // from plugin components
};
Javascript
function getMcpConfigPaths() {
return [
{ path: join(homedir(), ".claude.json"), scope: "user" },
{ path: join(claudeConfigDir, ".mcp.json"), scope: "user" },
{ path: join(cwd, ".mcp.json"), scope: "project" },
{ path: join(cwd, ".claude", ".mcp.json"), scope: "local" }
];
}
The Fix
Bash
python3 -c "
import json
with open('$HOME/.claude.json', 'r') as f:
data = json.load(f)
if 'obsidian' in data.get('mcpServers', {}):
del data['mcpServers']['obsidian']
with open('$HOME/.claude.json', 'w') as f:
json.dump(data, f, indent=2)
print('Removed obsidian')
"
Why the CLI and TUI Disagreed
- CLI (opencode mcp list) reads OpenCode's native config directly — no plugin processing
- TUI gets data from the server API (/mcp), which runs through the full plugin pipeline including oh-my-openagent's config merger
The Debugging Checklist
- ~/.config/opencode/opencode.json — OpenCode's own config
- ~/.claude.json → mcpServers — Claude Code's config (oh-my-openagent reads this)
- ~/.claude/.mcp.json — Claude Code's dedicated MCP file
- {project}/.mcp.json — Project-level Claude Code MCP config
- {project}/.claude/.mcp.json — Local Claude Code MCP config
- Query the actual merged state: curl -s http://127.0.0.1:4096/mcp?directory={dir}
Takeaway
- Native: opencode.json — what opencode mcp list reads
- Plugin-injected: ~/.claude.json + .mcp.json files — merged at runtime by the plugin
Let AI Do It
Bash
I have a ghost MCP entry showing "failed" in OpenCode's TUI that I can't remove.
Reference: https://mjshao.fun/blog/opencode-mcp-ghost-debugging
MCP name: [ask me]
OpenCode port: [default 4096, ask me]
Steps:
1. curl -s http://127.0.0.1:{port}/mcp?directory=$(pwd) to see actual merged MCP state
2. Check ~/.claude.json mcpServers for the ghost entry
3. Check ~/.claude/.mcp.json, ./.mcp.json, ./.claude/.mcp.json
4. Remove the entry from whichever file has it
5. Restart OpenCode server and verify the ghost is gone
删了一个 MCP 服务器的配置,它却怎么也不消失。这是一个关于隐藏配置合并层的调试故事。
问题
Text
ENOENT: no such file or directory, posix_spawn '/opt/homebrew/bin/mcp-obsidian'
大搜索
| 位置 | 结果 |
|---|---|
| ~/.config/opencode/opencode.json | 没有 obsidian |
| opencode.jsonc | 不存在 |
| 项目本地配置 | 不存在 |
| ~/.opencode/opencode.json | 不存在 |
| SQLite 数据库(全部 11 张表) | 没有 MCP 存储 |
| mcp-auth.json | 只有 notion |
| ~/.claude/settings.json | mcpServers: {} (空) |
| oh-my-opencode 配置(5 个文件) | 没有 MCP 配置 |
| OpenCode 二进制文件(strings 扫描) | 没有 obsidian |
| WAL 日志 | 只在消息内容里出现 |
| OPENCODE_CONFIG 环境变量 | 未设置 |
突破口
Bash
curl -s "http://127.0.0.1:4096/mcp?directory=$PWD" | python3 -m json.tool
Json
{
"obsidian": {
"status": "failed",
"error": "ENOENT: no such file or directory, posix_spawn '/opt/homebrew/bin/mcp-obsidian'"
}
}
根因
Javascript
const merged = {
...createBuiltinMcps(disabledMcps, pluginConfig), // websearch, grep_app
...userMcp ?? {}, // 来自 opencode.json
...mcpResult.servers, // 来自 Claude Code 配置文件(!)
...pluginComponents.mcpServers // 来自插件组件
};
Javascript
function getMcpConfigPaths() {
return [
{ path: join(homedir(), ".claude.json"), scope: "user" },
{ path: join(claudeConfigDir, ".mcp.json"), scope: "user" },
{ path: join(cwd, ".mcp.json"), scope: "project" },
{ path: join(cwd, ".claude", ".mcp.json"), scope: "local" }
];
}
修复
Bash
python3 -c "
import json
with open('$HOME/.claude.json', 'r') as f:
data = json.load(f)
if 'obsidian' in data.get('mcpServers', {}):
del data['mcpServers']['obsidian']
with open('$HOME/.claude.json', 'w') as f:
json.dump(data, f, indent=2)
print('已删除 obsidian')
"
为什么 CLI 和 TUI 不一致
- CLI(opencode mcp list)直接读取 OpenCode 的原生配置——不经过插件处理
- TUI 从服务器 API(/mcp)获取数据,而这个 API 会走完整的插件管线,包括 oh-my-openagent 的配置合并
调试清单
- ~/.config/opencode/opencode.json — OpenCode 自己的配置
- ~/.claude.json → mcpServers — Claude Code 的配置(oh-my-openagent 会读取)
- ~/.claude/.mcp.json — Claude Code 的专用 MCP 文件
- {project}/.mcp.json — 项目级 Claude Code MCP 配置
- {project}/.claude/.mcp.json — 本地 Claude Code MCP 配置
- 查询实际合并状态:curl -s http://127.0.0.1:4096/mcp?directory={dir}
结论
- 原生:opencode.json — opencode mcp list 读取的
- 插件注入:~/.claude.json + .mcp.json 文件 — 由插件在运行时合并
让 AI 来搞定
Bash
我在 OpenCode 的 TUI 里有一个幽灵 MCP 条目显示 "failed",怎么都删不掉。
参考:https://mjshao.fun/blog/opencode-mcp-ghost-debugging
MCP 名称:[问我]
OpenCode 端口:[默认 4096,问我]
步骤:
1. curl -s http://127.0.0.1:{port}/mcp?directory=$(pwd) 查看实际合并的 MCP 状态
2. 检查 ~/.claude.json 的 mcpServers 是否有幽灵条目
3. 检查 ~/.claude/.mcp.json、./.mcp.json、./.claude/.mcp.json
4. 从找到条目的文件中删除它
5. 重启 OpenCode 服务器并验证幽灵消失