Asia/Shanghai
March 23, 2026

The MCP Ghost: Hunting a Config Entry That Wouldn't Die

MCP 幽灵:追踪一个怎么也删不掉的配置项

Mingjian Shao
The MCP Ghost: Hunting a Config Entry That Wouldn't Die
I removed an MCP server from my config. It kept coming back. Here's a debugging story about hidden config merge layers.
After uninstalling the Obsidian MCP server binary (npm uninstall -g @mauricio.wolff/mcp-obsidian), the entry kept showing as "failed" in OpenCode's MCP panel with the error:
Text
ENOENT: no such file or directory, posix_spawn '/opt/homebrew/bin/mcp-obsidian'
I removed it from ~/.config/opencode/opencode.json. Restarted the server. Kill -9'd all processes. The ghost remained.What followed was an exhaustive hunt through every config source I could find:
LocationResult
~/.config/opencode/opencode.jsonNo obsidian entry
opencode.jsoncDoesn't exist
Project-local configsDon't exist
~/.opencode/opencode.jsonDoesn't exist
SQLite database (all 11 tables)No MCP storage
mcp-auth.jsonOnly notion
~/.claude/settings.jsonmcpServers: {} (empty)
oh-my-opencode profiles (5 files)No MCP config
OpenCode binary (strings scan)No obsidian
WAL journalOnly in message content
OPENCODE_CONFIG env varNot set
Everything came back clean. Yet opencode mcp list (CLI) showed 7 servers — no obsidian. The TUI showed 8 — including the ghost. The CLI and TUI disagreed.I queried the server's actual API directly:
Bash
curl -s "http://127.0.0.1:4096/mcp?directory=$PWD" | python3 -m json.tool
The response included:
Json
{
  "obsidian": {
    "status": "failed",
    "error": "ENOENT: no such file or directory, posix_spawn '/opt/homebrew/bin/mcp-obsidian'"
  }
}
The server was returning obsidian. Which meant Config.get() was finding it somewhere. But where?I traced through the OpenCode source code and found the oh-my-openagent plugin's mcp-config-handler.ts. Here's the merge logic:
Javascript
const merged = {
  ...createBuiltinMcps(disabledMcps, pluginConfig),  // websearch, grep_app
  ...userMcp ?? {},                                    // from opencode.json
  ...mcpResult.servers,                                // from Claude Code configs (!)
  ...pluginComponents.mcpServers                       // from plugin components
};
The mcpResult.servers comes from loadMcpConfigs(), which reads Claude Code's config files:
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 oh-my-openagent plugin bridges Claude Code and OpenCode by merging their MCP configs. And ~/.claude.json had a root-level mcpServers object containing the obsidian entry — left over from when I used Claude Code directly.
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')
"
Restart OpenCode. Ghost gone.This confused me the most. opencode mcp list showed no obsidian, but the TUI did. The reason:
  • 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
They see different config layers.If you hit an MCP ghost entry in OpenCode, check these in order:
  • ~/.config/opencode/opencode.json — OpenCode's own config
  • ~/.claude.jsonmcpServers — 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}
OpenCode with oh-my-openagent has two config systems that affect MCP:
  • Native: opencode.json — what opencode mcp list reads
  • Plugin-injected: ~/.claude.json + .mcp.json files — merged at runtime by the plugin
When you remove an MCP from one but not the other, you get a ghost. The fix is knowing both systems exist.If you use an AI coding assistant like OpenCode, paste this prompt to debug your own MCP ghost:
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 服务器的配置,它却怎么也不消失。这是一个关于隐藏配置合并层的调试故事。
卸载 Obsidian MCP 服务器二进制文件(npm uninstall -g @mauricio.wolff/mcp-obsidian)后,这个条目一直在 OpenCode 的 MCP 面板里显示 "failed"
Text
ENOENT: no such file or directory, posix_spawn '/opt/homebrew/bin/mcp-obsidian'
我从 ~/.config/opencode/opencode.json 里删了它。重启服务器。kill -9 所有进程。幽灵依然存在。接下来是一场穷举式的配置文件搜索:
位置结果
~/.config/opencode/opencode.json没有 obsidian
opencode.jsonc不存在
项目本地配置不存在
~/.opencode/opencode.json不存在
SQLite 数据库(全部 11 张表)没有 MCP 存储
mcp-auth.json只有 notion
~/.claude/settings.jsonmcpServers: {} (空)
oh-my-opencode 配置(5 个文件)没有 MCP 配置
OpenCode 二进制文件(strings 扫描)没有 obsidian
WAL 日志只在消息内容里出现
OPENCODE_CONFIG 环境变量未设置
全部干净。但 opencode mcp list(CLI)显示 7 个服务器——没有 obsidian。TUI 显示 8 个——包括那个幽灵。CLI 和 TUI 给出了不同的结果。直接查询服务器的 API:
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'"
  }
}
服务器确实在返回 obsidian。说明 Config.get() 从某个地方找到了它。但到底是哪里?我翻了 OpenCode 的源码,找到了 oh-my-openagent 插件的 mcp-config-handler.ts。合并逻辑如下:
Javascript
const merged = {
  ...createBuiltinMcps(disabledMcps, pluginConfig),  // websearch, grep_app
  ...userMcp ?? {},                                    // 来自 opencode.json
  ...mcpResult.servers,                                // 来自 Claude Code 配置文件(!)
  ...pluginComponents.mcpServers                       // 来自插件组件
};
mcpResult.servers 来自 loadMcpConfigs(),它会读取 Claude Code 的配置文件
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" }
  ];
}
oh-my-openagent 插件通过合并 Claude Code 和 OpenCode 的 MCP 配置来桥接两者。而 ~/.claude.json 的根级 mcpServers 对象里包含了 obsidian 条目——这是我之前直接用 Claude Code 时留下的。
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')
"
重启 OpenCode。幽灵消失。这是最让我困惑的部分。opencode mcp list 不显示 obsidian,但 TUI 显示。原因:
  • CLIopencode mcp list)直接读取 OpenCode 的原生配置——不经过插件处理
  • TUI 从服务器 API(/mcp)获取数据,而这个 API 会走完整的插件管线,包括 oh-my-openagent 的配置合并
它们看到的是不同的配置层。如果你在 OpenCode 里遇到 MCP 幽灵条目,按这个顺序检查:
  • ~/.config/opencode/opencode.json — OpenCode 自己的配置
  • ~/.claude.jsonmcpServers — 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}
使用 oh-my-openagent 的 OpenCode 有 两套配置系统 影响 MCP:
  • 原生opencode.jsonopencode mcp list 读取的
  • 插件注入~/.claude.json + .mcp.json 文件 — 由插件在运行时合并
当你从一个系统删除了 MCP 但另一个没删,就会出现幽灵。解决方法就是知道两个系统都存在。如果你用 OpenCode 之类的 AI 编程助手,粘贴这个 prompt 来调试你自己的 MCP 幽灵:
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 服务器并验证幽灵消失
Share this post:
Enjoy this post? Subscribe via RSS: English | 中文