MCP JSON 配置草稿生成器

生成 mcpServers 结构的本地进程启动配置草稿;导入前请核对目标客户端格式、路径与权限。

点击生成

用途与使用方法

适合整理使用 mcpServers JSON 结构的本地进程启动参数。不同客户端的配置入口并不统一,文件名相同也不意味着字段兼容。

  1. 填写 Server 名、command,以及表单注明按空格分隔的 args。
  2. 点击“生成”,检查 mcpServers 下的服务名、command 和逐项 args。
  3. 复制或使用“下载 mcp.json”入口后,按目标客户端文档放入正确位置;先核对路径、程序来源与允许访问的目录,再连接测试。

默认输入与实际输出

以下为 2026-10-05 使用页面内置合成示例的浏览器核对记录;没有执行生成的命令、配置或技能。

mcp-name: filesystem
mcp-cmd: npx
mcp-args: -y @modelcontextprotocol/server-filesystem /tmp
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/tmp"
      ]
    }
  }
}

适用边界

  • 默认结果是 mcpServers JSON。Codex 原生配置使用 config.toml 中的 mcp_servers 表,不能仅把本页结果改名就当成 Codex 配置。
  • 默认 npx 与 /tmp 是示例,不证明本机依赖存在、包可信或目录授权合适;生成文本不会安装或运行服务器。
  • args 表单写明空格分隔。带空格路径、引号、转义与 shell 表达式的处理未验证,必要时在下载结果中逐项修正 JSON 数组。
  • 页面没有 HTTP URL、OAuth、环境变量或超时选项;本次未验证这些类型,也没有执行下载、导入、进程启动或真实连通性测试。
  • 不要把真实令牌或密码放进 command、args 或截图;遵循目标客户端的凭据管理方式。

常见问题

这就是 Codex 的 config.toml 吗?

不是。本页生成 mcpServers JSON;Codex 原生 MCP 配置应按其 TOML 文档整理。

生成成功代表服务已接通吗?

不代表。仍需在正确客户端确认启动、初始化、工具列表与一次已知输入的调用。

路径里有空格怎么办?

不要假设输入框的空格拆分理解 shell 引号。检查输出 args 的每个元素,按目标程序实际参数边界修正。

继续阅读

请只使用脱敏示例,不要粘贴密钥或保密资料。站点的数据处理声明见隐私政策;本次核对没有进行独立网络行为审计。

人工校对练习:一个目录名里有空格

假设你只打算让 filesystem 访问一个专用测试目录 /tmp/mcp demo,目录内只有一份合成文件。以下是手工编写的参数边界反例与参考配置,不是本页对带空格输入的实测输出,也不表示目录已存在或服务已启动。

先数参数,再看 JSON 是否有效

下面的错误数组在 JSON 语法上可以成立,但把一个目录拆成了两个参数。即使客户端接受 JSON,也没有表达原本的目录范围。

["-y", "@modelcontextprotocol/server-filesystem", "/tmp/mcp", "demo"]

本练习要表达的是三个数组元素:第 1 项是 npx 选项,第 2 项是包名,第 3 项才是完整目录。正确边界的完整 JSON 参考如下,仅面向使用 mcpServers 结构的客户端:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/tmp/mcp demo"
      ]
    }
  }
}

JSON 字符串两端的双引号负责界定字符串;目录中的空格仍属于第三个元素。不要额外把 shell 用的引号字符塞进目录值,也不要把整条命令放进 command。这个例子不要求你把带引号文本重新粘进本页 args 输入框:当前输入框的引号处理仍未验证,参考修订应在生成后的 JSON 文本中人工完成。

配置审阅与连接验收,分别留什么证据?

  1. 程序来源:记录目标客户端、服务器包及实际版本,确认本机将使用哪个启动程序。示例未固定包版本,不能作为可重复安装清单;不要因 JSON 生成成功就批准下载或运行来源不明的程序。
  2. 目录范围:先确认专用目录存在且没有真实私密文件。按服务器官方说明,客户端提供的 Roots 可能替换命令行目录;连接后还要核对实际允许目录,不能只看 args。
  3. 参数边界:核对 args 为三个字符串,第三项完整等于 /tmp/mcp demo。若看到 /tmp/mcp 与 demo 分成两项,就先修正;JSON 能解析仅证明语法成立。
  4. 客户端格式:确认此客户端的配置文件位置和顶层字段确为 mcpServers;已有其他服务时合并指定服务项,不覆盖整个配置。使用其他格式的客户端须另按官方文档转换。
  5. 连接证据:在你授权的测试环境连接后,分别记录进程是否启动、MCP 初始化是否完成、客户端是否取得工具列表。这三个阶段不能用同一张“已保存配置”截图代替。
  6. 最小调用证据:若工具列表包含 list_allowed_directories,核对其返回的实际目录;再对测试目录中的已知合成文件做一次获准的只读调用,记录输入路径、返回内容与预期是否一致。没有这些记录时,结论应停留在“配置待验证”。

本练习的 JSON 结构、目录参数和 Roots 说明参考 MCP 官方 filesystem 服务器说明(2026-10-13 查阅的 main 分支文档)。文档会更新,实际工具名称与行为仍以所用版本为准。本次只补充人工审阅示例,没有下载配置、安装包、启动服务器或发起调用。

生成启动配置

填写服务名称、command 和 args,然后点击生成。输出的 mcpServers 对象以服务名称为键,记录启动命令和参数数组。你可以核对 JSON 后下载 mcp.json,再按目标客户端要求放入配置。

参数示例

服务名称填写 filesystem,command 填写 npx,args 填写 -y @modelcontextprotocol/server-filesystem /tmp,会得到三个独立参数。当前工具按空白拆分 args;带空格的路径或需要保留引号的参数,应在生成的数组里手动调整。客户端若使用其他配置格式,需要按其说明转换。

离线合并案例:新增一个服务,原来两个对象不能消失

旧服务与顶层设置保留,新增服务逐项合并,遇到重复键或重名差异停止
本例只生成候选;保留未知字段不等于批准其权限或验证客户端兼容性。

生成器交付的是一份配置片段。实际接入时,已有文件通常已经包含其他服务,甚至还有客户端自己的顶层设置。如果用一次顶层 update 把新片段盖上去,整个 mcpServers 对象可能被替换:新增服务还在,旧服务却消失了。另一种更难发现的情况是原始文本写了两次同名键,普通 JSON 解析后只剩其中一个,审阅者已看不到丢失的那份定义。

下面提供一个独立的 Python 标准库审阅器。它读取两份脱敏文件,输出合并候选与审阅清单,不修改原件、不导入客户端、不启动配置里的命令。案例固定为向已有 calendar-demo 服务旁边加入 fixture-reader;editor 是合成的客户端自定义字段,只用于证明无关顶层内容被保留,并非 Claude Code 或其他客户端必需或接受的字段。

这是一项“配置文件合并”实验,不是 MCP 连通性实验。example.invalid 地址、目录和服务名全是合成输入;probe.py 只回显参数,不实现 MCP。不要把案例当成可直接接入的服务器配置。它补足生成后的离线校对步骤,不改变本页表单行为。

合同先写清:仅新增一个 command/args 服务,不偷偷覆盖

基础文件必须是含对象型 mcpServers 的 JSON 对象;其余字段按解析后的值保留。增量文件顶层只能有 mcpServers,里面恰好一个服务;新增服务只接受 command 与 args。这样,试图顺带改顶层设置或传入 env、HTTP URL、客户端专属选项会被拒绝。本例刻意只覆盖页面产生的本地启动子集,不是完整客户端配置 Schema。已有服务作为不透明 JSON 值保留,不意味着已通过有效性审查。

同名服务内容完全相同,返回 unchanged;同名而内容不同,返回 name-conflict,要求人做决定。比较是 JSON 解析后的值比较,不比较空格与键顺序。不提供自动覆盖开关,也不把同名冲突解释成重试即可成功。

两份原始 JSON 的任意对象出现重复键都拒绝;非 JSON 的 NaN/Infinity 常量也拒绝。args 必须是字符串数组,空字符串允许,NUL 字符拒绝;不做 shell 拆词、引号补全、通配符展开或变量替换。命令字段只是非空文本检查,不能证明可执行文件可信、存在或权限合适。

输出保留的是数据语义,不保留源文件排版。输入采用 UTF-8;本实现限定每份输入最多 65,536 字节、结构嵌套最多 32 层,超出分别拒绝为 input-too-large、input-too-deep;指数溢出为无穷的数值拒绝为 number-range。JSON 小数采用 Python float 精度,不适合承载必须逐字保持的高精度数值;原始数字写法与排版不会保留。candidate 会包含基础文件原有字段,因此请只用脱敏副本,勿把真实凭据或个人配置打印到共享日志。

完整合成输入:空格、空参数、通配符和中文都留在数组里

在新的练习目录保存下面两份 JSON,各自最后保留一个换行。基础文件有一个旧服务和一个无关设置。增量有六个 args 元素;其中 probe.py 是脚本路径,后面五项才是探针看到的业务参数。参数里的 ${DEMO_DIR} 在本实验中只是字面文本。

不要把这些参数重新填入本页按空白拆分的输入框来代替这份 JSON;本节测试的是离线数组合并。目标客户端有自己的变量展开规则,探针保住字面文本不等于所有客户端都会原样传递。

base.json

{"mcpServers":{"calendar-demo":{"type":"http","url":"https://example.invalid/mcp"}},"editor":{"theme":"dark"}}

patch.json

{"mcpServers":{"fixture-reader":{"command":"python3","args":["probe.py","/tmp/mcp demo","","*.txt","${DEMO_DIR}","中文"]}}}

可复用审阅器:只读输入,候选输出到标准输出

保存为 config_merge.py。review 接收原始 bytes,先完成解析与冲突检查,再复制基础对象并添加服务。SHA-256 绑定的是本次读取的原始字节,方便审阅人核对用了哪一版文件;它不是签名、授权记录或锁,也不能阻止检查之后文件又被其他进程修改。

退出码 0 表示生成候选;退出码 2 表示受控输入错误,错误 JSON 只写 stderr,不产生成功候选。程序从不写回 base 或 patch。执行时只指定本练习目录的文件,勿直接指向真实客户端配置。

config_merge.py

"""Offline JSON review only. Never starts a configured command or writes inputs."""
import argparse
import copy
import hashlib
import json
import math
import re
import sys
from pathlib import Path

class Rejected(ValueError):
    pass

def unique(pairs):
    out = {}
    for key, value in pairs:
        if key in out:
            raise Rejected('duplicate-key')
        out[key] = value
    return out

def bad_constant(value):
    raise Rejected('non-json-number')

MAX_BYTES = 65536
MAX_DEPTH = 32

def finite_float(token):
    value = float(token)
    if not math.isfinite(value):
        raise Rejected('number-range')
    return value

def parse(raw):
    if len(raw) > MAX_BYTES:
        raise Rejected('input-too-large')
    try:
        text = raw.decode('utf-8')
        # Count structural depth outside strings before recursive JSON parsing.
        depth = 0
        quoted = escaped = False
        for char in text:
            if quoted:
                if escaped:
                    escaped = False
                elif char == '\\':
                    escaped = True
                elif char == '"':
                    quoted = False
            elif char == '"':
                quoted = True
            elif char in '[{':
                depth += 1
                if depth > MAX_DEPTH:
                    raise Rejected('input-too-deep')
            elif char in ']}':
                depth -= 1
        return json.loads(text, object_pairs_hook=unique,
                          parse_constant=bad_constant, parse_float=finite_float)
    except (UnicodeError, json.JSONDecodeError, ValueError) as exc:
        if isinstance(exc, Rejected):
            raise
        raise Rejected('invalid-json') from exc

def review(base_raw, patch_raw):
    base, patch = parse(base_raw), parse(patch_raw)
    if not isinstance(base, dict) or not isinstance(base.get('mcpServers'), dict):
        raise Rejected('base-shape')
    if not isinstance(patch, dict) or set(patch) != {'mcpServers'}:
        raise Rejected('patch-shape')
    additions = patch['mcpServers']
    if not isinstance(additions, dict) or len(additions) != 1:
        raise Rejected('one-server-required')
    name, spec = next(iter(additions.items()))
    if not re.fullmatch(r'[A-Za-z0-9_-]+', name):
        raise Rejected('server-name')
    if not isinstance(spec, dict) or set(spec) != {'command', 'args'}:
        raise Rejected('local-stdio-subset-only')
    if not isinstance(spec['command'], str) or not spec['command'].strip():
        raise Rejected('command-string')
    args = spec['args']
    if not isinstance(args, list) or any(not isinstance(x, str) for x in args):
        raise Rejected('args-string-array')
    if any('\x00' in s for s in [spec['command'], *args]):
        raise Rejected('nul-character')
    existing = base['mcpServers']
    if name in existing and existing[name] != spec:
        raise Rejected('name-conflict')
    action = 'unchanged' if name in existing else 'add'
    candidate = copy.deepcopy(base)
    candidate['mcpServers'][name] = copy.deepcopy(spec)
    return {
        'review': {
            'action': action,
            'server': name,
            'before': sorted(existing),
            'after': sorted(candidate['mcpServers']),
            'args_count': len(args),
            'base_sha256': hashlib.sha256(base_raw).hexdigest(),
            'patch_sha256': hashlib.sha256(patch_raw).hexdigest(),
            'client_validated': False,
            'server_started': False,
        },
        'candidate': candidate,
    }

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('base')
    parser.add_argument('patch')
    args = parser.parse_args()
    try:
        result = review(Path(args.base).read_bytes(), Path(args.patch).read_bytes())
    except Rejected as exc:
        print(json.dumps({'error': str(exc)}), file=sys.stderr)
        return 2
    except OSError:
        print('{"error":"input-read-failed"}', file=sys.stderr)
        return 2
    print(json.dumps(result, ensure_ascii=True, indent=2, allow_nan=False))
    return 0

if __name__ == '__main__':
    raise SystemExit(main())

运行与完整输出:review 包装不能整份当成客户端配置

在装有 Python 3 的环境中运行下列命令。实际记录来自 2026-10-14 的 Linux、CPython 3.12.14,未安装第三方依赖。命令的重定向只保存一份新的 review.json;不要把输出文件名设成任一输入文件名,shell 可能在程序读取前就截断该文件。

下方是这两份完整输入对应的实测输出。人工核对 before 仍有 calendar-demo,after 多出 fixture-reader,editor 值未变,六个参数逐项未变。review 是本例自己的报告包装;只有 candidate 才是候选配置对象,不能把整个输出直接作为客户端配置。

如果更换换行格式或调整输入缩进,SHA-256 会变化,即使解析后的候选值相同。正式应用前仍需重新读取当前基础文件,比较本次记录的哈希;发生变化就重新生成并审阅候选。本文没有实现原子写入、备份恢复或客户端导入。

命令

python3 config_merge.py base.json patch.json > review.json

实测 stdout

{
  "review": {
    "action": "add",
    "server": "fixture-reader",
    "before": [
      "calendar-demo"
    ],
    "after": [
      "calendar-demo",
      "fixture-reader"
    ],
    "args_count": 6,
    "base_sha256": "842465074e34be4aa6a0b9ddf15c68edc5c06faa4de03c2b7526a6fb2f7dc9d1",
    "patch_sha256": "fdce45c6266cff4ebfe198d579c45822d5090aba511053f273961bdcc3b1e0f3",
    "client_validated": false,
    "server_started": false
  },
  "candidate": {
    "mcpServers": {
      "calendar-demo": {
        "type": "http",
        "url": "https://example.invalid/mcp"
      },
      "fixture-reader": {
        "command": "python3",
        "args": [
          "probe.py",
          "/tmp/mcp demo",
          "",
          "*.txt",
          "${DEMO_DIR}",
          "\u4e2d\u6587"
        ]
      }
    },
    "editor": {
      "theme": "dark"
    }
  }
}

独立验收:50 项固定断言与两个故意写错的合并策略

保存下面的 probe.py 与 test_merge.py。expected.json 是人工固定的完整输出对象,包含报告与候选;EXPECTED 另对候选进行交叉核对。测试不会调用 review 来生成自己的标准答案。两个输入哈希也固定写入测试;正常候选、幂等合并、失败后的修复、CLI 成败输出与输入原件保留分别检查。

负对照一故意使用顶层 dict.update,确认它丢掉 calendar-demo,不能通过完整对象标准答案;负对照二故意把参数 join 再 split,确认它破坏空格路径与空参数。重复键、非 JSON 常量、错误形状、非字符串参数、NUL、无效 UTF-8、同名冲突另有明确错误码断言。

唯一的参数执行测试使用当前 Python 的 sys.executable 和本节固定 probe.py 的绝对路径,shell=False。它没有执行 JSON 中的 command,也没有把任意文件内容当成程序。返回值必须精确等于五个手写参数;不是只检查进程退出码。另用 mutation_test.py 在临时目录实际改坏实现:替换服务表、拆散参数、容许重复键、覆盖重名冲突。四种变异均分别在 normal 与 -O 下被断言拦住;先编译变异源码,避免把语法错误当成测试成功。测试使用显式抛错,不依赖会被 -O 移除的 assert 语句。Windows、macOS 和实际客户端的参数传递没有实测。

expected.json:固定完整输出标准答案

{
  "review": {
    "action": "add",
    "server": "fixture-reader",
    "before": [
      "calendar-demo"
    ],
    "after": [
      "calendar-demo",
      "fixture-reader"
    ],
    "args_count": 6,
    "base_sha256": "842465074e34be4aa6a0b9ddf15c68edc5c06faa4de03c2b7526a6fb2f7dc9d1",
    "patch_sha256": "fdce45c6266cff4ebfe198d579c45822d5090aba511053f273961bdcc3b1e0f3",
    "client_validated": false,
    "server_started": false
  },
  "candidate": {
    "mcpServers": {
      "calendar-demo": {
        "type": "http",
        "url": "https://example.invalid/mcp"
      },
      "fixture-reader": {
        "command": "python3",
        "args": [
          "probe.py",
          "/tmp/mcp demo",
          "",
          "*.txt",
          "${DEMO_DIR}",
          "\u4e2d\u6587"
        ]
      }
    },
    "editor": {
      "theme": "dark"
    }
  }
}

probe.py

import json
import sys
print(json.dumps(sys.argv[1:], ensure_ascii=True))

test_merge.py

import copy
import json
import subprocess
import sys
from pathlib import Path
from config_merge import Rejected, review

HERE = Path(__file__).resolve().parent
BASE = (HERE / 'base.json').read_bytes()
PATCH = (HERE / 'patch.json').read_bytes()
EXPECTED = {
    'mcpServers': {
        'calendar-demo': {'type': 'http', 'url': 'https://example.invalid/mcp'},
        'fixture-reader': {'command': 'python3', 'args': [
            'probe.py', '/tmp/mcp demo', '', '*.txt', '${DEMO_DIR}', '中文']},
    },
    'editor': {'theme': 'dark'},
}
COUNT = 0

def check(condition):
    global COUNT
    if not condition:
        raise AssertionError('fixed oracle mismatch')
    COUNT += 1

def raw(value):
    return json.dumps(value, ensure_ascii=True).encode()

def reject(base, patch, code):
    try:
        review(base, patch)
    except Rejected as exc:
        check(str(exc) == code)
    else:
        raise AssertionError('invalid input accepted')

result = review(BASE, PATCH)
check(result == json.loads((HERE / 'expected.json').read_text()))
check(result['candidate'] == EXPECTED)
check(result['review']['before'] == ['calendar-demo'])
check(result['review']['after'] == ['calendar-demo', 'fixture-reader'])
check(result['review']['action'] == 'add')
check(result['review']['args_count'] == 6)
check(result['review']['client_validated'] is False)
check(result['review']['server_started'] is False)
check((HERE / 'base.json').read_bytes() == BASE)
check((HERE / 'patch.json').read_bytes() == PATCH)
check(review(raw(EXPECTED), PATCH)['review']['action'] == 'unchanged')
check(review(raw(EXPECTED), PATCH)['candidate'] == EXPECTED)
reject(BASE, b'{"mcpServers":{},"mcpServers":{}}', 'duplicate-key')
reject(BASE, b'{"mcpServers":{"a":{"command":"x","command":"y","args":[]}}}', 'duplicate-key')
reject(b'{"mcpServers":{},"x":NaN}', PATCH, 'non-json-number')
reject(b'{"mcpServers":[]}', PATCH, 'base-shape')
reject(BASE, b'[]', 'patch-shape')
reject(BASE, b'{"mcpServers":{},"editor":{}}', 'patch-shape')
reject(BASE, b'{"mcpServers":{}}', 'one-server-required')
reject(BASE, b'{"mcpServers":{"a":{},"b":{}}}', 'one-server-required')
reject(BASE, b'{"mcpServers":{"bad name":{"command":"x","args":[]}}}', 'server-name')
reject(BASE, b'{"mcpServers":{"x":{"url":"https://example.invalid"}}}', 'local-stdio-subset-only')
reject(BASE, b'{"mcpServers":{"x":{"command":" ","args":[]}}}', 'command-string')
reject(BASE, b'{"mcpServers":{"x":{"command":"x","args":"one two"}}}', 'args-string-array')
reject(BASE, b'{"mcpServers":{"x":{"command":"x","args":[3]}}}', 'args-string-array')
reject(BASE, b'{"mcpServers":{"x":{"command":"x","args":["\\u0000"]}}}', 'nul-character')
reject(BASE, b'{bad', 'invalid-json')
reject(BASE, b'\xff', 'invalid-json')
conflict = copy.deepcopy(EXPECTED)
conflict['mcpServers']['fixture-reader']['args'] = ['old.py']
reject(raw(conflict), PATCH, 'name-conflict')
# Recovery: explicitly choose a distinct name; the older service is preserved.
renamed = {'mcpServers': {'fixture-reader-v2': EXPECTED['mcpServers']['fixture-reader']}}
recovered = review(raw(conflict), raw(renamed))
check(recovered['candidate']['mcpServers']['fixture-reader']['args'] == ['old.py'])
check(recovered['review']['after'] == ['calendar-demo', 'fixture-reader', 'fixture-reader-v2'])
# Two deliberately defective implementations must disagree with the fixed oracle.
shallow = json.loads(BASE)
shallow.update(json.loads(PATCH))
check(shallow != EXPECTED)
check('calendar-demo' not in shallow['mcpServers'])
broken_args = ' '.join(EXPECTED['mcpServers']['fixture-reader']['args']).split()
check(broken_args != EXPECTED['mcpServers']['fixture-reader']['args'])
# Run only OUR fixed probe with the CURRENT Python, never the config's command.
received = subprocess.run(
    [sys.executable, str(HERE / 'probe.py'), *result['candidate']['mcpServers']['fixture-reader']['args'][1:]],
    check=True, capture_output=True, text=True, shell=False, timeout=5)
check(json.loads(received.stdout) == ['/tmp/mcp demo', '', '*.txt', '${DEMO_DIR}', '中文'])
# CLI failure: no successful result on stdout and no source modification.
failed = subprocess.run([sys.executable, str(HERE / 'config_merge.py'),
                         str(HERE / 'base.json'), str(HERE / 'base.json')],
                        capture_output=True, text=True, timeout=5)
check(failed.returncode == 2)
check(failed.stdout == '')
check(json.loads(failed.stderr) == {'error': 'patch-shape'})
check((HERE / 'base.json').read_bytes() == BASE)
check(result['review']['base_sha256'] == '842465074e34be4aa6a0b9ddf15c68edc5c06faa4de03c2b7526a6fb2f7dc9d1')
check(result['review']['patch_sha256'] == 'fdce45c6266cff4ebfe198d579c45822d5090aba511053f273961bdcc3b1e0f3')
success = subprocess.run([sys.executable, str(HERE / 'config_merge.py'),
                          str(HERE / 'base.json'), str(HERE / 'patch.json')],
                         capture_output=True, text=True, timeout=5)
check(success.returncode == 0)
check(success.stderr == '')
check(json.loads(success.stdout)['candidate'] == EXPECTED)
# Additional boundaries and full-object recovery oracle.
reject(b'{"mcpServers":{},"n":1e999}', PATCH, 'number-range')
reject(b' ' * 65537, PATCH, 'input-too-large')
reject(b'[' * 33 + b']' * 33, PATCH, 'input-too-deep')
reject(b'{"mcpServers":{},"x":{"k":1,"k":2}}', PATCH, 'duplicate-key')
unknown = json.loads(BASE)
unknown['vendor'] = {'enabled': False, 'items': [None, 7, 'literal'], 'nested': {'brace': '{["'}}
unknown['mcpServers']['opaque'] = {'unknown': [1, 'x']}
unknown_expected = copy.deepcopy(EXPECTED)
unknown_expected['vendor'] = copy.deepcopy(unknown['vendor'])
unknown_expected['mcpServers']['opaque'] = {'unknown': [1, 'x']}
check(review(raw(unknown), PATCH)['candidate'] == unknown_expected)
recovery_expected = copy.deepcopy(conflict)
recovery_expected['mcpServers']['fixture-reader-v2'] = copy.deepcopy(EXPECTED['mcpServers']['fixture-reader'])
check(recovered['candidate'] == recovery_expected)
print(f'PASS {COUNT} fixed checks; 2 defective merge strategies rejected; argv probe matched')

mutation_test.py:实际改坏实现后重跑测试

"""Run deliberately broken copies in disposable folders; never edit the baseline."""
import json
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile

HERE = Path(__file__).resolve().parent
source = (HERE / 'config_merge.py').read_text()
mutations = {
    'replace-service-map': ("candidate['mcpServers'][name] = copy.deepcopy(spec)", "candidate['mcpServers'] = {name: copy.deepcopy(spec)}"),
    'split-argv': ("candidate['mcpServers'][name] = copy.deepcopy(spec)", "candidate['mcpServers'][name] = copy.deepcopy(spec)\n    candidate['mcpServers'][name]['args'] = ' '.join(args).split()"),
    'allow-duplicate-keys': ("if key in out:", "if False and key in out:"),
    'overwrite-conflict': ("if name in existing and existing[name] != spec:", "if False and name in existing and existing[name] != spec:"),
}
results = []
for name, (old, new) in mutations.items():
    if source.count(old) != 1:
        raise RuntimeError('mutation anchor is not unique: ' + name)
    mutated = source.replace(old, new)
    compile(mutated, name, 'exec')
    with tempfile.TemporaryDirectory(prefix='merge-negative-') as folder:
        target = Path(folder)
        for file in ('test_merge.py', 'base.json', 'patch.json', 'expected.json', 'probe.py'):
            shutil.copyfile(HERE / file, target / file)
        (target / 'config_merge.py').write_text(mutated)
        for flags in ([], ['-O']):
            run = subprocess.run([sys.executable, *flags, str(target / 'test_merge.py')], capture_output=True, text=True, timeout=20)
            if run.returncode != 1 or 'AssertionError' not in run.stderr:
                raise RuntimeError('mutant survived or failed unexpectedly: ' + name + '\n' + run.stderr)
            results.append({'mutation': name, 'mode': '-O' if flags else 'normal', 'result': 'rejected by test assertion'})
print(json.dumps(results, indent=2))

运行与实测结果

python3 test_merge.py
python3 -O test_merge.py
PASS 50 fixed checks; 2 defective merge strategies rejected; argv probe matched
PASS 50 fixed checks; 2 defective merge strategies rejected; argv probe matched

四种变异的实际运行记录

[
  {
    "mutation": "replace-service-map",
    "mode": "normal",
    "result": "rejected by test assertion"
  },
  {
    "mutation": "replace-service-map",
    "mode": "-O",
    "result": "rejected by test assertion"
  },
  {
    "mutation": "split-argv",
    "mode": "normal",
    "result": "rejected by test assertion"
  },
  {
    "mutation": "split-argv",
    "mode": "-O",
    "result": "rejected by test assertion"
  },
  {
    "mutation": "allow-duplicate-keys",
    "mode": "normal",
    "result": "rejected by test assertion"
  },
  {
    "mutation": "allow-duplicate-keys",
    "mode": "-O",
    "result": "rejected by test assertion"
  },
  {
    "mutation": "overwrite-conflict",
    "mode": "normal",
    "result": "rejected by test assertion"
  },
  {
    "mutation": "overwrite-conflict",
    "mode": "-O",
    "result": "rejected by test assertion"
  }
]

失败到恢复:同名冲突应由人命名,不能靠覆盖消失

测试中先构造一个已有 fixture-reader、args 为 ["old.py"] 的基础配置;再提交本文新片段,必须得到 name-conflict。修复由操作者明确选择新名字 fixture-reader-v2,保持旧服务原样。测试断言修复后的三个名字为 calendar-demo、fixture-reader、fixture-reader-v2,并且旧 fixture-reader 仍指向 old.py。这里的恢复是重新形成可审阅候选,未在任何客户端增删服务。

若 duplicate-key 失败,回到原始文本找到两处定义,人工决定保留哪份;不要先用普通 json.loads 再导出来“清理”,那会在你看见冲突之前丢掉一份内容。若 args-string-array 失败,恢复逐项数组;不要对整条字符串自动 split。若客户端实际连不上,回到程序路径、运行目录、客户端格式与初始化阶段分别取证,不能用本工具 PASS 替代连接证据。

真正需要替换旧服务时,本练习的新增合同不适用。先在目标客户端核对配置作用域和实际生效项,准备旧值备份与单项替换方案,由操作者审阅后处理。多个作用域可能出现相同服务名,本工具只看两份输入文件,不能判断哪一个定义最终生效。

conflict-base.json

{
  "mcpServers": {
    "calendar-demo": {
      "type": "http",
      "url": "https://example.invalid/mcp"
    },
    "fixture-reader": {
      "command": "python3",
      "args": [
        "old.py"
      ]
    }
  },
  "editor": {
    "theme": "dark"
  }
}

conflict-result.json

{
  "exit_code": 2,
  "stdout": "",
  "stderr": "{\"error\": \"name-conflict\"}\n"
}

renamed-patch.json

{
  "mcpServers": {
    "fixture-reader-v2": {
      "command": "python3",
      "args": [
        "probe.py",
        "/tmp/mcp demo",
        "",
        "*.txt",
        "${DEMO_DIR}",
        "\u4e2d\u6587"
      ]
    }
  }
}

recovered.json

{
  "review": {
    "action": "add",
    "server": "fixture-reader-v2",
    "before": [
      "calendar-demo",
      "fixture-reader"
    ],
    "after": [
      "calendar-demo",
      "fixture-reader",
      "fixture-reader-v2"
    ],
    "args_count": 6,
    "base_sha256": "2c4ce1b241a56484ad9999b9068883536cb18d55b07d764ee3e63bf767478eb7",
    "patch_sha256": "b20716b463dca2982171e002ff105883490dc29657b0a8e6396691189dbbe538",
    "client_validated": false,
    "server_started": false
  },
  "candidate": {
    "mcpServers": {
      "calendar-demo": {
        "type": "http",
        "url": "https://example.invalid/mcp"
      },
      "fixture-reader": {
        "command": "python3",
        "args": [
          "old.py"
        ]
      },
      "fixture-reader-v2": {
        "command": "python3",
        "args": [
          "probe.py",
          "/tmp/mcp demo",
          "",
          "*.txt",
          "${DEMO_DIR}",
          "\u4e2d\u6587"
        ]
      }
    },
    "editor": {
      "theme": "dark"
    }
  }
}

资料依据与实测边界

Python JSON 文档:重复名称说明默认解析保留最后一个同名值,可用 object_pairs_hook 改变处理;本例据此实现重复键拒绝。Python subprocess 参数文档说明序列参数与平台差异;本例只对当前 Linux 上的固定 Python 探针给出实测结论。

Claude Code MCP 官方说明区分 mcpServers 包装与单服务 add-json 对象,另有配置作用域和状态检查。这说明“JSON 能解析”与“客户端接受且已连接”是不同验收步骤;本文不执行其中的导入命令。以上资料查阅日期为 2026-10-14,页面会更新。

实际完成:Python 离线合并、50 个固定检查、两种故障策略负对照、四种实际源码变异的双模式运行、一次固定本地参数探针、CLI 成功与失败输出。合成内容:全部配置、路径、服务名和输入。未完成或未声称:本页浏览器新测试、配置下载/导入、MCP 服务启动、协议初始化、工具发现、实际目录授权、OAuth、真实凭据处理、生产回滚及跨平台验证。

常见问题

生成后会启动 MCP 服务吗?

工具只生成配置文本。服务是否能启动、目录是否存在,以及客户端怎样加载配置,都需要在实际环境验证。

继续阅读